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__ = 'jcgregorio@google.com (Joe Gregorio)' 65 66import copy 67 68from oauth2client import util 69 70 71class Schemas(object): 72 """Schemas for an API.""" 73 74 def __init__(self, discovery): 75 """Constructor. 76 77 Args: 78 discovery: object, Deserialized discovery document from which we pull 79 out the named schema. 80 """ 81 self.schemas = discovery.get('schemas', {}) 82 83 # Cache of pretty printed schemas. 84 self.pretty = {} 85 86 @util.positional(2) 87 def _prettyPrintByName(self, name, seen=None, dent=0): 88 """Get pretty printed object prototype from the schema name. 89 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 """ 99 if seen is None: 100 seen = [] 101 102 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) 106 107 if name not in self.pretty: 108 self.pretty[name] = _SchemaToStruct(self.schemas[name], 109 seen, dent=dent).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=1)[:-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=1)[:-2] 158 159 def get(self, name): 160 """Get deserialized JSON schema from the schema name. 161 162 Args: 163 name: string, Schema name. 164 """ 165 return self.schemas[name] 166 167 168class _SchemaToStruct(object): 169 """Convert schema to a prototype object.""" 170 171 @util.positional(3) 172 def __init__(self, schema, seen, dent=0): 173 """Constructor. 174 175 Args: 176 schema: object, Parsed JSON schema. 177 seen: list, List of names of schema already seen while parsing. Used to 178 handle recursive definitions. 179 dent: int, Initial indentation depth. 180 """ 181 # The result of this parsing kept as list of strings. 182 self.value = [] 183 184 # The final value of the parsing. 185 self.string = None 186 187 # The parsed JSON schema. 188 self.schema = schema 189 190 # Indentation level. 191 self.dent = dent 192 193 # Method that when called returns a prototype object for the schema with 194 # the given name. 195 self.from_cache = None 196 197 # List of names of schema already seen while parsing. 198 self.seen = seen 199 200 def emit(self, text): 201 """Add text as a line to the output. 202 203 Args: 204 text: string, Text to output. 205 """ 206 self.value.extend([" " * self.dent, text, '\n']) 207 208 def emitBegin(self, text): 209 """Add text to the output, but with no line terminator. 210 211 Args: 212 text: string, Text to output. 213 """ 214 self.value.extend([" " * self.dent, text]) 215 216 def emitEnd(self, text, comment): 217 """Add text and comment to the output with line terminator. 218 219 Args: 220 text: string, Text to output. 221 comment: string, Python comment. 222 """ 223 if comment: 224 divider = '\n' + ' ' * (self.dent + 2) + '# ' 225 lines = comment.splitlines() 226 lines = [x.rstrip() for x in lines] 227 comment = divider.join(lines) 228 self.value.extend([text, ' # ', comment, '\n']) 229 else: 230 self.value.extend([text, '\n']) 231 232 def indent(self): 233 """Increase indentation level.""" 234 self.dent += 1 235 236 def undent(self): 237 """Decrease indentation level.""" 238 self.dent -= 1 239 240 def _to_str_impl(self, schema): 241 """Prototype object based on the schema, in Python code with comments. 242 243 Args: 244 schema: object, Parsed JSON schema file. 245 246 Returns: 247 Prototype object based on the schema, in Python code with comments. 248 """ 249 stype = schema.get('type') 250 if stype == 'object': 251 self.emitEnd('{', schema.get('description', '')) 252 self.indent() 253 if 'properties' in schema: 254 for pname, pschema in six.iteritems(schema.get('properties', {})): 255 self.emitBegin('"%s": ' % pname) 256 self._to_str_impl(pschema) 257 elif 'additionalProperties' in schema: 258 self.emitBegin('"a_key": ') 259 self._to_str_impl(schema['additionalProperties']) 260 self.undent() 261 self.emit('},') 262 elif '$ref' in schema: 263 schemaName = schema['$ref'] 264 description = schema.get('description', '') 265 s = self.from_cache(schemaName, seen=self.seen) 266 parts = s.splitlines() 267 self.emitEnd(parts[0], description) 268 for line in parts[1:]: 269 self.emit(line.rstrip()) 270 elif stype == 'boolean': 271 value = schema.get('default', 'True or False') 272 self.emitEnd('%s,' % str(value), schema.get('description', '')) 273 elif stype == 'string': 274 value = schema.get('default', 'A String') 275 self.emitEnd('"%s",' % str(value), schema.get('description', '')) 276 elif stype == 'integer': 277 value = schema.get('default', '42') 278 self.emitEnd('%s,' % str(value), schema.get('description', '')) 279 elif stype == 'number': 280 value = schema.get('default', '3.14') 281 self.emitEnd('%s,' % str(value), schema.get('description', '')) 282 elif stype == 'null': 283 self.emitEnd('None,', schema.get('description', '')) 284 elif stype == 'any': 285 self.emitEnd('"",', schema.get('description', '')) 286 elif stype == 'array': 287 self.emitEnd('[', schema.get('description')) 288 self.indent() 289 self.emitBegin('') 290 self._to_str_impl(schema['items']) 291 self.undent() 292 self.emit('],') 293 else: 294 self.emit('Unknown type! %s' % stype) 295 self.emitEnd('', '') 296 297 self.string = ''.join(self.value) 298 return self.string 299 300 def to_str(self, from_cache): 301 """Prototype object based on the schema, in Python code with comments. 302 303 Args: 304 from_cache: callable(name, seen), Callable that retrieves an object 305 prototype for a schema with the given name. Seen is a list of schema 306 names already seen as we recursively descend the schema definition. 307 308 Returns: 309 Prototype object based on the schema, in Python code with comments. 310 The lines of the code will all be properly indented. 311 """ 312 self.from_cache = from_cache 313 return self._to_str_impl(self.schema) 314