blob: 95767ef55c8104765e12402c0cb64e7c967fcb3f [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
Joe Gregorio2b781282011-12-08 12:00:25 -050060
61# TODO(jcgregorio) support format, enum, minimum, maximum
62
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070063__author__ = "jcgregorio@google.com (Joe Gregorio)"
Joe Gregorio2b781282011-12-08 12:00:25 -050064
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040065
Anthonios Parthenioub1b0c832020-12-14 14:24:19 -050066from collections import OrderedDict
Helen Koikede13e3b2018-04-26 16:05:16 -030067from googleapiclient import _helpers as util
Joe Gregorio2b781282011-12-08 12:00:25 -050068
69
70class Schemas(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070071 """Schemas for an API."""
Joe Gregorio2b781282011-12-08 12:00:25 -050072
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070073 def __init__(self, discovery):
74 """Constructor.
Joe Gregorio2b781282011-12-08 12:00:25 -050075
arfy slowyd35c9122021-07-15 00:16:31 +070076 Args:
77 discovery: object, Deserialized discovery document from which we pull
78 out the named schema.
79 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070080 self.schemas = discovery.get("schemas", {})
Joe Gregorio2b781282011-12-08 12:00:25 -050081
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070082 # Cache of pretty printed schemas.
83 self.pretty = {}
Joe Gregorio2b781282011-12-08 12:00:25 -050084
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070085 @util.positional(2)
86 def _prettyPrintByName(self, name, seen=None, dent=0):
87 """Get pretty printed object prototype from the schema name.
Joe Gregorio2b781282011-12-08 12:00:25 -050088
arfy slowyd35c9122021-07-15 00:16:31 +070089 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.
Joe Gregorio2b781282011-12-08 12:00:25 -050093
arfy slowyd35c9122021-07-15 00:16:31 +070094 Returns:
95 string, A string that contains a prototype object with
96 comments that conforms to the given schema.
97 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070098 if seen is None:
99 seen = []
Joe Gregorio2b781282011-12-08 12:00:25 -0500100
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700101 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)
Joe Gregorio2b781282011-12-08 12:00:25 -0500105
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700106 if name not in self.pretty:
107 self.pretty[name] = _SchemaToStruct(
108 self.schemas[name], seen, dent=dent
109 ).to_str(self._prettyPrintByName)
Joe Gregorio2b781282011-12-08 12:00:25 -0500110
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700111 seen.pop()
Joe Gregorio2b781282011-12-08 12:00:25 -0500112
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700113 return self.pretty[name]
Joe Gregorio2b781282011-12-08 12:00:25 -0500114
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700115 def prettyPrintByName(self, name):
116 """Get pretty printed object prototype from the schema name.
Joe Gregorio2b781282011-12-08 12:00:25 -0500117
arfy slowyd35c9122021-07-15 00:16:31 +0700118 Args:
119 name: string, Name of schema in the discovery document.
Joe Gregorio2b781282011-12-08 12:00:25 -0500120
arfy slowyd35c9122021-07-15 00:16:31 +0700121 Returns:
122 string, A string that contains a prototype object with
123 comments that conforms to the given schema.
124 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700125 # Return with trailing comma and newline removed.
Anthonios Parthenioub1b0c832020-12-14 14:24:19 -0500126 return self._prettyPrintByName(name, seen=[], dent=0)[:-2]
Joe Gregorio2b781282011-12-08 12:00:25 -0500127
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700128 @util.positional(2)
129 def _prettyPrintSchema(self, schema, seen=None, dent=0):
130 """Get pretty printed object prototype of schema.
Joe Gregorio2b781282011-12-08 12:00:25 -0500131
arfy slowyd35c9122021-07-15 00:16:31 +0700132 Args:
133 schema: object, Parsed JSON schema.
134 seen: list of string, Names of schema already seen. Used to handle
135 recursive definitions.
Joe Gregorio2b781282011-12-08 12:00:25 -0500136
arfy slowyd35c9122021-07-15 00:16:31 +0700137 Returns:
138 string, A string that contains a prototype object with
139 comments that conforms to the given schema.
140 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700141 if seen is None:
142 seen = []
Joe Gregorio2b781282011-12-08 12:00:25 -0500143
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700144 return _SchemaToStruct(schema, seen, dent=dent).to_str(self._prettyPrintByName)
Joe Gregorio2b781282011-12-08 12:00:25 -0500145
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700146 def prettyPrintSchema(self, schema):
147 """Get pretty printed object prototype of schema.
Joe Gregorio2b781282011-12-08 12:00:25 -0500148
arfy slowyd35c9122021-07-15 00:16:31 +0700149 Args:
150 schema: object, Parsed JSON schema.
Joe Gregorio2b781282011-12-08 12:00:25 -0500151
arfy slowyd35c9122021-07-15 00:16:31 +0700152 Returns:
153 string, A string that contains a prototype object with
154 comments that conforms to the given schema.
155 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700156 # Return with trailing comma and newline removed.
Anthonios Parthenioub1b0c832020-12-14 14:24:19 -0500157 return self._prettyPrintSchema(schema, dent=0)[:-2]
Joe Gregorio2b781282011-12-08 12:00:25 -0500158
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700159 def get(self, name, default=None):
160 """Get deserialized JSON schema from the schema name.
Joe Gregorio2b781282011-12-08 12:00:25 -0500161
arfy slowyd35c9122021-07-15 00:16:31 +0700162 Args:
163 name: string, Schema name.
164 default: object, return value if name not found.
165 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700166 return self.schemas.get(name, default)
Joe Gregorio2b781282011-12-08 12:00:25 -0500167
168
169class _SchemaToStruct(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700170 """Convert schema to a prototype object."""
Joe Gregorio2b781282011-12-08 12:00:25 -0500171
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700172 @util.positional(3)
173 def __init__(self, schema, seen, dent=0):
174 """Constructor.
Joe Gregorio2b781282011-12-08 12:00:25 -0500175
arfy slowyd35c9122021-07-15 00:16:31 +0700176 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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700182 # The result of this parsing kept as list of strings.
183 self.value = []
Joe Gregorio2b781282011-12-08 12:00:25 -0500184
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700185 # The final value of the parsing.
186 self.string = None
Joe Gregorio2b781282011-12-08 12:00:25 -0500187
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700188 # The parsed JSON schema.
189 self.schema = schema
Joe Gregorio2b781282011-12-08 12:00:25 -0500190
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700191 # Indentation level.
192 self.dent = dent
Joe Gregorio2b781282011-12-08 12:00:25 -0500193
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700194 # Method that when called returns a prototype object for the schema with
195 # the given name.
196 self.from_cache = None
Joe Gregorio2b781282011-12-08 12:00:25 -0500197
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700198 # List of names of schema already seen while parsing.
199 self.seen = seen
Joe Gregorio2b781282011-12-08 12:00:25 -0500200
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700201 def emit(self, text):
202 """Add text as a line to the output.
Joe Gregorio2b781282011-12-08 12:00:25 -0500203
arfy slowyd35c9122021-07-15 00:16:31 +0700204 Args:
205 text: string, Text to output.
206 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700207 self.value.extend([" " * self.dent, text, "\n"])
Joe Gregorio2b781282011-12-08 12:00:25 -0500208
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700209 def emitBegin(self, text):
210 """Add text to the output, but with no line terminator.
Joe Gregorio2b781282011-12-08 12:00:25 -0500211
arfy slowyd35c9122021-07-15 00:16:31 +0700212 Args:
213 text: string, Text to output.
214 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700215 self.value.extend([" " * self.dent, text])
Joe Gregorio2b781282011-12-08 12:00:25 -0500216
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700217 def emitEnd(self, text, comment):
218 """Add text and comment to the output with line terminator.
Joe Gregorio2b781282011-12-08 12:00:25 -0500219
arfy slowyd35c9122021-07-15 00:16:31 +0700220 Args:
221 text: string, Text to output.
222 comment: string, Python comment.
223 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700224 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"])
Joe Gregorio2b781282011-12-08 12:00:25 -0500232
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700233 def indent(self):
234 """Increase indentation level."""
235 self.dent += 1
Joe Gregorio2b781282011-12-08 12:00:25 -0500236
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700237 def undent(self):
238 """Decrease indentation level."""
239 self.dent -= 1
Joe Gregorio2b781282011-12-08 12:00:25 -0500240
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700241 def _to_str_impl(self, schema):
242 """Prototype object based on the schema, in Python code with comments.
Joe Gregorio2b781282011-12-08 12:00:25 -0500243
arfy slowyd35c9122021-07-15 00:16:31 +0700244 Args:
245 schema: object, Parsed JSON schema file.
Joe Gregorio2b781282011-12-08 12:00:25 -0500246
arfy slowyd35c9122021-07-15 00:16:31 +0700247 Returns:
248 Prototype object based on the schema, in Python code with comments.
249 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700250 stype = schema.get("type")
251 if stype == "object":
252 self.emitEnd("{", schema.get("description", ""))
253 self.indent()
254 if "properties" in schema:
Anthonios Parthenioub1b0c832020-12-14 14:24:19 -0500255 properties = schema.get("properties", {})
256 sorted_properties = OrderedDict(sorted(properties.items()))
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -0400257 for pname, pschema in sorted_properties.items():
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700258 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("", "")
Joe Gregorio2b781282011-12-08 12:00:25 -0500299
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700300 self.string = "".join(self.value)
301 return self.string
Joe Gregorio2b781282011-12-08 12:00:25 -0500302
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700303 def to_str(self, from_cache):
304 """Prototype object based on the schema, in Python code with comments.
Joe Gregorio2b781282011-12-08 12:00:25 -0500305
arfy slowyd35c9122021-07-15 00:16:31 +0700306 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.
Joe Gregorio2b781282011-12-08 12:00:25 -0500310
arfy slowyd35c9122021-07-15 00:16:31 +0700311 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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700315 self.from_cache = from_cache
316 return self._to_str_impl(self.schema)