model.py revision 5821806d5e7f356e8fa4b058a389a808ea183019
1# Copyright (c) 2012 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import copy 6import os.path 7import re 8 9class ParseException(Exception): 10 """Thrown when data in the model is invalid. 11 """ 12 def __init__(self, parent, message): 13 hierarchy = _GetModelHierarchy(parent) 14 hierarchy.append(message) 15 Exception.__init__( 16 self, 'Model parse exception at:\n' + '\n'.join(hierarchy)) 17 18class Model(object): 19 """Model of all namespaces that comprise an API. 20 21 Properties: 22 - |namespaces| a map of a namespace name to its model.Namespace 23 """ 24 def __init__(self): 25 self.namespaces = {} 26 27 def AddNamespace(self, json, source_file): 28 """Add a namespace's json to the model and returns the namespace. 29 """ 30 namespace = Namespace(json, source_file) 31 self.namespaces[namespace.name] = namespace 32 return namespace 33 34class Namespace(object): 35 """An API namespace. 36 37 Properties: 38 - |name| the name of the namespace 39 - |unix_name| the unix_name of the namespace 40 - |source_file| the file that contained the namespace definition 41 - |source_file_dir| the directory component of |source_file| 42 - |source_file_filename| the filename component of |source_file| 43 - |types| a map of type names to their model.Type 44 - |functions| a map of function names to their model.Function 45 - |events| a map of event names to their model.Function 46 - |properties| a map of property names to their model.Property 47 """ 48 def __init__(self, json, source_file): 49 self.name = json['namespace'] 50 self.unix_name = UnixName(self.name) 51 self.source_file = source_file 52 self.source_file_dir, self.source_file_filename = os.path.split(source_file) 53 self.parent = None 54 _AddTypes(self, json, self) 55 _AddFunctions(self, json, self) 56 _AddEvents(self, json, self) 57 _AddProperties(self, json, self) 58 59class Type(object): 60 """A Type defined in the json. 61 62 Properties: 63 - |name| the type name 64 - |description| the description of the type (if provided) 65 - |properties| a map of property unix_names to their model.Property 66 - |functions| a map of function names to their model.Function 67 - |events| a map of event names to their model.Event 68 - |from_client| indicates that instances of the Type can originate from the 69 users of generated code, such as top-level types and function results 70 - |from_json| indicates that instances of the Type can originate from the 71 JSON (as described by the schema), such as top-level types and function 72 parameters 73 - |type_| the PropertyType of this Type 74 - |item_type| if this is an array, the type of items in the array 75 - |simple_name| the name of this Type without a namespace 76 """ 77 def __init__(self, parent, name, json, namespace): 78 if json.get('type') == 'array': 79 self.type_ = PropertyType.ARRAY 80 self.item_type = Property(self, 81 name + "Element", 82 json['items'], 83 namespace, 84 from_json=True, 85 from_client=True) 86 elif 'enum' in json: 87 self.enum_values = [] 88 for value in json['enum']: 89 self.enum_values.append(value) 90 self.type_ = PropertyType.ENUM 91 elif json.get('type') == 'string': 92 self.type_ = PropertyType.STRING 93 else: 94 if not ( 95 'properties' in json or 96 'additionalProperties' in json or 97 'functions' in json or 98 'events' in json): 99 raise ParseException(self, name + " has no properties or functions") 100 self.type_ = PropertyType.OBJECT 101 self.name = name 102 self.simple_name = _StripNamespace(self.name, namespace) 103 self.unix_name = UnixName(self.name) 104 self.description = json.get('description') 105 self.from_json = True 106 self.from_client = True 107 self.parent = parent 108 self.instance_of = json.get('isInstanceOf', None) 109 _AddFunctions(self, json, namespace) 110 _AddEvents(self, json, namespace) 111 _AddProperties(self, json, namespace, from_json=True, from_client=True) 112 113 additional_properties_key = 'additionalProperties' 114 additional_properties = json.get(additional_properties_key) 115 if additional_properties: 116 self.properties[additional_properties_key] = Property( 117 self, 118 additional_properties_key, 119 additional_properties, 120 namespace, 121 is_additional_properties=True) 122 123class Function(object): 124 """A Function defined in the API. 125 126 Properties: 127 - |name| the function name 128 - |params| a list of parameters to the function (order matters). A separate 129 parameter is used for each choice of a 'choices' parameter. 130 - |description| a description of the function (if provided) 131 - |callback| the callback parameter to the function. There should be exactly 132 one 133 - |optional| whether the Function is "optional"; this only makes sense to be 134 present when the Function is representing a callback property. 135 - |simple_name| the name of this Function without a namespace 136 """ 137 def __init__(self, 138 parent, 139 json, 140 namespace, 141 from_json=False, 142 from_client=False): 143 self.name = json['name'] 144 self.simple_name = _StripNamespace(self.name, namespace) 145 self.params = [] 146 self.description = json.get('description') 147 self.callback = None 148 self.optional = json.get('optional', False) 149 self.parent = parent 150 self.nocompile = json.get('nocompile') 151 options = json.get('options', {}) 152 self.conditions = options.get('conditions', []) 153 self.actions = options.get('actions', []) 154 self.supports_listeners = options.get('supportsListeners', True) 155 self.supports_rules = options.get('supportsRules', False) 156 def GeneratePropertyFromParam(p): 157 return Property(self, 158 p['name'], p, 159 namespace, 160 from_json=from_json, 161 from_client=from_client) 162 163 self.filters = [GeneratePropertyFromParam(filter) 164 for filter in json.get('filters', [])] 165 callback_param = None 166 for param in json.get('parameters', []): 167 168 if param.get('type') == 'function': 169 if callback_param: 170 # No ParseException because the webstore has this. 171 # Instead, pretend all intermediate callbacks are properties. 172 self.params.append(GeneratePropertyFromParam(callback_param)) 173 callback_param = param 174 else: 175 self.params.append(GeneratePropertyFromParam(param)) 176 177 if callback_param: 178 self.callback = Function(self, 179 callback_param, 180 namespace, 181 from_client=True) 182 183 self.returns = None 184 if 'returns' in json: 185 self.returns = Property(self, 'return', json['returns'], namespace) 186 187class Property(object): 188 """A property of a type OR a parameter to a function. 189 190 Properties: 191 - |name| name of the property as in the json. This shouldn't change since 192 it is the key used to access DictionaryValues 193 - |unix_name| the unix_style_name of the property. Used as variable name 194 - |optional| a boolean representing whether the property is optional 195 - |description| a description of the property (if provided) 196 - |type_| the model.PropertyType of this property 197 - |compiled_type| the model.PropertyType that this property should be 198 compiled to from the JSON. Defaults to |type_|. 199 - |ref_type| the type that the REF property is referencing. Can be used to 200 map to its model.Type 201 - |item_type| a model.Property representing the type of each element in an 202 ARRAY 203 - |properties| the properties of an OBJECT parameter 204 - |from_client| indicates that instances of the Type can originate from the 205 users of generated code, such as top-level types and function results 206 - |from_json| indicates that instances of the Type can originate from the 207 JSON (as described by the schema), such as top-level types and function 208 parameters 209 - |simple_name| the name of this Property without a namespace 210 """ 211 212 def __init__(self, 213 parent, 214 name, 215 json, 216 namespace, 217 is_additional_properties=False, 218 from_json=False, 219 from_client=False): 220 self.name = name 221 self.simple_name = _StripNamespace(self.name, namespace) 222 self._unix_name = UnixName(self.name) 223 self._unix_name_used = False 224 self.optional = json.get('optional', False) 225 self.functions = {} 226 self.has_value = False 227 self.description = json.get('description') 228 self.parent = parent 229 self.from_json = from_json 230 self.from_client = from_client 231 self.instance_of = json.get('isInstanceOf', None) 232 self.params = [] 233 self.returns = None 234 _AddProperties(self, json, namespace) 235 if is_additional_properties: 236 self.type_ = PropertyType.ADDITIONAL_PROPERTIES 237 elif '$ref' in json: 238 self.ref_type = json['$ref'] 239 self.type_ = PropertyType.REF 240 elif 'enum' in json and json.get('type') == 'string': 241 # Non-string enums (as in the case of [legalValues=(1,2)]) should fall 242 # through to the next elif. 243 self.enum_values = [] 244 for value in json['enum']: 245 self.enum_values.append(value) 246 self.type_ = PropertyType.ENUM 247 elif 'type' in json: 248 self.type_ = self._JsonTypeToPropertyType(json['type']) 249 if self.type_ == PropertyType.ARRAY: 250 self.item_type = Property(self, 251 name + "Element", 252 json['items'], 253 namespace, 254 from_json=from_json, 255 from_client=from_client) 256 elif self.type_ == PropertyType.OBJECT: 257 # These members are read when this OBJECT Property is used as a Type 258 type_ = Type(self, self.name, json, namespace) 259 # self.properties will already have some value from |_AddProperties|. 260 self.properties.update(type_.properties) 261 self.functions = type_.functions 262 elif self.type_ == PropertyType.FUNCTION: 263 for p in json.get('parameters', []): 264 self.params.append(Property(self, 265 p['name'], 266 p, 267 namespace, 268 from_json=from_json, 269 from_client=from_client)) 270 if 'returns' in json: 271 self.returns = Property(self, 'return', json['returns'], namespace) 272 elif 'choices' in json: 273 if not json['choices'] or len(json['choices']) == 0: 274 raise ParseException(self, 'Choices has no choices') 275 self.choices = {} 276 self.type_ = PropertyType.CHOICES 277 self.compiled_type = self.type_ 278 for choice_json in json['choices']: 279 choice = Property(self, 280 self.name, 281 choice_json, 282 namespace, 283 from_json=from_json, 284 from_client=from_client) 285 choice.unix_name = UnixName(self.name + choice.type_.name) 286 # The existence of any single choice is optional 287 choice.optional = True 288 self.choices[choice.type_] = choice 289 elif 'value' in json: 290 self.has_value = True 291 self.value = json['value'] 292 if type(self.value) == int: 293 self.type_ = PropertyType.INTEGER 294 self.compiled_type = self.type_ 295 else: 296 # TODO(kalman): support more types as necessary. 297 raise ParseException( 298 self, '"%s" is not a supported type' % type(self.value)) 299 else: 300 raise ParseException( 301 self, 'Property has no type, $ref, choices, or value') 302 if 'compiled_type' in json: 303 if 'type' in json: 304 self.compiled_type = self._JsonTypeToPropertyType(json['compiled_type']) 305 else: 306 raise ParseException(self, 'Property has compiled_type but no type') 307 else: 308 self.compiled_type = self.type_ 309 310 def _JsonTypeToPropertyType(self, json_type): 311 try: 312 return { 313 'any': PropertyType.ANY, 314 'array': PropertyType.ARRAY, 315 'binary': PropertyType.BINARY, 316 'boolean': PropertyType.BOOLEAN, 317 'integer': PropertyType.INTEGER, 318 'int64': PropertyType.INT64, 319 'function': PropertyType.FUNCTION, 320 'number': PropertyType.DOUBLE, 321 'object': PropertyType.OBJECT, 322 'string': PropertyType.STRING, 323 }[json_type] 324 except KeyError: 325 raise NotImplementedError('Type %s not recognized' % json_type) 326 327 def GetUnixName(self): 328 """Gets the property's unix_name. Raises AttributeError if not set. 329 """ 330 if not self._unix_name: 331 raise AttributeError('No unix_name set on %s' % self.name) 332 self._unix_name_used = True 333 return self._unix_name 334 335 def SetUnixName(self, unix_name): 336 """Set the property's unix_name. Raises AttributeError if the unix_name has 337 already been used (GetUnixName has been called). 338 """ 339 if unix_name == self._unix_name: 340 return 341 if self._unix_name_used: 342 raise AttributeError( 343 'Cannot set the unix_name on %s; ' 344 'it is already used elsewhere as %s' % 345 (self.name, self._unix_name)) 346 self._unix_name = unix_name 347 348 def Copy(self): 349 """Makes a copy of this model.Property object and allow the unix_name to be 350 set again. 351 """ 352 property_copy = copy.copy(self) 353 property_copy._unix_name_used = False 354 return property_copy 355 356 unix_name = property(GetUnixName, SetUnixName) 357 358class _PropertyTypeInfo(object): 359 """This class is not an inner class of |PropertyType| so it can be pickled. 360 """ 361 def __init__(self, is_fundamental, name): 362 self.is_fundamental = is_fundamental 363 self.name = name 364 365 def __repr__(self): 366 return self.name 367 368 def __eq__(self, other): 369 return isinstance(other, _PropertyTypeInfo) and self.name == other.name 370 371 def __ne__(self, other): 372 # Yes. You seriously do need this. 373 return not (self == other) 374 375class PropertyType(object): 376 """Enum of different types of properties/parameters. 377 """ 378 INTEGER = _PropertyTypeInfo(True, "INTEGER") 379 INT64 = _PropertyTypeInfo(True, "INT64") 380 DOUBLE = _PropertyTypeInfo(True, "DOUBLE") 381 BOOLEAN = _PropertyTypeInfo(True, "BOOLEAN") 382 STRING = _PropertyTypeInfo(True, "STRING") 383 ENUM = _PropertyTypeInfo(False, "ENUM") 384 ARRAY = _PropertyTypeInfo(False, "ARRAY") 385 REF = _PropertyTypeInfo(False, "REF") 386 CHOICES = _PropertyTypeInfo(False, "CHOICES") 387 OBJECT = _PropertyTypeInfo(False, "OBJECT") 388 FUNCTION = _PropertyTypeInfo(False, "FUNCTION") 389 BINARY = _PropertyTypeInfo(False, "BINARY") 390 ANY = _PropertyTypeInfo(False, "ANY") 391 ADDITIONAL_PROPERTIES = _PropertyTypeInfo(False, "ADDITIONAL_PROPERTIES") 392 393def UnixName(name): 394 """Returns the unix_style name for a given lowerCamelCase string. 395 """ 396 # First replace any lowerUpper patterns with lower_Upper. 397 s1 = re.sub('([a-z])([A-Z])', r'\1_\2', name) 398 # Now replace any ACMEWidgets patterns with ACME_Widgets 399 s2 = re.sub('([A-Z]+)([A-Z][a-z])', r'\1_\2', s1) 400 # Finally, replace any remaining periods, and make lowercase. 401 return s2.replace('.', '_').lower() 402 403def _StripNamespace(name, namespace): 404 if name.startswith(namespace.name + '.'): 405 return name[len(namespace.name + '.'):] 406 return name 407 408def _GetModelHierarchy(entity): 409 """Returns the hierarchy of the given model entity.""" 410 hierarchy = [] 411 while entity: 412 try: 413 hierarchy.append(entity.name) 414 except AttributeError: 415 hierarchy.append(repr(entity)) 416 entity = entity.parent 417 hierarchy.reverse() 418 return hierarchy 419 420def _AddTypes(model, json, namespace): 421 """Adds Type objects to |model| contained in the 'types' field of |json|. 422 """ 423 model.types = {} 424 for type_json in json.get('types', []): 425 type_ = Type(model, type_json['id'], type_json, namespace) 426 model.types[type_.name] = type_ 427 428def _AddFunctions(model, json, namespace): 429 """Adds Function objects to |model| contained in the 'functions' field of 430 |json|. 431 """ 432 model.functions = {} 433 for function_json in json.get('functions', []): 434 function = Function(model, function_json, namespace, from_json=True) 435 model.functions[function.name] = function 436 437def _AddEvents(model, json, namespace): 438 """Adds Function objects to |model| contained in the 'events' field of |json|. 439 """ 440 model.events = {} 441 for event_json in json.get('events', []): 442 event = Function(model, event_json, namespace, from_client=True) 443 model.events[event.name] = event 444 445def _AddProperties(model, 446 json, 447 namespace, 448 from_json=False, 449 from_client=False): 450 """Adds model.Property objects to |model| contained in the 'properties' field 451 of |json|. 452 """ 453 model.properties = {} 454 for name, property_json in json.get('properties', {}).items(): 455 model.properties[name] = Property( 456 model, 457 name, 458 property_json, 459 namespace, 460 from_json=from_json, 461 from_client=from_client) 462