def __get_merged_api_info(self, services): """Builds a description of an API. Args: services: List of protorpc.remote.Service instances implementing an api/version. Returns: The _ApiInfo object to use for the API that the given services implement. Raises: ApiConfigurationError: If there's something wrong with the API configuration, such as a multiclass API decorated with different API descriptors (see the docstring for api()). """ merged_api_info = services[0].api_info # Verify that, if there are multiple classes here, they're allowed to # implement the same API. for service in services[1:]: if not merged_api_info.is_same_api(service.api_info): raise api_exceptions.ApiConfigurationError( _MULTICLASS_MISMATCH_ERROR_TEMPLATE % (service.api_info.name, service.api_info.version)) return merged_api_info
def __register_class(self, parsed_config): """Register the class implementing this config, so we only add it once. Args: parsed_config: The JSON object with the API configuration being added. Raises: ApiConfigurationError: If the class has already been registered. """ methods = parsed_config.get('methods') if not methods: return # Determine the name of the class that implements this configuration. service_classes = set() for method in methods.itervalues(): rosy_method = method.get('rosyMethod') if rosy_method and '.' in rosy_method: method_class = rosy_method.split('.', 1)[0] service_classes.add(method_class) for service_class in service_classes: if service_class in self.__registered_classes: raise api_exceptions.ApiConfigurationError( 'API class %s has already been registered.' % service_class) self.__registered_classes.add(service_class)
def __get_merged_api_info(self, services): """Builds a description of an API. Args: services: List of protorpc.remote.Service instances implementing an api/version. Returns: The _ApiInfo object to use for the API that the given services implement. """ base_paths = sorted(set(s.api_info.base_path for s in services)) if len(base_paths) != 1: raise api_exceptions.ApiConfigurationError( 'Multiple base_paths found: {!r}'.format(base_paths)) names_versions = sorted( set((s.api_info.name, s.api_info.version) for s in services)) if len(names_versions) != 1: raise api_exceptions.ApiConfigurationError( 'Multiple apis/versions found: {!r}'.format(names_versions)) return services[0].api_info
def __x_security_descriptor(self, audiences, security_definitions): default_auth_issuer = 'google_id_token' if isinstance(audiences, list): if default_auth_issuer not in security_definitions: raise api_exceptions.ApiConfigurationError( _INVALID_AUTH_ISSUER % default_auth_issuer) return [{ default_auth_issuer: { 'audiences': audiences, } }] elif isinstance(audiences, dict): descriptor = list() for audience_key, audience_value in audiences.items(): if audience_key not in security_definitions: raise api_exceptions.ApiConfigurationError( _INVALID_AUTH_ISSUER % audience_key) descriptor.append( {audience_key: { 'audiences': audience_value }}) return descriptor
def __discovery_doc_descriptor(self, services, hostname=None): """Builds a discovery doc for an API. Args: services: List of protorpc.remote.Service instances implementing an api/version. hostname: string, Hostname of the API, to override the value set on the current service. Defaults to None. Returns: A dictionary that can be deserialized into JSON in discovery doc format. Raises: ApiConfigurationError: If there's something wrong with the API configuration, such as a multiclass API decorated with different API descriptors (see the docstring for api()), or a repeated method signature. """ merged_api_info = self.__get_merged_api_info(services) descriptor = self.get_descriptor_defaults(merged_api_info, hostname=hostname) description = merged_api_info.description if not description and len(services) == 1: description = services[0].__doc__ if description: descriptor['description'] = description descriptor['parameters'] = self.__standard_parameters_descriptor() descriptor['auth'] = self.__standard_auth_descriptor() # Add namespace information, if provided if merged_api_info.namespace: descriptor['ownerDomain'] = merged_api_info.namespace.owner_domain descriptor['ownerName'] = merged_api_info.namespace.owner_name descriptor[ 'packagePath'] = merged_api_info.namespace.package_path or '' method_map = {} method_collision_tracker = {} rest_collision_tracker = {} resource_index = collections.defaultdict(list) resource_map = {} # For the first pass, only process top-level methods (that is, those methods # that are unattached to a resource). for service in services: remote_methods = service.all_remote_methods() for protorpc_meth_name, protorpc_meth_info in remote_methods.iteritems( ): method_info = getattr(protorpc_meth_info, 'method_info', None) # Skip methods that are not decorated with @method if method_info is None: continue path = method_info.get_path(service.api_info) method_id = method_info.method_id(service.api_info) canonical_method_id = self._get_canonical_method_id(method_id) resource_path = self._get_resource_path(method_id) # Make sure the same method name isn't repeated. if method_id in method_collision_tracker: raise api_exceptions.ApiConfigurationError( 'Method %s used multiple times, in classes %s and %s' % (method_id, method_collision_tracker[method_id], service.__name__)) else: method_collision_tracker[method_id] = service.__name__ # Make sure the same HTTP method & path aren't repeated. rest_identifier = (method_info.http_method, path) if rest_identifier in rest_collision_tracker: raise api_exceptions.ApiConfigurationError( '%s path "%s" used multiple times, in classes %s and %s' % (method_info.http_method, path, rest_collision_tracker[rest_identifier], service.__name__)) else: rest_collision_tracker[rest_identifier] = service.__name__ # If this method is part of a resource, note it and skip it for now if resource_path: resource_index[resource_path[0]].append( (service, protorpc_meth_info)) else: method_map[canonical_method_id] = self.__method_descriptor( service, method_info, protorpc_meth_info) # Do another pass for methods attached to resources for resource, resource_methods in resource_index.items(): resource_map[resource] = self.__resource_descriptor( resource, resource_methods) if method_map: descriptor['methods'] = method_map if resource_map: descriptor['resources'] = resource_map # Add schemas, if any schemas = self.__schemas_descriptor() if schemas: descriptor['schemas'] = schemas return descriptor
def __api_swagger_descriptor(self, services, hostname=None): """Builds a Swagger description of an API. Args: services: List of protorpc.remote.Service instances implementing an api/version. hostname: string, Hostname of the API, to override the value set on the current service. Defaults to None. Returns: A dictionary that can be deserialized into JSON and stored as an API description document in Swagger format. Raises: ApiConfigurationError: If there's something wrong with the API configuration, such as a multiclass API decorated with different API descriptors (see the docstring for api()), or a repeated method signature. """ merged_api_info = self.__get_merged_api_info(services) descriptor = self.get_descriptor_defaults(merged_api_info, hostname=hostname) description = merged_api_info.description if not description and len(services) == 1: description = services[0].__doc__ if description: descriptor['info']['description'] = description security_definitions = self.__security_definitions_descriptor( merged_api_info.issuers) method_map = {} method_collision_tracker = {} rest_collision_tracker = {} for service in services: remote_methods = service.all_remote_methods() for protorpc_meth_name, protorpc_meth_info in remote_methods.iteritems( ): method_info = getattr(protorpc_meth_info, 'method_info', None) # Skip methods that are not decorated with @method if method_info is None: continue method_id = method_info.method_id(service.api_info) path = '/{0}/{1}/{2}'.format( merged_api_info.name, merged_api_info.version, method_info.get_path(service.api_info)) verb = method_info.http_method.lower() if path not in method_map: method_map[path] = {} # Derive an OperationId from the method name data operation_id = self._construct_operation_id( service.__name__, protorpc_meth_name) method_map[path][verb] = self.__method_descriptor( service, method_info, operation_id, protorpc_meth_info, security_definitions) # Make sure the same method name isn't repeated. if method_id in method_collision_tracker: raise api_exceptions.ApiConfigurationError( 'Method %s used multiple times, in classes %s and %s' % (method_id, method_collision_tracker[method_id], service.__name__)) else: method_collision_tracker[method_id] = service.__name__ # Make sure the same HTTP method & path aren't repeated. rest_identifier = (method_info.http_method, method_info.get_path(service.api_info)) if rest_identifier in rest_collision_tracker: raise api_exceptions.ApiConfigurationError( '%s path "%s" used multiple times, in classes %s and %s' % (method_info.http_method, method_info.get_path(service.api_info), rest_collision_tracker[rest_identifier], service.__name__)) else: rest_collision_tracker[rest_identifier] = service.__name__ if method_map: descriptor['paths'] = method_map # Add request and/or response definitions, if any definitions = self.__definitions_descriptor() if definitions: descriptor['definitions'] = definitions descriptor['securityDefinitions'] = security_definitions return descriptor