blob: 2d589840cf2c471387397efd709d2e32c19aeca9 [file] [log] [blame]
Craig Citro751b7fb2014-09-23 11:20:38 -07001# Copyright 2014 Google Inc. All Rights Reserved.
Joe Gregorio2b781282011-12-08 12:00:25 -05002#
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"""
INADA Naokie4ea1a92015-03-04 03:45:42 +090059from __future__ import absolute_import
60import six
Joe Gregorio2b781282011-12-08 12:00:25 -050061
62# TODO(jcgregorio) support format, enum, minimum, maximum
63
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070064__author__ = "jcgregorio@google.com (Joe Gregorio)"
Joe Gregorio2b781282011-12-08 12:00:25 -050065
66import copy
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040067
Anthonios Parthenioub1b0c832020-12-14 14:24:19 -050068from collections import OrderedDict
Helen Koikede13e3b2018-04-26 16:05:16 -030069from googleapiclient import _helpers as util
Joe Gregorio2b781282011-12-08 12:00:25 -050070
71
72class Schemas(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070073 """Schemas for an API."""
Joe Gregorio2b781282011-12-08 12:00:25 -050074
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070075 def __init__(self, discovery):
76 """Constructor.
Joe Gregorio2b781282011-12-08 12:00:25 -050077
78 Args:
79 discovery: object, Deserialized discovery document from which we pull
80 out the named schema.
81 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070082 self.schemas = discovery.get("schemas", {})
Joe Gregorio2b781282011-12-08 12:00:25 -050083
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070084 # Cache of pretty printed schemas.
85 self.pretty = {}
Joe Gregorio2b781282011-12-08 12:00:25 -050086
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070087 @util.positional(2)
88 def _prettyPrintByName(self, name, seen=None, dent=0):
89 """Get pretty printed object prototype from the schema name.
Joe Gregorio2b781282011-12-08 12:00:25 -050090
91 Args:
92 name: string, Name of schema in the discovery document.
93 seen: list of string, Names of schema already seen. Used to handle
94 recursive definitions.
95
96 Returns:
97 string, A string that contains a prototype object with
98 comments that conforms to the given schema.
99 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700100 if seen is None:
101 seen = []
Joe Gregorio2b781282011-12-08 12:00:25 -0500102
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700103 if name in seen:
104 # Do not fall into an infinite loop over recursive definitions.
105 return "# Object with schema name: %s" % name
106 seen.append(name)
Joe Gregorio2b781282011-12-08 12:00:25 -0500107
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700108 if name not in self.pretty:
109 self.pretty[name] = _SchemaToStruct(
110 self.schemas[name], seen, dent=dent
111 ).to_str(self._prettyPrintByName)
Joe Gregorio2b781282011-12-08 12:00:25 -0500112
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700113 seen.pop()
Joe Gregorio2b781282011-12-08 12:00:25 -0500114
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700115 return self.pretty[name]
Joe Gregorio2b781282011-12-08 12:00:25 -0500116
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700117 def prettyPrintByName(self, name):
118 """Get pretty printed object prototype from the schema name.
Joe Gregorio2b781282011-12-08 12:00:25 -0500119
120 Args:
121 name: string, Name of schema in the discovery document.
122
123 Returns:
124 string, A string that contains a prototype object with
125 comments that conforms to the given schema.
126 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700127 # Return with trailing comma and newline removed.
Anthonios Parthenioub1b0c832020-12-14 14:24:19 -0500128 return self._prettyPrintByName(name, seen=[], dent=0)[:-2]
Joe Gregorio2b781282011-12-08 12:00:25 -0500129
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700130 @util.positional(2)
131 def _prettyPrintSchema(self, schema, seen=None, dent=0):
132 """Get pretty printed object prototype of schema.
Joe Gregorio2b781282011-12-08 12:00:25 -0500133
134 Args:
135 schema: object, Parsed JSON schema.
136 seen: list of string, Names of schema already seen. Used to handle
137 recursive definitions.
138
139 Returns:
140 string, A string that contains a prototype object with
141 comments that conforms to the given schema.
142 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700143 if seen is None:
144 seen = []
Joe Gregorio2b781282011-12-08 12:00:25 -0500145
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700146 return _SchemaToStruct(schema, seen, dent=dent).to_str(self._prettyPrintByName)
Joe Gregorio2b781282011-12-08 12:00:25 -0500147
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700148 def prettyPrintSchema(self, schema):
149 """Get pretty printed object prototype of schema.
Joe Gregorio2b781282011-12-08 12:00:25 -0500150
151 Args:
152 schema: object, Parsed JSON schema.
153
154 Returns:
155 string, A string that contains a prototype object with
156 comments that conforms to the given schema.
157 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700158 # Return with trailing comma and newline removed.
Anthonios Parthenioub1b0c832020-12-14 14:24:19 -0500159 return self._prettyPrintSchema(schema, dent=0)[:-2]
Joe Gregorio2b781282011-12-08 12:00:25 -0500160
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700161 def get(self, name, default=None):
162 """Get deserialized JSON schema from the schema name.
Joe Gregorio2b781282011-12-08 12:00:25 -0500163
164 Args:
165 name: string, Schema name.
Thomas Coffee20af04d2017-02-10 15:24:44 -0800166 default: object, return value if name not found.
Joe Gregorio2b781282011-12-08 12:00:25 -0500167 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700168 return self.schemas.get(name, default)
Joe Gregorio2b781282011-12-08 12:00:25 -0500169
170
171class _SchemaToStruct(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700172 """Convert schema to a prototype object."""
Joe Gregorio2b781282011-12-08 12:00:25 -0500173
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700174 @util.positional(3)
175 def __init__(self, schema, seen, dent=0):
176 """Constructor.
Joe Gregorio2b781282011-12-08 12:00:25 -0500177
178 Args:
179 schema: object, Parsed JSON schema.
180 seen: list, List of names of schema already seen while parsing. Used to
181 handle recursive definitions.
182 dent: int, Initial indentation depth.
183 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700184 # The result of this parsing kept as list of strings.
185 self.value = []
Joe Gregorio2b781282011-12-08 12:00:25 -0500186
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700187 # The final value of the parsing.
188 self.string = None
Joe Gregorio2b781282011-12-08 12:00:25 -0500189
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700190 # The parsed JSON schema.
191 self.schema = schema
Joe Gregorio2b781282011-12-08 12:00:25 -0500192
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700193 # Indentation level.
194 self.dent = dent
Joe Gregorio2b781282011-12-08 12:00:25 -0500195
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700196 # Method that when called returns a prototype object for the schema with
197 # the given name.
198 self.from_cache = None
Joe Gregorio2b781282011-12-08 12:00:25 -0500199
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700200 # List of names of schema already seen while parsing.
201 self.seen = seen
Joe Gregorio2b781282011-12-08 12:00:25 -0500202
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700203 def emit(self, text):
204 """Add text as a line to the output.
Joe Gregorio2b781282011-12-08 12:00:25 -0500205
206 Args:
207 text: string, Text to output.
208 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700209 self.value.extend([" " * self.dent, text, "\n"])
Joe Gregorio2b781282011-12-08 12:00:25 -0500210
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700211 def emitBegin(self, text):
212 """Add text to the output, but with no line terminator.
Joe Gregorio2b781282011-12-08 12:00:25 -0500213
214 Args:
215 text: string, Text to output.
216 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700217 self.value.extend([" " * self.dent, text])
Joe Gregorio2b781282011-12-08 12:00:25 -0500218
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700219 def emitEnd(self, text, comment):
220 """Add text and comment to the output with line terminator.
Joe Gregorio2b781282011-12-08 12:00:25 -0500221
222 Args:
223 text: string, Text to output.
224 comment: string, Python comment.
225 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700226 if comment:
227 divider = "\n" + " " * (self.dent + 2) + "# "
228 lines = comment.splitlines()
229 lines = [x.rstrip() for x in lines]
230 comment = divider.join(lines)
231 self.value.extend([text, " # ", comment, "\n"])
232 else:
233 self.value.extend([text, "\n"])
Joe Gregorio2b781282011-12-08 12:00:25 -0500234
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700235 def indent(self):
236 """Increase indentation level."""
237 self.dent += 1
Joe Gregorio2b781282011-12-08 12:00:25 -0500238
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700239 def undent(self):
240 """Decrease indentation level."""
241 self.dent -= 1
Joe Gregorio2b781282011-12-08 12:00:25 -0500242
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700243 def _to_str_impl(self, schema):
244 """Prototype object based on the schema, in Python code with comments.
Joe Gregorio2b781282011-12-08 12:00:25 -0500245
246 Args:
247 schema: object, Parsed JSON schema file.
248
249 Returns:
250 Prototype object based on the schema, in Python code with comments.
251 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700252 stype = schema.get("type")
253 if stype == "object":
254 self.emitEnd("{", schema.get("description", ""))
255 self.indent()
256 if "properties" in schema:
Anthonios Parthenioub1b0c832020-12-14 14:24:19 -0500257 properties = schema.get("properties", {})
258 sorted_properties = OrderedDict(sorted(properties.items()))
259 for pname, pschema in six.iteritems(sorted_properties):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700260 self.emitBegin('"%s": ' % pname)
261 self._to_str_impl(pschema)
262 elif "additionalProperties" in schema:
263 self.emitBegin('"a_key": ')
264 self._to_str_impl(schema["additionalProperties"])
265 self.undent()
266 self.emit("},")
267 elif "$ref" in schema:
268 schemaName = schema["$ref"]
269 description = schema.get("description", "")
270 s = self.from_cache(schemaName, seen=self.seen)
271 parts = s.splitlines()
272 self.emitEnd(parts[0], description)
273 for line in parts[1:]:
274 self.emit(line.rstrip())
275 elif stype == "boolean":
276 value = schema.get("default", "True or False")
277 self.emitEnd("%s," % str(value), schema.get("description", ""))
278 elif stype == "string":
279 value = schema.get("default", "A String")
280 self.emitEnd('"%s",' % str(value), schema.get("description", ""))
281 elif stype == "integer":
282 value = schema.get("default", "42")
283 self.emitEnd("%s," % str(value), schema.get("description", ""))
284 elif stype == "number":
285 value = schema.get("default", "3.14")
286 self.emitEnd("%s," % str(value), schema.get("description", ""))
287 elif stype == "null":
288 self.emitEnd("None,", schema.get("description", ""))
289 elif stype == "any":
290 self.emitEnd('"",', schema.get("description", ""))
291 elif stype == "array":
292 self.emitEnd("[", schema.get("description"))
293 self.indent()
294 self.emitBegin("")
295 self._to_str_impl(schema["items"])
296 self.undent()
297 self.emit("],")
298 else:
299 self.emit("Unknown type! %s" % stype)
300 self.emitEnd("", "")
Joe Gregorio2b781282011-12-08 12:00:25 -0500301
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700302 self.string = "".join(self.value)
303 return self.string
Joe Gregorio2b781282011-12-08 12:00:25 -0500304
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700305 def to_str(self, from_cache):
306 """Prototype object based on the schema, in Python code with comments.
Joe Gregorio2b781282011-12-08 12:00:25 -0500307
308 Args:
309 from_cache: callable(name, seen), Callable that retrieves an object
310 prototype for a schema with the given name. Seen is a list of schema
311 names already seen as we recursively descend the schema definition.
312
313 Returns:
314 Prototype object based on the schema, in Python code with comments.
315 The lines of the code will all be properly indented.
316 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700317 self.from_cache = from_cache
318 return self._to_str_impl(self.schema)