def _CreateObjectWithProperties(cls, props, api, name, def_dict, wire_name, parent): properties = [] schema = cls(api, name, def_dict, parent=parent) if wire_name: schema.SetTemplateValue('wireName', wire_name) for prop_name in sorted(props): prop_dict = props[prop_name] _LOGGER.debug(' adding prop: %s to %s', prop_name, name) properties.append(Property(api, schema, prop_name, prop_dict)) # Some APIs express etag directly in the response, others don't. # Knowing that we have it explicitly makes special case code generation # easier if prop_name == 'etag': schema.SetTemplateValue('hasEtagProperty', True) schema.SetTemplateValue('properties', properties) # check for @ clashing. E.g. No 'foo' and '@foo' in the same object. names = set() for p in properties: wire_name = p.GetTemplateValue('wireName') no_at_sign = wire_name.replace('@', '') if no_at_sign in names: raise ApiException('Property name clash in schema %s:' ' %s conflicts with another property' % (name, wire_name)) names.add(no_at_sign) return schema
def _CreateArrayType(cls, api, def_dict, wire_name, class_name, schema_id, parent): items = def_dict.get('items') if not items: raise ApiException('array without items in: %s' % def_dict) tentative_class_name = class_name # TODO(user): THIS IS STUPID. We should not rename things items. # if we have an anonymous type within a map or array, it should be # called 'Item', and let the namespacing sort it out. if schema_id: _LOGGER.debug('Top level schema %s is an array', class_name) tentative_class_name += 'Items' base_type = api.DataTypeFromJson(items, tentative_class_name, parent=parent, wire_name=wire_name) _LOGGER.debug(' %s is ArrayOf<%s>', class_name, base_type.class_name) array_type = data_types.ArrayDataType(tentative_class_name, base_type, wire_name=wire_name, parent=parent) if schema_id: array_type.SetTemplateValue('className', schema_id) return array_type
def __init__(self, api, name, def_dict, parent=None): """Construct a method. Methods in REST discovery are inside of a resource. Note that the method name and id are calculable from each other. id will always be equal to api_name.resource_name[.sub_resource...].method_name. At least it should be, as that is the transformation Discovery makes from the API definition, which is essentially a flat list of methods, into a hierarchy of resources. Args: api: (Api) The Api which owns this Method. name: (string) The discovery name of the Method. def_dict: (dict) The discovery dictionary for this Method. parent: (CodeObject) The resource containing this Method, if any. Raises: ApiException: If the httpMethod type is not one we know how to handle. """ super(Method, self).__init__(def_dict, api, parent=(parent or api)) # TODO(user): Fix java templates to name vs. wireName correctly. Then # change the __init__ to have wire_name=def_dict.get('id') or name # then eliminate this line. self.SetTemplateValue('wireName', name) self.ValidateName(name) class_name = api.ToClassName(name, self, element_type='method') if parent and class_name == parent.values['className']: # Some languages complain when the collection name is the same as the # method name. class_name = '%sRequest' % class_name # The name is the key of the dict defining use. The id field is what you # have to use to call the method via RPC. That is unique, name might not be. self.SetTemplateValue('name', name) # Fix up very old discovery, which does not have an id. if 'id' not in self.values: self.values['id'] = name self.SetTemplateValue('className', class_name) http_method = def_dict.get('httpMethod', 'POST').upper() self.SetTemplateValue('httpMethod', http_method) self.SetTemplateValue('rpcMethod', def_dict.get('rpcMethod') or def_dict['id']) rest_path = def_dict.get('path') or def_dict.get('restPath') # TODO(user): if rest_path is not set, raise a good error and fail fast. self.SetTemplateValue('restPath', rest_path) # Figure out the input and output types and schemas for this method. expected_request = self.values.get('request') if expected_request: # TODO(user): RequestBody is only used if the schema is anonymous. # When we go to nested models, this could be a nested class off the # Method, making it unique without the silly name. Same for ResponseBody. request_schema = api.DataTypeFromJson(expected_request, '%sRequestContent' % name, parent=self) self.SetTemplateValue('requestType', request_schema) expected_response = def_dict.get('response') or def_dict.get('returns') if expected_response: response_schema = api.DataTypeFromJson(expected_response, '%sResponse' % name, parent=self) if self.values['wireName'] == 'get': response_schema.values['associatedResource'] = parent self.SetTemplateValue('responseType', response_schema) else: self.SetTemplateValue('responseType', api.void_type) # Make sure we can handle this method type and do any fixups. if http_method not in [ 'DELETE', 'GET', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'PROPFIND', 'PROPPATCH', 'REPORT' ]: raise ApiException('Unknown HTTP method: %s' % http_method, def_dict) if http_method == 'GET': self.SetTemplateValue('requestType', None) # Replace parameters dict with Parameters. We try to order them by their # position in the request path so that the generated code can track the # more human readable definition, rather than the order of the parameters # in the discovery doc. order = self.values.get('parameterOrder', []) req_parameters = [] opt_parameters = [] for name, def_dict in self.values.get('parameters', {}).iteritems(): param = Parameter(api, name, def_dict, self) if name == 'alt': # Treat the alt parameter differently self.SetTemplateValue('alt', param) continue # Standard params are part of the generic request class # We want to push all parameters that aren't declared inside # parameterOrder after those that are. if param.values['wireName'] in order: req_parameters.append(param) else: # optional parameters are appended in the order they're declared. opt_parameters.append(param) # pylint: disable=g-long-lambda req_parameters.sort(lambda x, y: cmp(order.index(x.values[ 'wireName']), order.index(y.values['wireName']))) req_parameters.extend(opt_parameters) self.SetTemplateValue('parameters', req_parameters) self._InitMediaUpload(parent) self._InitPageable(api) api.AddMethod(self)
def Create(cls, api, default_name, def_dict, wire_name, parent=None): """Construct a Schema or DataType from a discovery dictionary. Schemas contain either object declarations, simple type declarations, or references to other Schemas. Object declarations conceptually map to real classes. Simple types will map to a target language built-in type. References should effectively be replaced by the referenced Schema. Args: api: (Api) the Api instance owning the Schema default_name: (str) the default name of the Schema. If there is an 'id' member in the definition, that is used for the name instead. def_dict: (dict) a discovery dictionary wire_name: The name which will identify objects of this type in data on the wire. The path of wire_names can trace an item back through discovery. parent: (Schema) The containing schema. To be used to establish nesting for anonymous sub-schemas. Returns: A Schema or DataType. Raises: ApiException: If the definition dict is not correct. """ schema_id = def_dict.get('id') if schema_id: name = schema_id else: name = default_name class_name = api.ToClassName(name, None, element_type='schema') _LOGGER.debug( 'Create: %s, parent=%s', name, parent.values.get('wireName', '<anon>') if parent else 'None') # Schema objects come in several patterns. # # 1. Simple objects # { type: object, properties: { "foo": {schema} ... }} # # 2. Maps of objects # { type: object, additionalProperties: { "foo": {inner_schema} ... }} # # What we want is a data type which is Map<string, {inner_schema}> # The schema we create here is essentially a built in type which we # don't want to generate a class for. # # 3. Arrays of objects # { type: array, items: { inner_schema }} # # Same kind of issue as the map, but with List<{inner_schema}> # # 4. Primitive data types, described by type and format. # { type: string, format: int32 } # { type: string, enum: ["value", ...], enumDescriptions: ["desc", ...]} # # 5. Refs to another schema. # { $ref: name } # # 6. Variant schemas # { type: object, variant: { discriminant: "prop", map: # [ { 'type_value': value, '$ref': wireName }, ... ] } } # # What we do is map the variant schema to a schema with a single # property for the discriminant. To that property, we attach # the variant map which specifies which discriminator values map # to which schema references. We also collect variant information # in the api so we can later associate discriminator value and # base type with the generated variant subtypes. if 'type' in def_dict: # The 'type' field of the schema can either be 'array', 'object', or a # base json type. json_type = def_dict['type'] if json_type == 'object': # Look for variants variant = def_dict.get('variant') if variant: return cls._CreateVariantType(variant, api, name, def_dict, wire_name, parent) # Look for full object definition. You can have properties or # additionalProperties, but it does not do anything useful to have # both. # Replace properties dict with Property's props = def_dict.get('properties') if props: # This case 1 from above return cls._CreateObjectWithProperties( props, api, name, def_dict, wire_name, parent) # Look for case 2 additional_props = def_dict.get(_ADDITIONAL_PROPERTIES) if additional_props: return cls._CreateMapType(additional_props, api, name, wire_name, class_name, parent) # no properties return cls._CreateSchemaWithoutProperties( api, name, def_dict, wire_name, parent) elif json_type == 'array': # Case 3: Look for array definition return cls._CreateArrayType(api, def_dict, wire_name, class_name, schema_id, parent) else: # Case 4: This must be a basic type. Create a DataType for it. return data_types.CreatePrimitiveDataType(def_dict, api, wire_name, parent=parent) referenced_schema = def_dict.get('$ref') if referenced_schema: # Case 5: Reference to another Schema. # # There are 4 ways you can see '$ref' in discovery. # 1. In a property of a schema or a method request/response, pointing # back to a previously defined schema # 2. As above, pointing to something not defined yet. # 3. In a method request or response or property of a schema pointing to # something undefined. # # For case 1, the schema will be in the API name to schema map. # # For case 2, just creating this placeholder here is fine. When the # actual schema is hit in the loop in _BuildSchemaDefinitions, we will # replace the entry and DataTypeFromJson will resolve the to the new def. # # For case 3, we will end up with a dangling reference and fail later. schema = api.SchemaByName(referenced_schema) # The stored "schema" may not be an instance of Schema, but rather a # data_types.PrimitiveDataType, which has no 'wireName' value. if schema: _LOGGER.debug('Schema.Create: %s => %s', default_name, schema.values.get('wireName', '<unknown>')) return schema return data_types.SchemaReference(referenced_schema, api) raise ApiException('Cannot decode JSON Schema for: %s' % def_dict)
def testExceptionStr(self): e = ApiException('foo') self.assertEquals('foo', str(e)) e = ApiException('foo', {'bar': 1}) self.assertEquals("""foo: {'bar': 1}""", str(e))