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 60import six 61 62# TODO(jcgregorio) support format, enum, minimum, maximum 63 64__author__ = '[email protected] (Joe Gregorio)' 65 66import copy 67 68# Oauth2client < 3 has the positional helper in 'util', >= 3 has it 69# in '_helpers'. 70try: 71 from oauth2client import util 72except ImportError: 73 from oauth2client import _helpers as util 74 75 76class Schemas(object): 77 """Schemas for an API.""" 78 79 def __init__(self, discovery): 80 """Constructor. 81 82 Args: 83 discovery: object, Deserialized discovery document from which we pull 84 out the named schema. 85 """ 86 self.schemas = discovery.get('schemas', {}) 87 88 # Cache of pretty printed schemas. 89 self.pretty = {} 90 91 @util.positional(2) 92 def _prettyPrintByName(self, name, seen=None, dent=0): 93 """Get pretty printed object prototype from the schema name. 94 95 Args: 96 name: string, Name of schema in the discovery document. 97 seen: list of string, Names of schema already seen. Used to handle 98 recursive definitions. 99 100 Returns: 101 string, A string that contains a prototype object with 102 comments that conforms to the given schema. 103 """ 104 if seen is None: 105 seen = [] 106 107 if name in seen: 108 # Do not fall into an infinite loop over recursive definitions. 109 return '# Object with schema name: %s' % name 110 seen.append(name) 111 112 if name not in self.pretty: 113 self.pretty[name] = _SchemaToStruct(self.schemas[name], 114 seen, dent=dent).to_str(self._prettyPrintByName) 115 116 seen.pop() 117 118 return self.pretty[name] 119 120 def prettyPrintByName(self, name): 121 """Get pretty printed object prototype from the schema name. 122 123 Args: 124 name: string, Name of schema in the discovery document. 125 126 Returns: 127 string, A string that contains a prototype object with 128 comments that conforms to the given schema. 129 """ 130 # Return with trailing comma and newline removed. 131 return self._prettyPrintByName(name, seen=[], dent=1)[:-2] 132 133 @util.positional(2) 134 def _prettyPrintSchema(self, schema, seen=None, dent=0): 135 """Get pretty printed object prototype of schema. 136 137 Args: 138 schema: object, Parsed JSON schema. 139 seen: list of string, Names of schema already seen. Used to handle 140 recursive definitions. 141 142 Returns: 143 string, A string that contains a prototype object with 144 comments that conforms to the given schema. 145 """ 146 if seen is None: 147 seen = [] 148 149 return _SchemaToStruct(schema, seen, dent=dent).to_str(self._prettyPrintByName) 150 151 def prettyPrintSchema(self, schema): 152 """Get pretty printed object prototype of schema. 153 154 Args: 155 schema: object, Parsed JSON schema. 156 157 Returns: 158 string, A string that contains a prototype object with 159 comments that conforms to the given schema. 160 """ 161 # Return with trailing comma and newline removed. 162 return self._prettyPrintSchema(schema, dent=1)[:-2] 163 164 def get(self, name): 165 """Get deserialized JSON schema from the schema name. 166 167 Args: 168 name: string, Schema name. 169 """ 170 return self.schemas[name] 171 172 173class _SchemaToStruct(object): 174 """Convert schema to a prototype object.""" 175 176 @util.positional(3) 177 def __init__(self, schema, seen, dent=0): 178 """Constructor. 179 180 Args: 181 schema: object, Parsed JSON schema. 182 seen: list, List of names of schema already seen while parsing. Used to 183 handle recursive definitions. 184 dent: int, Initial indentation depth. 185 """ 186 # The result of this parsing kept as list of strings. 187 self.value = [] 188 189 # The final value of the parsing. 190 self.string = None 191 192 # The parsed JSON schema. 193 self.schema = schema 194 195 # Indentation level. 196 self.dent = dent 197 198 # Method that when called returns a prototype object for the schema with 199 # the given name. 200 self.from_cache = None 201 202 # List of names of schema already seen while parsing. 203 self.seen = seen 204 205 def emit(self, text): 206 """Add text as a line to the output. 207 208 Args: 209 text: string, Text to output. 210 """ 211 self.value.extend([" " * self.dent, text, '\n']) 212 213 def emitBegin(self, text): 214 """Add text to the output, but with no line terminator. 215 216 Args: 217 text: string, Text to output. 218 """ 219 self.value.extend([" " * self.dent, text]) 220 221 def emitEnd(self, text, comment): 222 """Add text and comment to the output with line terminator. 223 224 Args: 225 text: string, Text to output. 226 comment: string, Python comment. 227 """ 228 if comment: 229 divider = '\n' + ' ' * (self.dent + 2) + '# ' 230 lines = comment.splitlines() 231 lines = [x.rstrip() for x in lines] 232 comment = divider.join(lines) 233 self.value.extend([text, ' # ', comment, '\n']) 234 else: 235 self.value.extend([text, '\n']) 236 237 def indent(self): 238 """Increase indentation level.""" 239 self.dent += 1 240 241 def undent(self): 242 """Decrease indentation level.""" 243 self.dent -= 1 244 245 def _to_str_impl(self, schema): 246 """Prototype object based on the schema, in Python code with comments. 247 248 Args: 249 schema: object, Parsed JSON schema file. 250 251 Returns: 252 Prototype object based on the schema, in Python code with comments. 253 """ 254 stype = schema.get('type') 255 if stype == 'object': 256 self.emitEnd('{', schema.get('description', '')) 257 self.indent() 258 if 'properties' in schema: 259 for pname, pschema in six.iteritems(schema.get('properties', {})): 260 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('', '') 301 302 self.string = ''.join(self.value) 303 return self.string 304 305 def to_str(self, from_cache): 306 """Prototype object based on the schema, in Python code with comments. 307 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 """ 317 self.from_cache = from_cache 318 return self._to_str_impl(self.schema) 319