| # Copyright 2014 Google Inc. All Rights Reserved. | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| """Schema processing for discovery based APIs | |
| Schemas holds an APIs discovery schemas. It can return those schema as | |
| deserialized JSON objects, or pretty print them as prototype objects that | |
| conform to the schema. | |
| For example, given the schema: | |
| schema = \"\"\"{ | |
| "Foo": { | |
| "type": "object", | |
| "properties": { | |
| "etag": { | |
| "type": "string", | |
| "description": "ETag of the collection." | |
| }, | |
| "kind": { | |
| "type": "string", | |
| "description": "Type of the collection ('calendar#acl').", | |
| "default": "calendar#acl" | |
| }, | |
| "nextPageToken": { | |
| "type": "string", | |
| "description": "Token used to access the next | |
| page of this result. Omitted if no further results are available." | |
| } | |
| } | |
| } | |
| }\"\"\" | |
| s = Schemas(schema) | |
| print s.prettyPrintByName('Foo') | |
| Produces the following output: | |
| { | |
| "nextPageToken": "A String", # Token used to access the | |
| # next page of this result. Omitted if no further results are available. | |
| "kind": "A String", # Type of the collection ('calendar#acl'). | |
| "etag": "A String", # ETag of the collection. | |
| }, | |
| The constructor takes a discovery document in which to look up named schema. | |
| """ | |
| from __future__ import absolute_import | |
| import six | |
| # TODO(jcgregorio) support format, enum, minimum, maximum | |
| __author__ = 'jcgregorio@google.com (Joe Gregorio)' | |
| import copy | |
| from googleapiclient import _helpers as util | |
| class Schemas(object): | |
| """Schemas for an API.""" | |
| def __init__(self, discovery): | |
| """Constructor. | |
| Args: | |
| discovery: object, Deserialized discovery document from which we pull | |
| out the named schema. | |
| """ | |
| self.schemas = discovery.get('schemas', {}) | |
| # Cache of pretty printed schemas. | |
| self.pretty = {} | |
| @util.positional(2) | |
| def _prettyPrintByName(self, name, seen=None, dent=0): | |
| """Get pretty printed object prototype from the schema name. | |
| Args: | |
| name: string, Name of schema in the discovery document. | |
| seen: list of string, Names of schema already seen. Used to handle | |
| recursive definitions. | |
| Returns: | |
| string, A string that contains a prototype object with | |
| comments that conforms to the given schema. | |
| """ | |
| if seen is None: | |
| seen = [] | |
| if name in seen: | |
| # Do not fall into an infinite loop over recursive definitions. | |
| return '# Object with schema name: %s' % name | |
| seen.append(name) | |
| if name not in self.pretty: | |
| self.pretty[name] = _SchemaToStruct(self.schemas[name], | |
| seen, dent=dent).to_str(self._prettyPrintByName) | |
| seen.pop() | |
| return self.pretty[name] | |
| def prettyPrintByName(self, name): | |
| """Get pretty printed object prototype from the schema name. | |
| Args: | |
| name: string, Name of schema in the discovery document. | |
| Returns: | |
| string, A string that contains a prototype object with | |
| comments that conforms to the given schema. | |
| """ | |
| # Return with trailing comma and newline removed. | |
| return self._prettyPrintByName(name, seen=[], dent=1)[:-2] | |
| @util.positional(2) | |
| def _prettyPrintSchema(self, schema, seen=None, dent=0): | |
| """Get pretty printed object prototype of schema. | |
| Args: | |
| schema: object, Parsed JSON schema. | |
| seen: list of string, Names of schema already seen. Used to handle | |
| recursive definitions. | |
| Returns: | |
| string, A string that contains a prototype object with | |
| comments that conforms to the given schema. | |
| """ | |
| if seen is None: | |
| seen = [] | |
| return _SchemaToStruct(schema, seen, dent=dent).to_str(self._prettyPrintByName) | |
| def prettyPrintSchema(self, schema): | |
| """Get pretty printed object prototype of schema. | |
| Args: | |
| schema: object, Parsed JSON schema. | |
| Returns: | |
| string, A string that contains a prototype object with | |
| comments that conforms to the given schema. | |
| """ | |
| # Return with trailing comma and newline removed. | |
| return self._prettyPrintSchema(schema, dent=1)[:-2] | |
| def get(self, name, default=None): | |
| """Get deserialized JSON schema from the schema name. | |
| Args: | |
| name: string, Schema name. | |
| default: object, return value if name not found. | |
| """ | |
| return self.schemas.get(name, default) | |
| class _SchemaToStruct(object): | |
| """Convert schema to a prototype object.""" | |
| @util.positional(3) | |
| def __init__(self, schema, seen, dent=0): | |
| """Constructor. | |
| Args: | |
| schema: object, Parsed JSON schema. | |
| seen: list, List of names of schema already seen while parsing. Used to | |
| handle recursive definitions. | |
| dent: int, Initial indentation depth. | |
| """ | |
| # The result of this parsing kept as list of strings. | |
| self.value = [] | |
| # The final value of the parsing. | |
| self.string = None | |
| # The parsed JSON schema. | |
| self.schema = schema | |
| # Indentation level. | |
| self.dent = dent | |
| # Method that when called returns a prototype object for the schema with | |
| # the given name. | |
| self.from_cache = None | |
| # List of names of schema already seen while parsing. | |
| self.seen = seen | |
| def emit(self, text): | |
| """Add text as a line to the output. | |
| Args: | |
| text: string, Text to output. | |
| """ | |
| self.value.extend([" " * self.dent, text, '\n']) | |
| def emitBegin(self, text): | |
| """Add text to the output, but with no line terminator. | |
| Args: | |
| text: string, Text to output. | |
| """ | |
| self.value.extend([" " * self.dent, text]) | |
| def emitEnd(self, text, comment): | |
| """Add text and comment to the output with line terminator. | |
| Args: | |
| text: string, Text to output. | |
| comment: string, Python comment. | |
| """ | |
| if comment: | |
| divider = '\n' + ' ' * (self.dent + 2) + '# ' | |
| lines = comment.splitlines() | |
| lines = [x.rstrip() for x in lines] | |
| comment = divider.join(lines) | |
| self.value.extend([text, ' # ', comment, '\n']) | |
| else: | |
| self.value.extend([text, '\n']) | |
| def indent(self): | |
| """Increase indentation level.""" | |
| self.dent += 1 | |
| def undent(self): | |
| """Decrease indentation level.""" | |
| self.dent -= 1 | |
| def _to_str_impl(self, schema): | |
| """Prototype object based on the schema, in Python code with comments. | |
| Args: | |
| schema: object, Parsed JSON schema file. | |
| Returns: | |
| Prototype object based on the schema, in Python code with comments. | |
| """ | |
| stype = schema.get('type') | |
| if stype == 'object': | |
| self.emitEnd('{', schema.get('description', '')) | |
| self.indent() | |
| if 'properties' in schema: | |
| for pname, pschema in six.iteritems(schema.get('properties', {})): | |
| self.emitBegin('"%s": ' % pname) | |
| self._to_str_impl(pschema) | |
| elif 'additionalProperties' in schema: | |
| self.emitBegin('"a_key": ') | |
| self._to_str_impl(schema['additionalProperties']) | |
| self.undent() | |
| self.emit('},') | |
| elif '$ref' in schema: | |
| schemaName = schema['$ref'] | |
| description = schema.get('description', '') | |
| s = self.from_cache(schemaName, seen=self.seen) | |
| parts = s.splitlines() | |
| self.emitEnd(parts[0], description) | |
| for line in parts[1:]: | |
| self.emit(line.rstrip()) | |
| elif stype == 'boolean': | |
| value = schema.get('default', 'True or False') | |
| self.emitEnd('%s,' % str(value), schema.get('description', '')) | |
| elif stype == 'string': | |
| value = schema.get('default', 'A String') | |
| self.emitEnd('"%s",' % str(value), schema.get('description', '')) | |
| elif stype == 'integer': | |
| value = schema.get('default', '42') | |
| self.emitEnd('%s,' % str(value), schema.get('description', '')) | |
| elif stype == 'number': | |
| value = schema.get('default', '3.14') | |
| self.emitEnd('%s,' % str(value), schema.get('description', '')) | |
| elif stype == 'null': | |
| self.emitEnd('None,', schema.get('description', '')) | |
| elif stype == 'any': | |
| self.emitEnd('"",', schema.get('description', '')) | |
| elif stype == 'array': | |
| self.emitEnd('[', schema.get('description')) | |
| self.indent() | |
| self.emitBegin('') | |
| self._to_str_impl(schema['items']) | |
| self.undent() | |
| self.emit('],') | |
| else: | |
| self.emit('Unknown type! %s' % stype) | |
| self.emitEnd('', '') | |
| self.string = ''.join(self.value) | |
| return self.string | |
| def to_str(self, from_cache): | |
| """Prototype object based on the schema, in Python code with comments. | |
| Args: | |
| from_cache: callable(name, seen), Callable that retrieves an object | |
| prototype for a schema with the given name. Seen is a list of schema | |
| names already seen as we recursively descend the schema definition. | |
| Returns: | |
| Prototype object based on the schema, in Python code with comments. | |
| The lines of the code will all be properly indented. | |
| """ | |
| self.from_cache = from_cache | |
| return self._to_str_impl(self.schema) |