Package apiclient :: Module schema |
[hide private]
[frames] | no frames]

# Source Code for Module apiclient.schema

      1  # Copyright (C) 2010 Google Inc.
      2  #
      3  # Licensed under the Apache License, Version 2.0 (the "License");
      4  # you may not use this file except in compliance with the License.
      5  # You may obtain a copy of the License at
      6  #
      7  #      http://www.apache.org/licenses/LICENSE-2.0
      8  #
      9  # Unless required by applicable law or agreed to in writing, software
     10  # distributed under the License is distributed on an "AS IS" BASIS,
     11  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12  # See the License for the specific language governing permissions and
     13  # limitations under the License.
     14
     15  """Schema processing for discovery based APIs
     16
     17  Schemas holds an APIs discovery schemas. It can return those schema as
     18  deserialized JSON objects, or pretty print them as prototype objects that
     19  conform to the schema.
     20
     21  For example, given the schema:
     22
     23   schema = \"\"\"{
     24     "Foo": {
     25      "type": "object",
     26      "properties": {
     27       "etag": {
     28        "type": "string",
     29        "description": "ETag of the collection."
     30       },
     31       "kind": {
     32        "type": "string",
     33        "description": "Type of the collection ('calendar#acl').",
     34        "default": "calendar#acl"
     35       },
     36       "nextPageToken": {
     37        "type": "string",
     38        "description": "Token used to access the next
     39           page of this result. Omitted if no further results are available."
     40       }
     41      }
     42     }
     43   }\"\"\"
     44
     45   s = Schemas(schema)
     46   print s.prettyPrintByName('Foo')
     47
     48   Produces the following output:
     49
     50    {
     51     "nextPageToken": "A String", # Token used to access the
     52         # next page of this result. Omitted if no further results are available.
     53     "kind": "A String", # Type of the collection ('calendar#acl').
     54     "etag": "A String", # ETag of the collection.
     55    },
     56
     57  The constructor takes a discovery document in which to look up named schema.
     58  """
     59
     60  # TODO(jcgregorio) support format, enum, minimum, maximum
     61
     62  __author__ = 'jcgregorio@google.com (Joe Gregorio)'
     63
     64  import copy
     65
     66  from oauth2client import util
     67  from oauth2client.anyjson import simplejson
     68
     69
     70 -class Schemas(object):
     71    """Schemas for an API."""
     72
     73 -  def __init__(self, discovery):
     74      """Constructor.
     75
     76      Args:
     77        discovery: object, Deserialized discovery document from which we pull
     78          out the named schema.
     79      """
     80      self.schemas = discovery.get('schemas', {})
     81
     82      # Cache of pretty printed schemas.
     83      self.pretty = {}
     84
     85    @util.positional(2)
     86 -  def _prettyPrintByName(self, name, seen=None, dent=0):
     87      """Get pretty printed object prototype from the schema name.
     88
     89      Args:
     90        name: string, Name of schema in the discovery document.
     91        seen: list of string, Names of schema already seen. Used to handle
     92          recursive definitions.
     93
     94      Returns:
     95        string, A string that contains a prototype object with
     96          comments that conforms to the given schema.
     97      """
     98      if seen is None:
     99        seen = []
    100
    101      if name in seen:
    102        # Do not fall into an infinite loop over recursive definitions.
    103        return '# Object with schema name: %s' % name
    104      seen.append(name)
    105
    106      if name not in self.pretty:
    107        self.pretty[name] = _SchemaToStruct(self.schemas[name],
    108            seen, dent=dent).to_str(self._prettyPrintByName)
    109
    110      seen.pop()
    111
    112      return self.pretty[name]
    113
    114 -  def prettyPrintByName(self, name):
    115      """Get pretty printed object prototype from the schema name.
    116
    117      Args:
    118        name: string, Name of schema in the discovery document.
    119
    120      Returns:
    121        string, A string that contains a prototype object with
    122          comments that conforms to the given schema.
    123      """
    124      # Return with trailing comma and newline removed.
    125      return self._prettyPrintByName(name, seen=[], dent=1)[:-2]
    126
    127    @util.positional(2)
    128 -  def _prettyPrintSchema(self, schema, seen=None, dent=0):
    129      """Get pretty printed object prototype of schema.
    130
    131      Args:
    132        schema: object, Parsed JSON schema.
    133        seen: list of string, Names of schema already seen. Used to handle
    134          recursive definitions.
    135
    136      Returns:
    137        string, A string that contains a prototype object with
    138          comments that conforms to the given schema.
    139      """
    140      if seen is None:
    141        seen = []
    142
    143      return _SchemaToStruct(schema, seen, dent=dent).to_str(self._prettyPrintByName)
    144
    145 -  def prettyPrintSchema(self, schema):
    146      """Get pretty printed object prototype of schema.
    147
    148      Args:
    149        schema: object, Parsed JSON schema.
    150
    151      Returns:
    152        string, A string that contains a prototype object with
    153          comments that conforms to the given schema.
    154      """
    155      # Return with trailing comma and newline removed.
    156      return self._prettyPrintSchema(schema, dent=1)[:-2]
    157
    158 -  def get(self, name):
    159      """Get deserialized JSON schema from the schema name.
    160
    161      Args:
    162        name: string, Schema name.
    163      """
    164      return self.schemas[name]
    165
    166
    167 -class _SchemaToStruct(object):
    168    """Convert schema to a prototype object."""
    169
    170    @util.positional(3)
    171 -  def __init__(self, schema, seen, dent=0):
    172      """Constructor.
    173
    174      Args:
    175        schema: object, Parsed JSON schema.
    176        seen: list, List of names of schema already seen while parsing. Used to
    177          handle recursive definitions.
    178        dent: int, Initial indentation depth.
    179      """
    180      # The result of this parsing kept as list of strings.
    181      self.value = []
    182
    183      # The final value of the parsing.
    184      self.string = None
    185
    186      # The parsed JSON schema.
    187      self.schema = schema
    188
    189      # Indentation level.
    190      self.dent = dent
    191
    192      # Method that when called returns a prototype object for the schema with
    193      # the given name.
    194      self.from_cache = None
    195
    196      # List of names of schema already seen while parsing.
    197      self.seen = seen
    198
    199 -  def emit(self, text):
    200      """Add text as a line to the output.
    201
    202      Args:
    203        text: string, Text to output.
    204      """
    205      self.value.extend(["  " * self.dent, text, '\n'])
    206
    207 -  def emitBegin(self, text):
    208      """Add text to the output, but with no line terminator.
    209
    210      Args:
    211        text: string, Text to output.
    212        """
    213      self.value.extend(["  " * self.dent, text])
    214
    215 -  def emitEnd(self, text, comment):
    216      """Add text and comment to the output with line terminator.
    217
    218      Args:
    219        text: string, Text to output.
    220        comment: string, Python comment.
    221      """
    222      if comment:
    223        divider = '\n' + '  ' * (self.dent + 2) + '# '
    224        lines = comment.splitlines()
    225        lines = [x.rstrip() for x in lines]
    226        comment = divider.join(lines)
    227        self.value.extend([text, ' # ', comment, '\n'])
    228      else:
    229        self.value.extend([text, '\n'])
    230
    231 -  def indent(self):
    232      """Increase indentation level."""
    233      self.dent += 1
    234
    235 -  def undent(self):
    236      """Decrease indentation level."""
    237      self.dent -= 1
    238
    239 -  def _to_str_impl(self, schema):
    240      """Prototype object based on the schema, in Python code with comments.
    241
    242      Args:
    243        schema: object, Parsed JSON schema file.
    244
    245      Returns:
    246        Prototype object based on the schema, in Python code with comments.
    247      """
    248      stype = schema.get('type')
    249      if stype == 'object':
    250        self.emitEnd('{', schema.get('description', ''))
    251        self.indent()
    252        if 'properties' in schema:
    253          for pname, pschema in schema.get('properties', {}).iteritems():
    254            self.emitBegin('"%s": ' % pname)
    255            self._to_str_impl(pschema)
    256        elif 'additionalProperties' in schema:
    257          self.emitBegin('"a_key": ')
    258          self._to_str_impl(schema['additionalProperties'])
    259        self.undent()
    260        self.emit('},')
    261      elif '$ref' in schema:
    262        schemaName = schema['$ref']
    263        description = schema.get('description', '')
    264        s = self.from_cache(schemaName, seen=self.seen)
    265        parts = s.splitlines()
    266        self.emitEnd(parts[0], description)
    267        for line in parts[1:]:
    268          self.emit(line.rstrip())
    269      elif stype == 'boolean':
    270        value = schema.get('default', 'True or False')
    271        self.emitEnd('%s,' % str(value), schema.get('description', ''))
    272      elif stype == 'string':
    273        value = schema.get('default', 'A String')
    274        self.emitEnd('"%s",' % str(value), schema.get('description', ''))
    275      elif stype == 'integer':
    276        value = schema.get('default', '42')
    277        self.emitEnd('%s,' % str(value), schema.get('description', ''))
    278      elif stype == 'number':
    279        value = schema.get('default', '3.14')
    280        self.emitEnd('%s,' % str(value), schema.get('description', ''))
    281      elif stype == 'null':
    282        self.emitEnd('None,', schema.get('description', ''))
    283      elif stype == 'any':
    284        self.emitEnd('"",', schema.get('description', ''))
    285      elif stype == 'array':
    286        self.emitEnd('[', schema.get('description'))
    287        self.indent()
    288        self.emitBegin('')
    289        self._to_str_impl(schema['items'])
    290        self.undent()
    291        self.emit('],')
    292      else:
    293        self.emit('Unknown type! %s' % stype)
    294        self.emitEnd('', '')
    295
    296      self.string = ''.join(self.value)
    297      return self.string
    298
    299 -  def to_str(self, from_cache):
    300      """Prototype object based on the schema, in Python code with comments.
    301
    302      Args:
    303        from_cache: callable(name, seen), Callable that retrieves an object
    304           prototype for a schema with the given name. Seen is a list of schema
    305           names already seen as we recursively descend the schema definition.
    306
    307      Returns:
    308        Prototype object based on the schema, in Python code with comments.
    309        The lines of the code will all be properly indented.
    310      """
    311      self.from_cache = from_cache
    312      return self._to_str_impl(self.schema)
    313