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