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 googleapiclient import _helpers as 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, default=None):
    160     """Get deserialized JSON schema from the schema name.
    161 
    162     Args:
    163       name: string, Schema name.
    164       default: object, return value if name not found.
    165     """
    166     return self.schemas.get(name, default)
    167 
    168 
    169 class _SchemaToStruct(object):
    170   """Convert schema to a prototype object."""
    171 
    172   @util.positional(3)
    173   def __init__(self, schema, seen, dent=0):
    174     """Constructor.
    175 
    176     Args:
    177       schema: object, Parsed JSON schema.
    178       seen: list, List of names of schema already seen while parsing. Used to
    179         handle recursive definitions.
    180       dent: int, Initial indentation depth.
    181     """
    182     # The result of this parsing kept as list of strings.
    183     self.value = []
    184 
    185     # The final value of the parsing.
    186     self.string = None
    187 
    188     # The parsed JSON schema.
    189     self.schema = schema
    190 
    191     # Indentation level.
    192     self.dent = dent
    193 
    194     # Method that when called returns a prototype object for the schema with
    195     # the given name.
    196     self.from_cache = None
    197 
    198     # List of names of schema already seen while parsing.
    199     self.seen = seen
    200 
    201   def emit(self, text):
    202     """Add text as a line to the output.
    203 
    204     Args:
    205       text: string, Text to output.
    206     """
    207     self.value.extend(["  " * self.dent, text, '\n'])
    208 
    209   def emitBegin(self, text):
    210     """Add text to the output, but with no line terminator.
    211 
    212     Args:
    213       text: string, Text to output.
    214       """
    215     self.value.extend(["  " * self.dent, text])
    216 
    217   def emitEnd(self, text, comment):
    218     """Add text and comment to the output with line terminator.
    219 
    220     Args:
    221       text: string, Text to output.
    222       comment: string, Python comment.
    223     """
    224     if comment:
    225       divider = '\n' + '  ' * (self.dent + 2) + '# '
    226       lines = comment.splitlines()
    227       lines = [x.rstrip() for x in lines]
    228       comment = divider.join(lines)
    229       self.value.extend([text, ' # ', comment, '\n'])
    230     else:
    231       self.value.extend([text, '\n'])
    232 
    233   def indent(self):
    234     """Increase indentation level."""
    235     self.dent += 1
    236 
    237   def undent(self):
    238     """Decrease indentation level."""
    239     self.dent -= 1
    240 
    241   def _to_str_impl(self, schema):
    242     """Prototype object based on the schema, in Python code with comments.
    243 
    244     Args:
    245       schema: object, Parsed JSON schema file.
    246 
    247     Returns:
    248       Prototype object based on the schema, in Python code with comments.
    249     """
    250     stype = schema.get('type')
    251     if stype == 'object':
    252       self.emitEnd('{', schema.get('description', ''))
    253       self.indent()
    254       if 'properties' in schema:
    255         for pname, pschema in six.iteritems(schema.get('properties', {})):
    256           self.emitBegin('"%s": ' % pname)
    257           self._to_str_impl(pschema)
    258       elif 'additionalProperties' in schema:
    259         self.emitBegin('"a_key": ')
    260         self._to_str_impl(schema['additionalProperties'])
    261       self.undent()
    262       self.emit('},')
    263     elif '$ref' in schema:
    264       schemaName = schema['$ref']
    265       description = schema.get('description', '')
    266       s = self.from_cache(schemaName, seen=self.seen)
    267       parts = s.splitlines()
    268       self.emitEnd(parts[0], description)
    269       for line in parts[1:]:
    270         self.emit(line.rstrip())
    271     elif stype == 'boolean':
    272       value = schema.get('default', 'True or False')
    273       self.emitEnd('%s,' % str(value), schema.get('description', ''))
    274     elif stype == 'string':
    275       value = schema.get('default', 'A String')
    276       self.emitEnd('"%s",' % str(value), schema.get('description', ''))
    277     elif stype == 'integer':
    278       value = schema.get('default', '42')
    279       self.emitEnd('%s,' % str(value), schema.get('description', ''))
    280     elif stype == 'number':
    281       value = schema.get('default', '3.14')
    282       self.emitEnd('%s,' % str(value), schema.get('description', ''))
    283     elif stype == 'null':
    284       self.emitEnd('None,', schema.get('description', ''))
    285     elif stype == 'any':
    286       self.emitEnd('"",', schema.get('description', ''))
    287     elif stype == 'array':
    288       self.emitEnd('[', schema.get('description'))
    289       self.indent()
    290       self.emitBegin('')
    291       self._to_str_impl(schema['items'])
    292       self.undent()
    293       self.emit('],')
    294     else:
    295       self.emit('Unknown type! %s' % stype)
    296       self.emitEnd('', '')
    297 
    298     self.string = ''.join(self.value)
    299     return self.string
    300 
    301   def to_str(self, from_cache):
    302     """Prototype object based on the schema, in Python code with comments.
    303 
    304     Args:
    305       from_cache: callable(name, seen), Callable that retrieves an object
    306          prototype for a schema with the given name. Seen is a list of schema
    307          names already seen as we recursively descend the schema definition.
    308 
    309     Returns:
    310       Prototype object based on the schema, in Python code with comments.
    311       The lines of the code will all be properly indented.
    312     """
    313     self.from_cache = from_cache
    314     return self._to_str_impl(self.schema)
    315