Home | History | Annotate | Download | only in googleapiclient
      1 # Copyright 2014 Google Inc. All Rights Reserved.
      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 from __future__ import absolute_import
     60 import six
     61 
     62 # TODO(jcgregorio) support format, enum, minimum, maximum
     63 
     64 __author__ = 'jcgregorio (at] google.com (Joe Gregorio)'
     65 
     66 import copy
     67 
     68 from oauth2client import util
     69 
     70 
     71 class Schemas(object):
     72   """Schemas for an API."""
     73 
     74   def __init__(self, discovery):
     75     """Constructor.
     76 
     77     Args:
     78       discovery: object, Deserialized discovery document from which we pull
     79         out the named schema.
     80     """
     81     self.schemas = discovery.get('schemas', {})
     82 
     83     # Cache of pretty printed schemas.
     84     self.pretty = {}
     85 
     86   @util.positional(2)
     87   def _prettyPrintByName(self, name, seen=None, dent=0):
     88     """Get pretty printed object prototype from the schema name.
     89 
     90     Args:
     91       name: string, Name of schema in the discovery document.
     92       seen: list of string, Names of schema already seen. Used to handle
     93         recursive definitions.
     94 
     95     Returns:
     96       string, A string that contains a prototype object with
     97         comments that conforms to the given schema.
     98     """
     99     if seen is None:
    100       seen = []
    101 
    102     if name in seen:
    103       # Do not fall into an infinite loop over recursive definitions.
    104       return '# Object with schema name: %s' % name
    105     seen.append(name)
    106 
    107     if name not in self.pretty:
    108       self.pretty[name] = _SchemaToStruct(self.schemas[name],
    109           seen, dent=dent).to_str(self._prettyPrintByName)
    110 
    111     seen.pop()
    112 
    113     return self.pretty[name]
    114 
    115   def prettyPrintByName(self, name):
    116     """Get pretty printed object prototype from the schema name.
    117 
    118     Args:
    119       name: string, Name of schema in the discovery document.
    120 
    121     Returns:
    122       string, A string that contains a prototype object with
    123         comments that conforms to the given schema.
    124     """
    125     # Return with trailing comma and newline removed.
    126     return self._prettyPrintByName(name, seen=[], dent=1)[:-2]
    127 
    128   @util.positional(2)
    129   def _prettyPrintSchema(self, schema, seen=None, dent=0):
    130     """Get pretty printed object prototype of schema.
    131 
    132     Args:
    133       schema: object, Parsed JSON schema.
    134       seen: list of string, Names of schema already seen. Used to handle
    135         recursive definitions.
    136 
    137     Returns:
    138       string, A string that contains a prototype object with
    139         comments that conforms to the given schema.
    140     """
    141     if seen is None:
    142       seen = []
    143 
    144     return _SchemaToStruct(schema, seen, dent=dent).to_str(self._prettyPrintByName)
    145 
    146   def prettyPrintSchema(self, schema):
    147     """Get pretty printed object prototype of schema.
    148 
    149     Args:
    150       schema: object, Parsed JSON schema.
    151 
    152     Returns:
    153       string, A string that contains a prototype object with
    154         comments that conforms to the given schema.
    155     """
    156     # Return with trailing comma and newline removed.
    157     return self._prettyPrintSchema(schema, dent=1)[:-2]
    158 
    159   def get(self, name):
    160     """Get deserialized JSON schema from the schema name.
    161 
    162     Args:
    163       name: string, Schema name.
    164     """
    165     return self.schemas[name]
    166 
    167 
    168 class _SchemaToStruct(object):
    169   """Convert schema to a prototype object."""
    170 
    171   @util.positional(3)
    172   def __init__(self, schema, seen, dent=0):
    173     """Constructor.
    174 
    175     Args:
    176       schema: object, Parsed JSON schema.
    177       seen: list, List of names of schema already seen while parsing. Used to
    178         handle recursive definitions.
    179       dent: int, Initial indentation depth.
    180     """
    181     # The result of this parsing kept as list of strings.
    182     self.value = []
    183 
    184     # The final value of the parsing.
    185     self.string = None
    186 
    187     # The parsed JSON schema.
    188     self.schema = schema
    189 
    190     # Indentation level.
    191     self.dent = dent
    192 
    193     # Method that when called returns a prototype object for the schema with
    194     # the given name.
    195     self.from_cache = None
    196 
    197     # List of names of schema already seen while parsing.
    198     self.seen = seen
    199 
    200   def emit(self, text):
    201     """Add text as a line to the output.
    202 
    203     Args:
    204       text: string, Text to output.
    205     """
    206     self.value.extend(["  " * self.dent, text, '\n'])
    207 
    208   def emitBegin(self, text):
    209     """Add text to the output, but with no line terminator.
    210 
    211     Args:
    212       text: string, Text to output.
    213       """
    214     self.value.extend(["  " * self.dent, text])
    215 
    216   def emitEnd(self, text, comment):
    217     """Add text and comment to the output with line terminator.
    218 
    219     Args:
    220       text: string, Text to output.
    221       comment: string, Python comment.
    222     """
    223     if comment:
    224       divider = '\n' + '  ' * (self.dent + 2) + '# '
    225       lines = comment.splitlines()
    226       lines = [x.rstrip() for x in lines]
    227       comment = divider.join(lines)
    228       self.value.extend([text, ' # ', comment, '\n'])
    229     else:
    230       self.value.extend([text, '\n'])
    231 
    232   def indent(self):
    233     """Increase indentation level."""
    234     self.dent += 1
    235 
    236   def undent(self):
    237     """Decrease indentation level."""
    238     self.dent -= 1
    239 
    240   def _to_str_impl(self, schema):
    241     """Prototype object based on the schema, in Python code with comments.
    242 
    243     Args:
    244       schema: object, Parsed JSON schema file.
    245 
    246     Returns:
    247       Prototype object based on the schema, in Python code with comments.
    248     """
    249     stype = schema.get('type')
    250     if stype == 'object':
    251       self.emitEnd('{', schema.get('description', ''))
    252       self.indent()
    253       if 'properties' in schema:
    254         for pname, pschema in six.iteritems(schema.get('properties', {})):
    255           self.emitBegin('"%s": ' % pname)
    256           self._to_str_impl(pschema)
    257       elif 'additionalProperties' in schema:
    258         self.emitBegin('"a_key": ')
    259         self._to_str_impl(schema['additionalProperties'])
    260       self.undent()
    261       self.emit('},')
    262     elif '$ref' in schema:
    263       schemaName = schema['$ref']
    264       description = schema.get('description', '')
    265       s = self.from_cache(schemaName, seen=self.seen)
    266       parts = s.splitlines()
    267       self.emitEnd(parts[0], description)
    268       for line in parts[1:]:
    269         self.emit(line.rstrip())
    270     elif stype == 'boolean':
    271       value = schema.get('default', 'True or False')
    272       self.emitEnd('%s,' % str(value), schema.get('description', ''))
    273     elif stype == 'string':
    274       value = schema.get('default', 'A String')
    275       self.emitEnd('"%s",' % str(value), schema.get('description', ''))
    276     elif stype == 'integer':
    277       value = schema.get('default', '42')
    278       self.emitEnd('%s,' % str(value), schema.get('description', ''))
    279     elif stype == 'number':
    280       value = schema.get('default', '3.14')
    281       self.emitEnd('%s,' % str(value), schema.get('description', ''))
    282     elif stype == 'null':
    283       self.emitEnd('None,', schema.get('description', ''))
    284     elif stype == 'any':
    285       self.emitEnd('"",', schema.get('description', ''))
    286     elif stype == 'array':
    287       self.emitEnd('[', schema.get('description'))
    288       self.indent()
    289       self.emitBegin('')
    290       self._to_str_impl(schema['items'])
    291       self.undent()
    292       self.emit('],')
    293     else:
    294       self.emit('Unknown type! %s' % stype)
    295       self.emitEnd('', '')
    296 
    297     self.string = ''.join(self.value)
    298     return self.string
    299 
    300   def to_str(self, from_cache):
    301     """Prototype object based on the schema, in Python code with comments.
    302 
    303     Args:
    304       from_cache: callable(name, seen), Callable that retrieves an object
    305          prototype for a schema with the given name. Seen is a list of schema
    306          names already seen as we recursively descend the schema definition.
    307 
    308     Returns:
    309       Prototype object based on the schema, in Python code with comments.
    310       The lines of the code will all be properly indented.
    311     """
    312     self.from_cache = from_cache
    313     return self._to_str_impl(self.schema)
    314