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