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