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
27 def __init__(self
, allow_inline_enums
=True):
28 self
._allow
_inline
_enums
= allow_inline_enums
31 def AddNamespace(self
,
34 include_compiler_options
=False,
36 """Add a namespace's json to the model and returns the namespace.
38 namespace
= Namespace(json
,
40 include_compiler_options
=include_compiler_options
,
41 environment
=environment
,
42 allow_inline_enums
=self
._allow
_inline
_enums
)
43 self
.namespaces
[namespace
.name
] = namespace
47 def CreateFeature(name
, model
):
48 if isinstance(model
, dict):
49 return SimpleFeature(name
, model
)
50 return ComplexFeature(name
, [SimpleFeature(name
, child
) for child
in model
])
53 class ComplexFeature(object):
54 """A complex feature which may be made of several simple features.
57 - |name| the name of the feature
58 - |unix_name| the unix_name of the feature
59 - |feature_list| a list of simple features which make up the feature
61 def __init__(self
, feature_name
, features
):
62 self
.name
= feature_name
63 self
.unix_name
= UnixName(self
.name
)
64 self
.feature_list
= features
66 class SimpleFeature(object):
67 """A simple feature, which can make up a complex feature, as specified in
68 files such as chrome/common/extensions/api/_permission_features.json.
71 - |name| the name of the feature
72 - |unix_name| the unix_name of the feature
73 - |channel| the channel where the feature is released
74 - |extension_types| the types which can use the feature
75 - |whitelist| a list of extensions allowed to use the feature
77 def __init__(self
, feature_name
, feature_def
):
78 self
.name
= feature_name
79 self
.unix_name
= UnixName(self
.name
)
80 self
.channel
= feature_def
['channel']
81 self
.extension_types
= feature_def
['extension_types']
82 self
.whitelist
= feature_def
.get('whitelist')
85 class Namespace(object):
89 - |name| the name of the namespace
90 - |description| the description of the namespace
91 - |deprecated| a reason and possible alternative for a deprecated api
92 - |unix_name| the unix_name of the namespace
93 - |source_file| the file that contained the namespace definition
94 - |source_file_dir| the directory component of |source_file|
95 - |source_file_filename| the filename component of |source_file|
96 - |platforms| if not None, the list of platforms that the namespace is
98 - |types| a map of type names to their model.Type
99 - |functions| a map of function names to their model.Function
100 - |events| a map of event names to their model.Function
101 - |properties| a map of property names to their model.Property
102 - |compiler_options| the compiler_options dict, only not empty if
103 |include_compiler_options| is True
108 include_compiler_options
=False,
110 allow_inline_enums
=True):
111 self
.name
= json
['namespace']
112 if 'description' not in json
:
113 # TODO(kalman): Go back to throwing an error here.
114 print('%s must have a "description" field. This will appear '
115 'on the API summary page.' % self
.name
)
116 json
['description'] = ''
117 self
.description
= json
['description']
118 self
.deprecated
= json
.get('deprecated', None)
119 self
.unix_name
= UnixName(self
.name
)
120 self
.source_file
= source_file
121 self
.source_file_dir
, self
.source_file_filename
= os
.path
.split(source_file
)
122 self
.short_filename
= os
.path
.basename(source_file
).split('.')[0]
124 self
.allow_inline_enums
= allow_inline_enums
125 self
.platforms
= _GetPlatforms(json
)
126 toplevel_origin
= Origin(from_client
=True, from_json
=True)
127 self
.types
= _GetTypes(self
, json
, self
, toplevel_origin
)
128 self
.functions
= _GetFunctions(self
, json
, self
)
129 self
.events
= _GetEvents(self
, json
, self
)
130 self
.properties
= _GetProperties(self
, json
, self
, toplevel_origin
)
131 if include_compiler_options
:
132 self
.compiler_options
= json
.get('compiler_options', {})
134 self
.compiler_options
= {}
135 self
.environment
= environment
136 self
.documentation_options
= json
.get('documentation_options', {})
139 class Origin(object):
140 """Stores the possible origin of model object as a pair of bools. These are:
142 |from_client| indicating that instances can originate from users of
143 generated code (for example, function results), or
144 |from_json| indicating that instances can originate from the JSON (for
145 example, function parameters)
147 It is possible for model objects to originate from both the client and json,
148 for example Types defined in the top-level schema, in which case both
149 |from_client| and |from_json| would be True.
151 def __init__(self
, from_client
=False, from_json
=False):
152 if not from_client
and not from_json
:
153 raise ValueError('One of from_client or from_json must be true')
154 self
.from_client
= from_client
155 self
.from_json
= from_json
159 """A Type defined in the json.
162 - |name| the type name
163 - |namespace| the Type's namespace
164 - |description| the description of the type (if provided)
165 - |properties| a map of property unix_names to their model.Property
166 - |functions| a map of function names to their model.Function
167 - |events| a map of event names to their model.Event
168 - |origin| the Origin of the type
169 - |property_type| the PropertyType of this Type
170 - |item_type| if this is an array, the type of items in the array
171 - |simple_name| the name of this Type without a namespace
172 - |additional_properties| the type of the additional properties, if any is
182 self
.namespace
= namespace
183 self
.simple_name
= _StripNamespace(self
.name
, namespace
)
184 self
.unix_name
= UnixName(self
.name
)
185 self
.description
= json
.get('description', None)
188 self
.instance_of
= json
.get('isInstanceOf', None)
190 # TODO(kalman): Only objects need functions/events/properties, but callers
191 # assume that all types have them. Fix this.
192 self
.functions
= _GetFunctions(self
, json
, namespace
)
193 self
.events
= _GetEvents(self
, json
, namespace
)
194 self
.properties
= _GetProperties(self
, json
, namespace
, origin
)
196 json_type
= json
.get('type', None)
197 if json_type
== 'array':
198 self
.property_type
= PropertyType
.ARRAY
199 self
.item_type
= Type(
200 self
, '%sType' % name
, json
['items'], namespace
, origin
)
202 self
.property_type
= PropertyType
.REF
203 self
.ref_type
= json
['$ref']
204 elif 'enum' in json
and json_type
== 'string':
205 if not namespace
.allow_inline_enums
and not isinstance(parent
, Namespace
):
206 raise ParseException(
208 'Inline enum "%s" found in namespace "%s". These are not allowed. '
209 'See crbug.com/472279' % (name
, namespace
.name
))
210 self
.property_type
= PropertyType
.ENUM
211 self
.enum_values
= [EnumValue(value
) for value
in json
['enum']]
212 self
.cpp_enum_prefix_override
= json
.get('cpp_enum_prefix_override', None)
213 elif json_type
== 'any':
214 self
.property_type
= PropertyType
.ANY
215 elif json_type
== 'binary':
216 self
.property_type
= PropertyType
.BINARY
217 elif json_type
== 'boolean':
218 self
.property_type
= PropertyType
.BOOLEAN
219 elif json_type
== 'integer':
220 self
.property_type
= PropertyType
.INTEGER
221 elif (json_type
== 'double' or
222 json_type
== 'number'):
223 self
.property_type
= PropertyType
.DOUBLE
224 elif json_type
== 'string':
225 self
.property_type
= PropertyType
.STRING
226 elif 'choices' in json
:
227 self
.property_type
= PropertyType
.CHOICES
228 def generate_type_name(type_json
):
229 if 'items' in type_json
:
230 return '%ss' % generate_type_name(type_json
['items'])
231 if '$ref' in type_json
:
232 return type_json
['$ref']
233 if 'type' in type_json
:
234 return type_json
['type']
238 generate_type_name(choice
) or 'choice%s' % i
,
242 for i
, choice
in enumerate(json
['choices'])]
243 elif json_type
== 'object':
245 'isInstanceOf' in json
or
246 'properties' in json
or
247 'additionalProperties' in json
or
248 'functions' in json
or
250 raise ParseException(self
, name
+ " has no properties or functions")
251 self
.property_type
= PropertyType
.OBJECT
252 additional_properties_json
= json
.get('additionalProperties', None)
253 if additional_properties_json
is not None:
254 self
.additional_properties
= Type(self
,
255 'additionalProperties',
256 additional_properties_json
,
260 self
.additional_properties
= None
261 elif json_type
== 'function':
262 self
.property_type
= PropertyType
.FUNCTION
263 # Sometimes we might have an unnamed function, e.g. if it's a property
264 # of an object. Use the name of the property in that case.
265 function_name
= json
.get('name', name
)
266 self
.function
= Function(self
, function_name
, json
, namespace
, origin
)
268 raise ParseException(self
, 'Unsupported JSON type %s' % json_type
)
271 class Function(object):
272 """A Function defined in the API.
275 - |name| the function name
276 - |platforms| if not None, the list of platforms that the function is
278 - |params| a list of parameters to the function (order matters). A separate
279 parameter is used for each choice of a 'choices' parameter
280 - |deprecated| a reason and possible alternative for a deprecated function
281 - |description| a description of the function (if provided)
282 - |callback| the callback parameter to the function. There should be exactly
284 - |optional| whether the Function is "optional"; this only makes sense to be
285 present when the Function is representing a callback property
286 - |simple_name| the name of this Function without a namespace
287 - |returns| the return type of the function; None if the function does not
297 self
.simple_name
= _StripNamespace(self
.name
, namespace
)
298 self
.platforms
= _GetPlatforms(json
)
300 self
.description
= json
.get('description')
301 self
.deprecated
= json
.get('deprecated')
303 self
.optional
= json
.get('optional', False)
305 self
.nocompile
= json
.get('nocompile')
306 options
= json
.get('options', {})
307 self
.conditions
= options
.get('conditions', [])
308 self
.actions
= options
.get('actions', [])
309 self
.supports_listeners
= options
.get('supportsListeners', True)
310 self
.supports_rules
= options
.get('supportsRules', False)
311 self
.supports_dom
= options
.get('supportsDom', False)
313 def GeneratePropertyFromParam(p
):
314 return Property(self
, p
['name'], p
, namespace
, origin
)
316 self
.filters
= [GeneratePropertyFromParam(filter_instance
)
317 for filter_instance
in json
.get('filters', [])]
318 callback_param
= None
319 for param
in json
.get('parameters', []):
320 if param
.get('type') == 'function':
322 # No ParseException because the webstore has this.
323 # Instead, pretend all intermediate callbacks are properties.
324 self
.params
.append(GeneratePropertyFromParam(callback_param
))
325 callback_param
= param
327 self
.params
.append(GeneratePropertyFromParam(param
))
330 self
.callback
= Function(self
,
331 callback_param
['name'],
334 Origin(from_client
=True))
337 if 'returns' in json
:
338 self
.returns
= Type(self
,
339 '%sReturnType' % name
,
345 class Property(object):
346 """A property of a type OR a parameter to a function.
348 - |name| name of the property as in the json. This shouldn't change since
349 it is the key used to access DictionaryValues
350 - |unix_name| the unix_style_name of the property. Used as variable name
351 - |optional| a boolean representing whether the property is optional
352 - |description| a description of the property (if provided)
353 - |type_| the model.Type of this property
354 - |simple_name| the name of this Property without a namespace
355 - |deprecated| a reason and possible alternative for a deprecated property
357 def __init__(self
, parent
, name
, json
, namespace
, origin
):
358 """Creates a Property from JSON.
362 self
._unix
_name
= UnixName(self
.name
)
363 self
._unix
_name
_used
= False
365 self
.simple_name
= _StripNamespace(self
.name
, namespace
)
366 self
.description
= json
.get('description', None)
367 self
.optional
= json
.get('optional', None)
368 self
.instance_of
= json
.get('isInstanceOf', None)
369 self
.deprecated
= json
.get('deprecated')
371 # HACK: only support very specific value types.
373 '$ref' not in json
and
374 ('type' not in json
or json
['type'] == 'integer'
375 or json
['type'] == 'string'))
378 if 'value' in json
and is_allowed_value
:
379 self
.value
= json
['value']
380 if 'type' not in json
:
381 # Sometimes the type of the value is left out, and we need to figure
382 # it out for ourselves.
383 if isinstance(self
.value
, int):
384 json
['type'] = 'integer'
385 elif isinstance(self
.value
, basestring
):
386 json
['type'] = 'string'
388 # TODO(kalman): support more types as necessary.
389 raise ParseException(
391 '"%s" is not a supported type for "value"' % type(self
.value
))
393 self
.type_
= Type(parent
, name
, json
, namespace
, origin
)
395 def GetUnixName(self
):
396 """Gets the property's unix_name. Raises AttributeError if not set.
398 if not self
._unix
_name
:
399 raise AttributeError('No unix_name set on %s' % self
.name
)
400 self
._unix
_name
_used
= True
401 return self
._unix
_name
403 def SetUnixName(self
, unix_name
):
404 """Set the property's unix_name. Raises AttributeError if the unix_name has
405 already been used (GetUnixName has been called).
407 if unix_name
== self
._unix
_name
:
409 if self
._unix
_name
_used
:
410 raise AttributeError(
411 'Cannot set the unix_name on %s; '
412 'it is already used elsewhere as %s' %
413 (self
.name
, self
._unix
_name
))
414 self
._unix
_name
= unix_name
416 unix_name
= property(GetUnixName
, SetUnixName
)
418 class EnumValue(object):
419 """A single value from an enum.
421 - |name| name of the property as in the json.
422 - |description| a description of the property (if provided)
424 def __init__(self
, json
):
425 if isinstance(json
, dict):
426 self
.name
= json
['name']
427 self
.description
= json
.get('description')
430 self
.description
= None
433 return CamelName(self
.name
)
436 """Superclass for enum types with a "name" field, setting up repr/eq/ne.
437 Enums need to do this so that equality/non-equality work over pickling.
441 """Yields all _Enum objects declared in |cls|.
443 for prop_key
in dir(cls
):
444 prop_value
= getattr(cls
, prop_key
)
445 if isinstance(prop_value
, _Enum
):
448 def __init__(self
, name
):
451 def __eq__(self
, other
):
452 return type(other
) == type(self
) and other
.name
== self
.name
453 def __ne__(self
, other
):
454 return not (self
== other
)
463 class _PropertyTypeInfo(_Enum
):
464 def __init__(self
, is_fundamental
, name
):
465 _Enum
.__init
__(self
, name
)
466 self
.is_fundamental
= is_fundamental
471 class PropertyType(object):
472 """Enum of different types of properties/parameters.
474 ANY
= _PropertyTypeInfo(False, "any")
475 ARRAY
= _PropertyTypeInfo(False, "array")
476 BINARY
= _PropertyTypeInfo(False, "binary")
477 BOOLEAN
= _PropertyTypeInfo(True, "boolean")
478 CHOICES
= _PropertyTypeInfo(False, "choices")
479 DOUBLE
= _PropertyTypeInfo(True, "double")
480 ENUM
= _PropertyTypeInfo(False, "enum")
481 FUNCTION
= _PropertyTypeInfo(False, "function")
482 INT64
= _PropertyTypeInfo(True, "int64")
483 INTEGER
= _PropertyTypeInfo(True, "integer")
484 OBJECT
= _PropertyTypeInfo(False, "object")
485 REF
= _PropertyTypeInfo(False, "ref")
486 STRING
= _PropertyTypeInfo(True, "string")
491 '''Returns the unix_style name for a given lowerCamelCase string.
494 for i
, c
in enumerate(name
):
495 if c
.isupper() and i
> 0 and name
[i
- 1] != '_':
496 # Replace lowerUpper with lower_Upper.
497 if name
[i
- 1].islower():
498 unix_name
.append('_')
499 # Replace ACMEWidgets with ACME_Widgets
500 elif i
+ 1 < len(name
) and name
[i
+ 1].islower():
501 unix_name
.append('_')
503 # Replace hello.world with hello_world.
504 unix_name
.append('_')
506 # Everything is lowercase.
507 unix_name
.append(c
.lower())
508 return ''.join(unix_name
)
512 def CamelName(snake
):
513 ''' Converts a snake_cased_string to a camelCasedOne. '''
514 pieces
= snake
.split('_')
516 for i
, piece
in enumerate(pieces
):
520 camel
.append(piece
.capitalize())
521 return ''.join(camel
)
524 def _StripNamespace(name
, namespace
):
525 if name
.startswith(namespace
.name
+ '.'):
526 return name
[len(namespace
.name
+ '.'):]
530 def _GetModelHierarchy(entity
):
531 """Returns the hierarchy of the given model entity."""
533 while entity
is not None:
534 hierarchy
.append(getattr(entity
, 'name', repr(entity
)))
535 if isinstance(entity
, Namespace
):
536 hierarchy
.insert(0, ' in %s' % entity
.source_file
)
537 entity
= getattr(entity
, 'parent', None)
542 def _GetTypes(parent
, json
, namespace
, origin
):
543 """Creates Type objects extracted from |json|.
545 types
= OrderedDict()
546 for type_json
in json
.get('types', []):
547 type_
= Type(parent
, type_json
['id'], type_json
, namespace
, origin
)
548 types
[type_
.name
] = type_
552 def _GetFunctions(parent
, json
, namespace
):
553 """Creates Function objects extracted from |json|.
555 functions
= OrderedDict()
556 for function_json
in json
.get('functions', []):
557 function
= Function(parent
,
558 function_json
['name'],
561 Origin(from_json
=True))
562 functions
[function
.name
] = function
566 def _GetEvents(parent
, json
, namespace
):
567 """Creates Function objects generated from the events in |json|.
569 events
= OrderedDict()
570 for event_json
in json
.get('events', []):
571 event
= Function(parent
,
575 Origin(from_client
=True))
576 events
[event
.name
] = event
580 def _GetProperties(parent
, json
, namespace
, origin
):
581 """Generates Property objects extracted from |json|.
583 properties
= OrderedDict()
584 for name
, property_json
in json
.get('properties', {}).items():
585 properties
[name
] = Property(parent
, name
, property_json
, namespace
, origin
)
589 class _PlatformInfo(_Enum
):
590 def __init__(self
, name
):
591 _Enum
.__init
__(self
, name
)
594 class Platforms(object):
595 """Enum of the possible platforms.
597 CHROMEOS
= _PlatformInfo("chromeos")
598 CHROMEOS_TOUCH
= _PlatformInfo("chromeos_touch")
599 LINUX
= _PlatformInfo("linux")
600 MAC
= _PlatformInfo("mac")
601 WIN
= _PlatformInfo("win")
604 def _GetPlatforms(json
):
605 if 'platforms' not in json
or json
['platforms'] == None:
607 # Sanity check: platforms should not be an empty list.
608 if not json
['platforms']:
609 raise ValueError('"platforms" cannot be an empty list')
611 for platform_name
in json
['platforms']:
612 for platform_enum
in _Enum
.GetAll(Platforms
):
613 if platform_name
== platform_enum
.name
:
614 platforms
.append(platform_enum
)