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