def testSnakeToCamelWorksOnStringsWithMultipleUnderscoresBetweenWords(self): self.assertEqual("aBCD", casing.SnakeToCamel("a__b__c__d")) self.assertEqual("aBCD", casing.SnakeToCamel("a____b____c____d")) self.assertEqual("aBCD", casing.SnakeToCamel("___a___b___c___d___")) self.assertEqual("aaBbCcDd", casing.SnakeToCamel("___aa___bb___cc___dd___")) self.assertEqual("aaaBbbCccDdd", casing.SnakeToCamel("___aaa___bbb___ccc___ddd___"))
def testSnakeToCamelWorksOnStringsWithPrefixOrSuffixUnderscores(self): self.assertEqual("a", casing.SnakeToCamel("_a")) self.assertEqual("a", casing.SnakeToCamel("a_")) self.assertEqual("a", casing.SnakeToCamel("_a_")) self.assertEqual("a", casing.SnakeToCamel("___a___")) self.assertEqual("abcd", casing.SnakeToCamel("___abcd___")) self.assertEqual("aBCD", casing.SnakeToCamel("_a_b_c_d_")) self.assertEqual("aBCD", casing.SnakeToCamel("____a_b_c_d____")) self.assertEqual("aaBbCcDd", casing.SnakeToCamel("____aa_bb_cc_dd____")) self.assertEqual("aaaBbbCccDdd", casing.SnakeToCamel("____aaa_bbb_ccc_ddd____"))
def _SeparateFieldsIntoParams( self, http_method: str, path: str, args_type: Type[rdf_structs.RDFProtoStruct], ) -> Tuple[List[FieldDescriptor], List[FieldDescriptor], List[FieldDescriptor]]: """Group `FieldDescriptors` of a protobuf message by http param types.""" field_descriptors = [] if args_type: if not (inspect.isclass(args_type) and issubclass(args_type, rdf_structs.RDFProtoStruct)): raise TypeError("Router method args type is not a RDFProtoStruct " "subclass.") field_descriptors = args_type.protobuf.DESCRIPTOR.fields # Separate fields into params: path, query and part of the request body. path_param_names = set(_GetPathParamsFromPath(path)) path_params = [] query_params = [] body_params = [] for field_d in field_descriptors: if casing.SnakeToCamel(field_d.name) in path_param_names: path_params.append(field_d) elif http_method.upper() in ("GET", "HEAD"): query_params.append(field_d) else: body_params.append(field_d) return path_params, query_params, body_params
def _GetParameters( self, required_path_params: Iterable[FieldDescriptor], optional_path_params: Iterable[FieldDescriptor], query_params: Iterable[FieldDescriptor], ) -> List[Dict[str, Union[str, bool, SchemaReference, ArraySchema, DescribedSchema]]]: """Create the OpenAPI description of the parameters of a route.""" parameters = [] req_path_params_set = set(required_path_params) opt_path_params_set = set(optional_path_params) query_params_set = set(query_params) for field_d in req_path_params_set | opt_path_params_set | query_params_set: parameter_obj = {"name": casing.SnakeToCamel(field_d.name)} if field_d in req_path_params_set: parameter_obj["in"] = "path" parameter_obj["required"] = True elif field_d in opt_path_params_set: parameter_obj["in"] = "path" else: parameter_obj["in"] = "query" parameter_obj["schema"] = self._GetDescribedSchema(field_d) parameters.append(parameter_obj) return parameters
def _NormalizePathComponent(component: str) -> str: """Normalize the given path component to be used in a valid OpenAPI path.""" if _IsPathParameter(component): component = component[1:-1] component = component.split(":")[-1] component = casing.SnakeToCamel(component) component = f"{{{component}}}" return component
def _CreateMessageSchema( self, descriptor: Descriptor, visiting: Set[str], ) -> None: """Creates the OpenAPI schema of a protobuf message. This method should generally not be called directly, but rather through `_CreateSchema` which takes care of error-verifications and caching. Args: descriptor: The protobuf `Descriptor` associated with a protobuf message. visiting: A set of type names that are in the process of having their OpenAPI schemas constructed and have their associated `_Create*Schema` call in the current call stack. Returns: Nothing, the schema is stored in `self.schema_objs`. """ if self.schema_objs is None: # Check required by mypy. raise AssertionError("OpenAPI type schemas not initialized.") type_name = _GetTypeName(descriptor) properties = dict() visiting.add(type_name) # Create schemas for the fields' types. for field_descriptor in descriptor.fields: self._CreateSchema(field_descriptor, visiting) field_name = casing.SnakeToCamel(field_descriptor.name) properties[field_name] = self._GetDescribedSchema(field_descriptor) visiting.remove(type_name) self.schema_objs[type_name] = cast(MessageSchema, { "type": "object", "properties": properties, })
def _GetRequestBody( self, body_params: Iterable[FieldDescriptor], ) -> Dict[str, Dict[str, Any]]: """Create the OpenAPI description of the request body of a route.""" if not body_params: return {} properties: (Dict[str, Union[SchemaReference, ArraySchema, DescribedSchema]]) = dict() for field_d in body_params: field_name = casing.SnakeToCamel(field_d.name) properties[field_name] = self._GetDescribedSchema(field_d) return { "content": { "application/json": { "schema": { "type": "object", "properties": properties, }, }, }, }
def testSnakeToCamelWorksOnEmptyString(self): self.assertEqual("", casing.SnakeToCamel(""))
def testSnakeToCamelWorksOnStringsWithUnicodeCharacters(self): self.assertEqual("ąĆĘ", casing.SnakeToCamel("ą_ć_ę")) self.assertEqual("ąąąaĆććaĘęęa", casing.SnakeToCamel("ąąąa_ććća_ęęęa")) self.assertEqual("ąąąaĆććaĘęęa", casing.SnakeToCamel("ĄąĄA_ĆćĆa_ĘĘĘa"))
def testSnakeToCamelWorksOnStringsWithUppercaseLetters(self): self.assertEqual("a", casing.SnakeToCamel("A")) self.assertEqual("aaaaaa", casing.SnakeToCamel("Aaaaaa")) self.assertEqual("aaaaBbbbCccc", casing.SnakeToCamel("AaAa_bBbB_CcCc"))
def testSnakeToCamelWorksOnStringsWithUnderscoresOnly(self): self.assertEqual("", casing.SnakeToCamel("_")) self.assertEqual("", casing.SnakeToCamel("__")) self.assertEqual("", casing.SnakeToCamel("_________"))
def testSnakeToCamelWorksOnRegularStrings(self): self.assertEqual("thisIsACamel", casing.SnakeToCamel("this_is_a_camel")) self.assertEqual("aCamelThisIs", casing.SnakeToCamel("a_camel_this_is")) self.assertEqual("aBCD", casing.SnakeToCamel("a_b_c_d"))
def testSnakeToCamelWorksOnOneWordStrings(self): self.assertEqual("abcd", casing.SnakeToCamel("abcd")) self.assertEqual("a", casing.SnakeToCamel("a"))
def _GetPaths(self) -> Dict[str, Dict[Any, Any]]: """Create the OpenAPI description of all the routes exposed by the API.""" # The `Paths Object` `paths` field of the root `OpenAPI Object`. paths_obj: DefaultDict[str, Dict[Any, Any]] = collections.defaultdict(dict) router_methods = self.router.__class__.GetAnnotatedMethods() for router_method in router_methods.values(): # To extract optional path parameters, all the routes associated with this # router method must be analysed and grouped. ungrouped_routes = [] for http_method, path, _ in router_method.http_methods: path_components = path.split("/") # Remove any empty strings from the list of path components. path_components = [comp for comp in path_components if comp] ungrouped_routes.append([http_method] + path_components) grouped_routes = _GetGroupedRoutes(ungrouped_routes) for route_info in grouped_routes: # Components (comps) are URL components, including Werkzeug path # arguments such as `<client_id>` or `<path:file_path>`. route_comps, req_path_param_comps, opt_path_param_comps = route_info http_method = route_comps[0] path = "/".join(route_comps[1:]) # Separate the route parameters into path params, query params and # request body params. path_params, query_params, body_params = self._SeparateFieldsIntoParams( http_method, path, router_method.args_type) # Separate the path params into required and optional path params. # First, extract path param names by normalizing the Werkzeug path arg # components to OpenAPI path args and remove the surrounding brackets. req_path_param_names = [ _NormalizePathComponent(comp)[1:-1] for comp in req_path_param_comps ] opt_path_param_names = [ _NormalizePathComponent(comp)[1:-1] for comp in opt_path_param_comps ] req_path_params = [] opt_path_params = [] for path_param in path_params: path_param_name = casing.SnakeToCamel(path_param.name) if path_param_name in req_path_param_names: req_path_params.append(path_param) elif path_param_name in opt_path_param_names: opt_path_params.append(path_param) else: raise AssertionError( f"Path parameter {path_param_name} was not classified as " f"required/optional.") normalized_path = _NormalizePath(path) path_obj = paths_obj[normalized_path] path_obj[http_method.lower()] = ( self._GetOperationDescription(router_method, req_path_params, opt_path_params, query_params, body_params)) return paths_obj