blob: 022cb0acfd3979edce333ae2f709ea9c66b8a4c7 [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
Helen Koikede13e3b2018-04-26 16:05:16 -030068from googleapiclient import _helpers as util
Joe Gregorio2b781282011-12-08 12:00:25 -050069
70
71class Schemas(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070072 """Schemas for an API."""
Joe Gregorio2b781282011-12-08 12:00:25 -050073
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070074 def __init__(self, discovery):
75 """Constructor.
Joe Gregorio2b781282011-12-08 12:00:25 -050076
77 Args:
78 discovery: object, Deserialized discovery document from which we pull
79 out the named schema.
80 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070081 self.schemas = discovery.get("schemas", {})
Joe Gregorio2b781282011-12-08 12:00:25 -050082
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070083 # Cache of pretty printed schemas.
84 self.pretty = {}
Joe Gregorio2b781282011-12-08 12:00:25 -050085
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070086 @util.positional(2)
87 def _prettyPrintByName(self, name, seen=None, dent=0):
88 """Get pretty printed object prototype from the schema name.
Joe Gregorio2b781282011-12-08 12:00:25 -050089
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070099 if seen is None:
100 seen = []
Joe Gregorio2b781282011-12-08 12:00:25 -0500101
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700102 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)
Joe Gregorio2b781282011-12-08 12:00:25 -0500106
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700107 if name not in self.pretty:
108 self.pretty[name] = _SchemaToStruct(
109 self.schemas[name], seen, dent=dent
110 ).to_str(self._prettyPrintByName)
Joe Gregorio2b781282011-12-08 12:00:25 -0500111
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700112 seen.pop()
Joe Gregorio2b781282011-12-08 12:00:25 -0500113
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700114 return self.pretty[name]
Joe Gregorio2b781282011-12-08 12:00:25 -0500115
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700116 def prettyPrintByName(self, name):
117 """Get pretty printed object prototype from the schema name.
Joe Gregorio2b781282011-12-08 12:00:25 -0500118
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700126 # Return with trailing comma and newline removed.
127 return self._prettyPrintByName(name, seen=[], dent=1)[:-2]
Joe Gregorio2b781282011-12-08 12:00:25 -0500128
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700129 @util.positional(2)
130 def _prettyPrintSchema(self, schema, seen=None, dent=0):
131 """Get pretty printed object prototype of schema.
Joe Gregorio2b781282011-12-08 12:00:25 -0500132
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700142 if seen is None:
143 seen = []
Joe Gregorio2b781282011-12-08 12:00:25 -0500144
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700145 return _SchemaToStruct(schema, seen, dent=dent).to_str(self._prettyPrintByName)
Joe Gregorio2b781282011-12-08 12:00:25 -0500146
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700147 def prettyPrintSchema(self, schema):
148 """Get pretty printed object prototype of schema.
Joe Gregorio2b781282011-12-08 12:00:25 -0500149
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700157 # Return with trailing comma and newline removed.
158 return self._prettyPrintSchema(schema, dent=1)[:-2]
Joe Gregorio2b781282011-12-08 12:00:25 -0500159
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700160 def get(self, name, default=None):
161 """Get deserialized JSON schema from the schema name.
Joe Gregorio2b781282011-12-08 12:00:25 -0500162
163 Args:
164 name: string, Schema name.
Thomas Coffee20af04d2017-02-10 15:24:44 -0800165 default: object, return value if name not found.
Joe Gregorio2b781282011-12-08 12:00:25 -0500166 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700167 return self.schemas.get(name, default)
Joe Gregorio2b781282011-12-08 12:00:25 -0500168
169
170class _SchemaToStruct(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700171 """Convert schema to a prototype object."""
Joe Gregorio2b781282011-12-08 12:00:25 -0500172
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700173 @util.positional(3)
174 def __init__(self, schema, seen, dent=0):
175 """Constructor.
Joe Gregorio2b781282011-12-08 12:00:25 -0500176
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700183 # The result of this parsing kept as list of strings.
184 self.value = []
Joe Gregorio2b781282011-12-08 12:00:25 -0500185
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700186 # The final value of the parsing.
187 self.string = None
Joe Gregorio2b781282011-12-08 12:00:25 -0500188
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700189 # The parsed JSON schema.
190 self.schema = schema
Joe Gregorio2b781282011-12-08 12:00:25 -0500191
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700192 # Indentation level.
193 self.dent = dent
Joe Gregorio2b781282011-12-08 12:00:25 -0500194
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700195 # Method that when called returns a prototype object for the schema with
196 # the given name.
197 self.from_cache = None
Joe Gregorio2b781282011-12-08 12:00:25 -0500198
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700199 # List of names of schema already seen while parsing.
200 self.seen = seen
Joe Gregorio2b781282011-12-08 12:00:25 -0500201
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700202 def emit(self, text):
203 """Add text as a line to the output.
Joe Gregorio2b781282011-12-08 12:00:25 -0500204
205 Args:
206 text: string, Text to output.
207 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700208 self.value.extend([" " * self.dent, text, "\n"])
Joe Gregorio2b781282011-12-08 12:00:25 -0500209
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700210 def emitBegin(self, text):
211 """Add text to the output, but with no line terminator.
Joe Gregorio2b781282011-12-08 12:00:25 -0500212
213 Args:
214 text: string, Text to output.
215 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700216 self.value.extend([" " * self.dent, text])
Joe Gregorio2b781282011-12-08 12:00:25 -0500217
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700218 def emitEnd(self, text, comment):
219 """Add text and comment to the output with line terminator.
Joe Gregorio2b781282011-12-08 12:00:25 -0500220
221 Args:
222 text: string, Text to output.
223 comment: string, Python comment.
224 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700225 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"])
Joe Gregorio2b781282011-12-08 12:00:25 -0500233
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700234 def indent(self):
235 """Increase indentation level."""
236 self.dent += 1
Joe Gregorio2b781282011-12-08 12:00:25 -0500237
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700238 def undent(self):
239 """Decrease indentation level."""
240 self.dent -= 1
Joe Gregorio2b781282011-12-08 12:00:25 -0500241
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700242 def _to_str_impl(self, schema):
243 """Prototype object based on the schema, in Python code with comments.
Joe Gregorio2b781282011-12-08 12:00:25 -0500244
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700251 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("", "")
Joe Gregorio2b781282011-12-08 12:00:25 -0500298
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700299 self.string = "".join(self.value)
300 return self.string
Joe Gregorio2b781282011-12-08 12:00:25 -0500301
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700302 def to_str(self, from_cache):
303 """Prototype object based on the schema, in Python code with comments.
Joe Gregorio2b781282011-12-08 12:00:25 -0500304
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700314 self.from_cache = from_cache
315 return self._to_str_impl(self.schema)