• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
17Schemas holds an APIs discovery schemas. It can return those schema as
18deserialized JSON objects, or pretty print them as prototype objects that
19conform to the schema.
20
21For 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
57The constructor takes a discovery document in which to look up named schema.
58"""
59from __future__ import absolute_import
60import six
61
62# TODO(jcgregorio) support format, enum, minimum, maximum
63
64__author__ = 'jcgregorio@google.com (Joe Gregorio)'
65
66import copy
67
68from googleapiclient import _helpers as util
69
70
71class 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
169class _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