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
60
61# TODO(jcgregorio) support format, enum, minimum, maximum
62
63__author__ = "[email protected] (Joe Gregorio)"
64
65
66from collections import OrderedDict
67from googleapiclient import _helpers as util
68
69
70class 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(
108                self.schemas[name], seen, dent=dent
109            ).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=0)[:-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=0)[:-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                properties = schema.get("properties", {})
256                sorted_properties = OrderedDict(sorted(properties.items()))
257                for pname, pschema in sorted_properties.items():
258                    self.emitBegin('"%s": ' % pname)
259                    self._to_str_impl(pschema)
260            elif "additionalProperties" in schema:
261                self.emitBegin('"a_key": ')
262                self._to_str_impl(schema["additionalProperties"])
263            self.undent()
264            self.emit("},")
265        elif "$ref" in schema:
266            schemaName = schema["$ref"]
267            description = schema.get("description", "")
268            s = self.from_cache(schemaName, seen=self.seen)
269            parts = s.splitlines()
270            self.emitEnd(parts[0], description)
271            for line in parts[1:]:
272                self.emit(line.rstrip())
273        elif stype == "boolean":
274            value = schema.get("default", "True or False")
275            self.emitEnd("%s," % str(value), schema.get("description", ""))
276        elif stype == "string":
277            value = schema.get("default", "A String")
278            self.emitEnd('"%s",' % str(value), schema.get("description", ""))
279        elif stype == "integer":
280            value = schema.get("default", "42")
281            self.emitEnd("%s," % str(value), schema.get("description", ""))
282        elif stype == "number":
283            value = schema.get("default", "3.14")
284            self.emitEnd("%s," % str(value), schema.get("description", ""))
285        elif stype == "null":
286            self.emitEnd("None,", schema.get("description", ""))
287        elif stype == "any":
288            self.emitEnd('"",', schema.get("description", ""))
289        elif stype == "array":
290            self.emitEnd("[", schema.get("description"))
291            self.indent()
292            self.emitBegin("")
293            self._to_str_impl(schema["items"])
294            self.undent()
295            self.emit("],")
296        else:
297            self.emit("Unknown type! %s" % stype)
298            self.emitEnd("", "")
299
300        self.string = "".join(self.value)
301        return self.string
302
303    def to_str(self, from_cache):
304        """Prototype object based on the schema, in Python code with comments.
305
306        Args:
307          from_cache: callable(name, seen), Callable that retrieves an object
308             prototype for a schema with the given name. Seen is a list of schema
309             names already seen as we recursively descend the schema definition.
310
311        Returns:
312          Prototype object based on the schema, in Python code with comments.
313          The lines of the code will all be properly indented.
314        """
315        self.from_cache = from_cache
316        return self._to_str_impl(self.schema)
317