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