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.
7 from json_parse
import OrderedDict
8 from memoize
import memoize
11 class ParseException(Exception):
12 """Thrown when data in the model is invalid.
14 def __init__(self
, parent
, message
):
15 hierarchy
= _GetModelHierarchy(parent
)
16 hierarchy
.append(message
)
18 self
, 'Model parse exception at:\n' + '\n'.join(hierarchy
))
22 """Model of all namespaces that comprise an API.
25 - |namespaces| a map of a namespace name to its model.Namespace
30 def AddNamespace(self
,
33 include_compiler_options
=False,
35 """Add a namespace's json to the model and returns the namespace.
37 namespace
= Namespace(json
,
39 include_compiler_options
=include_compiler_options
,
40 environment
=environment
)
41 self
.namespaces
[namespace
.name
] = namespace
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
])
51 class ComplexFeature(object):
52 """A complex feature which may be made of several simple features.
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
59 def __init__(self
, feature_name
, features
):
60 self
.name
= feature_name
61 self
.unix_name
= UnixName(self
.name
)
62 self
.feature_list
= features
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.
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
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')
83 class Namespace(object):
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
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
106 include_compiler_options
=False,
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]
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', {})
130 self
.compiler_options
= {}
131 self
.environment
= environment
132 self
.documentation_options
= json
.get('documentation_options', {})
135 class Origin(object):
136 """Stores the possible origin of model object as a pair of bools. These are:
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)
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.
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
155 """A Type defined in the json.
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
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)
184 self
.instance_of
= json
.get('isInstanceOf', None)
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
)
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
)
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']
229 generate_type_name(choice
) or 'choice%s' % i
,
233 for i
, choice
in enumerate(json
['choices'])]
234 elif json_type
== 'object':
236 'isInstanceOf' in json
or
237 'properties' in json
or
238 'additionalProperties' in json
or
239 'functions' in json
or
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
,
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
)
259 raise ParseException(self
, 'Unsupported JSON type %s' % json_type
)
262 class Function(object):
263 """A Function defined in the API.
266 - |name| the function name
267 - |platforms| if not None, the list of platforms that the function is
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
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
288 self
.simple_name
= _StripNamespace(self
.name
, namespace
)
289 self
.platforms
= _GetPlatforms(json
)
291 self
.description
= json
.get('description')
292 self
.deprecated
= json
.get('deprecated')
294 self
.optional
= json
.get('optional', False)
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)
304 def GeneratePropertyFromParam(p
):
305 return Property(self
, p
['name'], p
, namespace
, origin
)
307 self
.filters
= [GeneratePropertyFromParam(filter_instance
)
308 for filter_instance
in json
.get('filters', [])]
309 callback_param
= None
310 for param
in json
.get('parameters', []):
311 if param
.get('type') == 'function':
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
318 self
.params
.append(GeneratePropertyFromParam(param
))
321 self
.callback
= Function(self
,
322 callback_param
['name'],
325 Origin(from_client
=True))
328 if 'returns' in json
:
329 self
.returns
= Type(self
,
330 '%sReturnType' % name
,
336 class Property(object):
337 """A property of a type OR a parameter to a function.
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
348 def __init__(self
, parent
, name
, json
, namespace
, origin
):
349 """Creates a Property from JSON.
353 self
._unix
_name
= UnixName(self
.name
)
354 self
._unix
_name
_used
= False
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')
362 # HACK: only support very specific value types.
364 '$ref' not in json
and
365 ('type' not in json
or json
['type'] == 'integer'
366 or json
['type'] == 'string'))
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'
379 # TODO(kalman): support more types as necessary.
380 raise ParseException(
382 '"%s" is not a supported type for "value"' % type(self
.value
))
384 self
.type_
= Type(parent
, name
, json
, namespace
, origin
)
386 def GetUnixName(self
):
387 """Gets the property's unix_name. Raises AttributeError if not set.
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
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).
398 if unix_name
== self
._unix
_name
:
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
407 unix_name
= property(GetUnixName
, SetUnixName
)
409 class EnumValue(object):
410 """A single value from an enum.
412 - |name| name of the property as in the json.
413 - |description| a description of the property (if provided)
415 def __init__(self
, json
):
416 if isinstance(json
, dict):
417 self
.name
= json
['name']
418 self
.description
= json
.get('description')
421 self
.description
= None
424 return CamelName(self
.name
)
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.
432 """Yields all _Enum objects declared in |cls|.
434 for prop_key
in dir(cls
):
435 prop_value
= getattr(cls
, prop_key
)
436 if isinstance(prop_value
, _Enum
):
439 def __init__(self
, name
):
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
)
454 class _PropertyTypeInfo(_Enum
):
455 def __init__(self
, is_fundamental
, name
):
456 _Enum
.__init
__(self
, name
)
457 self
.is_fundamental
= is_fundamental
460 class PropertyType(object):
461 """Enum of different types of properties/parameters.
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")
480 '''Returns the unix_style name for a given lowerCamelCase string.
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('_')
492 # Replace hello.world with hello_world.
493 unix_name
.append('_')
495 # Everything is lowercase.
496 unix_name
.append(c
.lower())
497 return ''.join(unix_name
)
501 def CamelName(snake
):
502 ''' Converts a snake_cased_string to a camelCasedOne. '''
503 pieces
= snake
.split('_')
505 for i
, piece
in enumerate(pieces
):
509 camel
.append(piece
.capitalize())
510 return ''.join(camel
)
513 def _StripNamespace(name
, namespace
):
514 if name
.startswith(namespace
.name
+ '.'):
515 return name
[len(namespace
.name
+ '.'):]
519 def _GetModelHierarchy(entity
):
520 """Returns the hierarchy of the given model entity."""
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)
531 def _GetTypes(parent
, json
, namespace
, origin
):
532 """Creates Type objects extracted from |json|.
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_
541 def _GetFunctions(parent
, json
, namespace
):
542 """Creates Function objects extracted from |json|.
544 functions
= OrderedDict()
545 for function_json
in json
.get('functions', []):
546 function
= Function(parent
,
547 function_json
['name'],
550 Origin(from_json
=True))
551 functions
[function
.name
] = function
555 def _GetEvents(parent
, json
, namespace
):
556 """Creates Function objects generated from the events in |json|.
558 events
= OrderedDict()
559 for event_json
in json
.get('events', []):
560 event
= Function(parent
,
564 Origin(from_client
=True))
565 events
[event
.name
] = event
569 def _GetProperties(parent
, json
, namespace
, origin
):
570 """Generates Property objects extracted from |json|.
572 properties
= OrderedDict()
573 for name
, property_json
in json
.get('properties', {}).items():
574 properties
[name
] = Property(parent
, name
, property_json
, namespace
, origin
)
578 class _PlatformInfo(_Enum
):
579 def __init__(self
, name
):
580 _Enum
.__init
__(self
, name
)
583 class Platforms(object):
584 """Enum of the possible platforms.
586 CHROMEOS
= _PlatformInfo("chromeos")
587 CHROMEOS_TOUCH
= _PlatformInfo("chromeos_touch")
588 LINUX
= _PlatformInfo("linux")
589 MAC
= _PlatformInfo("mac")
590 WIN
= _PlatformInfo("win")
593 def _GetPlatforms(json
):
594 if 'platforms' not in json
or json
['platforms'] == 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')
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
)