model.py revision f2477e01787aa58f445919b809d89e252beef54f
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 os.path 6 7from json_parse import OrderedDict 8from memoize import memoize 9 10 11class ParseException(Exception): 12 """Thrown when data in the model is invalid. 13 """ 14 def __init__(self, parent, message): 15 hierarchy = _GetModelHierarchy(parent) 16 hierarchy.append(message) 17 Exception.__init__( 18 self, 'Model parse exception at:\n' + '\n'.join(hierarchy)) 19 20 21class Model(object): 22 """Model of all namespaces that comprise an API. 23 24 Properties: 25 - |namespaces| a map of a namespace name to its model.Namespace 26 """ 27 def __init__(self): 28 self.namespaces = {} 29 30 def AddNamespace(self, json, source_file, include_compiler_options=False): 31 """Add a namespace's json to the model and returns the namespace. 32 """ 33 namespace = Namespace(json, 34 source_file, 35 include_compiler_options=include_compiler_options) 36 self.namespaces[namespace.name] = namespace 37 return namespace 38 39 40def CreateFeature(name, model): 41 if isinstance(model, dict): 42 return SimpleFeature(name, model) 43 return ComplexFeature(name, [SimpleFeature(name, child) for child in model]) 44 45 46class ComplexFeature(object): 47 """A complex feature which may be made of several simple features. 48 49 Properties: 50 - |name| the name of the feature 51 - |unix_name| the unix_name of the feature 52 - |feature_list| a list of simple features which make up the feature 53 """ 54 def __init__(self, feature_name, features): 55 self.name = feature_name 56 self.unix_name = UnixName(self.name) 57 self.feature_list = features 58 59class SimpleFeature(object): 60 """A simple feature, which can make up a complex feature, as specified in 61 files such as chrome/common/extensions/api/_permission_features.json. 62 63 Properties: 64 - |name| the name of the feature 65 - |unix_name| the unix_name of the feature 66 - |channel| the channel where the feature is released 67 - |extension_types| the types which can use the feature 68 - |whitelist| a list of extensions allowed to use the feature 69 """ 70 def __init__(self, feature_name, feature_def): 71 self.name = feature_name 72 self.unix_name = UnixName(self.name) 73 self.channel = feature_def['channel'] 74 self.extension_types = feature_def['extension_types'] 75 self.whitelist = feature_def.get('whitelist') 76 77 78class Namespace(object): 79 """An API namespace. 80 81 Properties: 82 - |name| the name of the namespace 83 - |description| the description of the namespace 84 - |unix_name| the unix_name of the namespace 85 - |source_file| the file that contained the namespace definition 86 - |source_file_dir| the directory component of |source_file| 87 - |source_file_filename| the filename component of |source_file| 88 - |platforms| if not None, the list of platforms that the namespace is 89 available to 90 - |types| a map of type names to their model.Type 91 - |functions| a map of function names to their model.Function 92 - |events| a map of event names to their model.Function 93 - |properties| a map of property names to their model.Property 94 - |compiler_options| the compiler_options dict, only not empty if 95 |include_compiler_options| is True 96 """ 97 def __init__(self, json, source_file, include_compiler_options=False): 98 self.name = json['namespace'] 99 if 'description' not in json: 100 # TODO(kalman): Go back to throwing an error here. 101 print('%s must have a "description" field. This will appear ' 102 'on the API summary page.' % self.name) 103 json['description'] = '' 104 self.description = json['description'] 105 self.unix_name = UnixName(self.name) 106 self.source_file = source_file 107 self.source_file_dir, self.source_file_filename = os.path.split(source_file) 108 self.parent = None 109 self.platforms = _GetPlatforms(json) 110 toplevel_origin = Origin(from_client=True, from_json=True) 111 self.types = _GetTypes(self, json, self, toplevel_origin) 112 self.functions = _GetFunctions(self, json, self) 113 self.events = _GetEvents(self, json, self) 114 self.properties = _GetProperties(self, json, self, toplevel_origin) 115 if include_compiler_options: 116 self.compiler_options = json.get('compiler_options', {}) 117 else: 118 self.compiler_options = {} 119 self.documentation_options = json.get('documentation_options', {}) 120 121 122class Origin(object): 123 """Stores the possible origin of model object as a pair of bools. These are: 124 125 |from_client| indicating that instances can originate from users of 126 generated code (for example, function results), or 127 |from_json| indicating that instances can originate from the JSON (for 128 example, function parameters) 129 130 It is possible for model objects to originate from both the client and json, 131 for example Types defined in the top-level schema, in which case both 132 |from_client| and |from_json| would be True. 133 """ 134 def __init__(self, from_client=False, from_json=False): 135 if not from_client and not from_json: 136 raise ValueError('One of from_client or from_json must be true') 137 self.from_client = from_client 138 self.from_json = from_json 139 140 141class Type(object): 142 """A Type defined in the json. 143 144 Properties: 145 - |name| the type name 146 - |namespace| the Type's namespace 147 - |description| the description of the type (if provided) 148 - |properties| a map of property unix_names to their model.Property 149 - |functions| a map of function names to their model.Function 150 - |events| a map of event names to their model.Event 151 - |origin| the Origin of the type 152 - |property_type| the PropertyType of this Type 153 - |item_type| if this is an array, the type of items in the array 154 - |simple_name| the name of this Type without a namespace 155 - |additional_properties| the type of the additional properties, if any is 156 specified 157 """ 158 def __init__(self, 159 parent, 160 name, 161 json, 162 namespace, 163 origin): 164 self.name = name 165 self.namespace = namespace 166 self.simple_name = _StripNamespace(self.name, namespace) 167 self.unix_name = UnixName(self.name) 168 self.description = json.get('description', None) 169 self.origin = origin 170 self.parent = parent 171 self.instance_of = json.get('isInstanceOf', None) 172 173 # TODO(kalman): Only objects need functions/events/properties, but callers 174 # assume that all types have them. Fix this. 175 self.functions = _GetFunctions(self, json, namespace) 176 self.events = _GetEvents(self, json, namespace) 177 self.properties = _GetProperties(self, json, namespace, origin) 178 179 json_type = json.get('type', None) 180 if json_type == 'array': 181 self.property_type = PropertyType.ARRAY 182 self.item_type = Type( 183 self, '%sType' % name, json['items'], namespace, origin) 184 elif '$ref' in json: 185 self.property_type = PropertyType.REF 186 self.ref_type = json['$ref'] 187 elif 'enum' in json and json_type == 'string': 188 self.property_type = PropertyType.ENUM 189 self.enum_values = [EnumValue(value) for value in json['enum']] 190 elif json_type == 'any': 191 self.property_type = PropertyType.ANY 192 elif json_type == 'binary': 193 self.property_type = PropertyType.BINARY 194 elif json_type == 'boolean': 195 self.property_type = PropertyType.BOOLEAN 196 elif json_type == 'integer': 197 self.property_type = PropertyType.INTEGER 198 elif (json_type == 'double' or 199 json_type == 'number'): 200 self.property_type = PropertyType.DOUBLE 201 elif json_type == 'string': 202 self.property_type = PropertyType.STRING 203 elif 'choices' in json: 204 self.property_type = PropertyType.CHOICES 205 def generate_type_name(type_json): 206 if 'items' in type_json: 207 return '%ss' % generate_type_name(type_json['items']) 208 if '$ref' in type_json: 209 return type_json['$ref'] 210 if 'type' in type_json: 211 return type_json['type'] 212 return None 213 self.choices = [ 214 Type(self, 215 generate_type_name(choice) or 'choice%s' % i, 216 choice, 217 namespace, 218 origin) 219 for i, choice in enumerate(json['choices'])] 220 elif json_type == 'object': 221 if not ( 222 'isInstanceOf' in json or 223 'properties' in json or 224 'additionalProperties' in json or 225 'functions' in json or 226 'events' in json): 227 raise ParseException(self, name + " has no properties or functions") 228 self.property_type = PropertyType.OBJECT 229 additional_properties_json = json.get('additionalProperties', None) 230 if additional_properties_json is not None: 231 self.additional_properties = Type(self, 232 'additionalProperties', 233 additional_properties_json, 234 namespace, 235 origin) 236 else: 237 self.additional_properties = None 238 elif json_type == 'function': 239 self.property_type = PropertyType.FUNCTION 240 # Sometimes we might have an unnamed function, e.g. if it's a property 241 # of an object. Use the name of the property in that case. 242 function_name = json.get('name', name) 243 self.function = Function(self, function_name, json, namespace, origin) 244 else: 245 raise ParseException(self, 'Unsupported JSON type %s' % json_type) 246 247 248class Function(object): 249 """A Function defined in the API. 250 251 Properties: 252 - |name| the function name 253 - |platforms| if not None, the list of platforms that the function is 254 available to 255 - |params| a list of parameters to the function (order matters). A separate 256 parameter is used for each choice of a 'choices' parameter 257 - |deprecated| a reason and possible alternative for a deprecated function 258 - |description| a description of the function (if provided) 259 - |callback| the callback parameter to the function. There should be exactly 260 one 261 - |optional| whether the Function is "optional"; this only makes sense to be 262 present when the Function is representing a callback property 263 - |simple_name| the name of this Function without a namespace 264 - |returns| the return type of the function; None if the function does not 265 return a value 266 """ 267 def __init__(self, 268 parent, 269 name, 270 json, 271 namespace, 272 origin): 273 self.name = name 274 self.simple_name = _StripNamespace(self.name, namespace) 275 self.platforms = _GetPlatforms(json) 276 self.params = [] 277 self.description = json.get('description') 278 self.deprecated = json.get('deprecated') 279 self.callback = None 280 self.optional = json.get('optional', False) 281 self.parent = parent 282 self.nocompile = json.get('nocompile') 283 options = json.get('options', {}) 284 self.conditions = options.get('conditions', []) 285 self.actions = options.get('actions', []) 286 self.supports_listeners = options.get('supportsListeners', True) 287 self.supports_rules = options.get('supportsRules', False) 288 self.supports_dom = options.get('supportsDom', False) 289 290 def GeneratePropertyFromParam(p): 291 return Property(self, p['name'], p, namespace, origin) 292 293 self.filters = [GeneratePropertyFromParam(filter) 294 for filter in json.get('filters', [])] 295 callback_param = None 296 for param in json.get('parameters', []): 297 if param.get('type') == 'function': 298 if callback_param: 299 # No ParseException because the webstore has this. 300 # Instead, pretend all intermediate callbacks are properties. 301 self.params.append(GeneratePropertyFromParam(callback_param)) 302 callback_param = param 303 else: 304 self.params.append(GeneratePropertyFromParam(param)) 305 306 if callback_param: 307 self.callback = Function(self, 308 callback_param['name'], 309 callback_param, 310 namespace, 311 Origin(from_client=True)) 312 313 self.returns = None 314 if 'returns' in json: 315 self.returns = Type(self, 316 '%sReturnType' % name, 317 json['returns'], 318 namespace, 319 origin) 320 321 322class Property(object): 323 """A property of a type OR a parameter to a function. 324 Properties: 325 - |name| name of the property as in the json. This shouldn't change since 326 it is the key used to access DictionaryValues 327 - |unix_name| the unix_style_name of the property. Used as variable name 328 - |optional| a boolean representing whether the property is optional 329 - |description| a description of the property (if provided) 330 - |type_| the model.Type of this property 331 - |simple_name| the name of this Property without a namespace 332 - |deprecated| a reason and possible alternative for a deprecated property 333 """ 334 def __init__(self, parent, name, json, namespace, origin): 335 """Creates a Property from JSON. 336 """ 337 self.parent = parent 338 self.name = name 339 self._unix_name = UnixName(self.name) 340 self._unix_name_used = False 341 self.origin = origin 342 self.simple_name = _StripNamespace(self.name, namespace) 343 self.description = json.get('description', None) 344 self.optional = json.get('optional', None) 345 self.instance_of = json.get('isInstanceOf', None) 346 self.deprecated = json.get('deprecated') 347 348 # HACK: only support very specific value types. 349 is_allowed_value = ( 350 '$ref' not in json and 351 ('type' not in json or json['type'] == 'integer' 352 or json['type'] == 'string')) 353 354 self.value = None 355 if 'value' in json and is_allowed_value: 356 self.value = json['value'] 357 if 'type' not in json: 358 # Sometimes the type of the value is left out, and we need to figure 359 # it out for ourselves. 360 if isinstance(self.value, int): 361 json['type'] = 'integer' 362 elif isinstance(self.value, basestring): 363 json['type'] = 'string' 364 else: 365 # TODO(kalman): support more types as necessary. 366 raise ParseException( 367 parent, 368 '"%s" is not a supported type for "value"' % type(self.value)) 369 370 self.type_ = Type(parent, name, json, namespace, origin) 371 372 def GetUnixName(self): 373 """Gets the property's unix_name. Raises AttributeError if not set. 374 """ 375 if not self._unix_name: 376 raise AttributeError('No unix_name set on %s' % self.name) 377 self._unix_name_used = True 378 return self._unix_name 379 380 def SetUnixName(self, unix_name): 381 """Set the property's unix_name. Raises AttributeError if the unix_name has 382 already been used (GetUnixName has been called). 383 """ 384 if unix_name == self._unix_name: 385 return 386 if self._unix_name_used: 387 raise AttributeError( 388 'Cannot set the unix_name on %s; ' 389 'it is already used elsewhere as %s' % 390 (self.name, self._unix_name)) 391 self._unix_name = unix_name 392 393 unix_name = property(GetUnixName, SetUnixName) 394 395class EnumValue(object): 396 """A single value from an enum. 397 Properties: 398 - |name| name of the property as in the json. 399 - |description| a description of the property (if provided) 400 """ 401 def __init__(self, json): 402 if isinstance(json, dict): 403 self.name = json['name'] 404 self.description = json.get('description') 405 else: 406 self.name = json 407 self.description = None 408 409class _Enum(object): 410 """Superclass for enum types with a "name" field, setting up repr/eq/ne. 411 Enums need to do this so that equality/non-equality work over pickling. 412 """ 413 @staticmethod 414 def GetAll(cls): 415 """Yields all _Enum objects declared in |cls|. 416 """ 417 for prop_key in dir(cls): 418 prop_value = getattr(cls, prop_key) 419 if isinstance(prop_value, _Enum): 420 yield prop_value 421 422 def __init__(self, name): 423 self.name = name 424 425 def __eq__(self, other): 426 return type(other) == type(self) and other.name == self.name 427 def __ne__(self, other): 428 return not (self == other) 429 430 def __repr__(self): 431 return self.name 432 433 def __str__(self): 434 return repr(self) 435 436 437class _PropertyTypeInfo(_Enum): 438 def __init__(self, is_fundamental, name): 439 _Enum.__init__(self, name) 440 self.is_fundamental = is_fundamental 441 442 443class PropertyType(object): 444 """Enum of different types of properties/parameters. 445 """ 446 ANY = _PropertyTypeInfo(False, "any") 447 ARRAY = _PropertyTypeInfo(False, "array") 448 BINARY = _PropertyTypeInfo(False, "binary") 449 BOOLEAN = _PropertyTypeInfo(True, "boolean") 450 CHOICES = _PropertyTypeInfo(False, "choices") 451 DOUBLE = _PropertyTypeInfo(True, "double") 452 ENUM = _PropertyTypeInfo(False, "enum") 453 FUNCTION = _PropertyTypeInfo(False, "function") 454 INT64 = _PropertyTypeInfo(True, "int64") 455 INTEGER = _PropertyTypeInfo(True, "integer") 456 OBJECT = _PropertyTypeInfo(False, "object") 457 REF = _PropertyTypeInfo(False, "ref") 458 STRING = _PropertyTypeInfo(True, "string") 459 460 461@memoize 462def UnixName(name): 463 '''Returns the unix_style name for a given lowerCamelCase string. 464 ''' 465 unix_name = [] 466 for i, c in enumerate(name): 467 if c.isupper() and i > 0 and name[i - 1] != '_': 468 # Replace lowerUpper with lower_Upper. 469 if name[i - 1].islower(): 470 unix_name.append('_') 471 # Replace ACMEWidgets with ACME_Widgets 472 elif i + 1 < len(name) and name[i + 1].islower(): 473 unix_name.append('_') 474 if c == '.': 475 # Replace hello.world with hello_world. 476 unix_name.append('_') 477 else: 478 # Everything is lowercase. 479 unix_name.append(c.lower()) 480 return ''.join(unix_name) 481 482 483def _StripNamespace(name, namespace): 484 if name.startswith(namespace.name + '.'): 485 return name[len(namespace.name + '.'):] 486 return name 487 488 489def _GetModelHierarchy(entity): 490 """Returns the hierarchy of the given model entity.""" 491 hierarchy = [] 492 while entity is not None: 493 hierarchy.append(getattr(entity, 'name', repr(entity))) 494 if isinstance(entity, Namespace): 495 hierarchy.insert(0, ' in %s' % entity.source_file) 496 entity = getattr(entity, 'parent', None) 497 hierarchy.reverse() 498 return hierarchy 499 500 501def _GetTypes(parent, json, namespace, origin): 502 """Creates Type objects extracted from |json|. 503 """ 504 types = OrderedDict() 505 for type_json in json.get('types', []): 506 type_ = Type(parent, type_json['id'], type_json, namespace, origin) 507 types[type_.name] = type_ 508 return types 509 510 511def _GetFunctions(parent, json, namespace): 512 """Creates Function objects extracted from |json|. 513 """ 514 functions = OrderedDict() 515 for function_json in json.get('functions', []): 516 function = Function(parent, 517 function_json['name'], 518 function_json, 519 namespace, 520 Origin(from_json=True)) 521 functions[function.name] = function 522 return functions 523 524 525def _GetEvents(parent, json, namespace): 526 """Creates Function objects generated from the events in |json|. 527 """ 528 events = OrderedDict() 529 for event_json in json.get('events', []): 530 event = Function(parent, 531 event_json['name'], 532 event_json, 533 namespace, 534 Origin(from_client=True)) 535 events[event.name] = event 536 return events 537 538 539def _GetProperties(parent, json, namespace, origin): 540 """Generates Property objects extracted from |json|. 541 """ 542 properties = OrderedDict() 543 for name, property_json in json.get('properties', {}).items(): 544 properties[name] = Property(parent, name, property_json, namespace, origin) 545 return properties 546 547 548class _PlatformInfo(_Enum): 549 def __init__(self, name): 550 _Enum.__init__(self, name) 551 552 553class Platforms(object): 554 """Enum of the possible platforms. 555 """ 556 CHROMEOS = _PlatformInfo("chromeos") 557 CHROMEOS_TOUCH = _PlatformInfo("chromeos_touch") 558 LINUX = _PlatformInfo("linux") 559 MAC = _PlatformInfo("mac") 560 WIN = _PlatformInfo("win") 561 562 563def _GetPlatforms(json): 564 if 'platforms' not in json or json['platforms'] == None: 565 return None 566 # Sanity check: platforms should not be an empty list. 567 if not json['platforms']: 568 raise ValueError('"platforms" cannot be an empty list') 569 platforms = [] 570 for platform_name in json['platforms']: 571 for platform_enum in _Enum.GetAll(Platforms): 572 if platform_name == platform_enum.name: 573 platforms.append(platform_enum) 574 break 575 return platforms 576