cc: Fix DRLI unit test failure with property tree verification
[chromium-blink-merge.git] / tools / json_schema_compiler / model.py
blob642e818492b6fe63a6f1ca51375157364770f41c
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.
5 import os.path
7 from json_parse import OrderedDict
8 from memoize import memoize
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))
21 class Model(object):
22 """Model of all namespaces that comprise an API.
24 Properties:
25 - |namespaces| a map of a namespace name to its model.Namespace
26 """
27 def __init__(self, allow_inline_enums=True):
28 self._allow_inline_enums = allow_inline_enums
29 self.namespaces = {}
31 def AddNamespace(self,
32 json,
33 source_file,
34 include_compiler_options=False,
35 environment=None):
36 """Add a namespace's json to the model and returns the namespace.
37 """
38 namespace = Namespace(json,
39 source_file,
40 include_compiler_options=include_compiler_options,
41 environment=environment,
42 allow_inline_enums=self._allow_inline_enums)
43 self.namespaces[namespace.name] = namespace
44 return 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.
56 Properties:
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
60 """
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.
70 Properties:
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
76 """
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):
86 """An API namespace.
88 Properties:
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
97 available to
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
105 def __init__(self,
106 json,
107 source_file,
108 include_compiler_options=False,
109 environment=None,
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]
123 self.parent = None
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', {})
133 else:
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
158 class Type(object):
159 """A Type defined in the json.
161 Properties:
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
173 specified
175 def __init__(self,
176 parent,
177 name,
178 json,
179 namespace,
180 origin):
181 self.name = name
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)
186 self.origin = origin
187 self.parent = parent
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)
201 elif '$ref' in json:
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(
207 self,
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']
235 return None
236 self.choices = [
237 Type(self,
238 generate_type_name(choice) or 'choice%s' % i,
239 choice,
240 namespace,
241 origin)
242 for i, choice in enumerate(json['choices'])]
243 elif json_type == 'object':
244 if not (
245 'isInstanceOf' in json or
246 'properties' in json or
247 'additionalProperties' in json or
248 'functions' in json or
249 'events' in json):
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,
257 namespace,
258 origin)
259 else:
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)
267 else:
268 raise ParseException(self, 'Unsupported JSON type %s' % json_type)
271 class Function(object):
272 """A Function defined in the API.
274 Properties:
275 - |name| the function name
276 - |platforms| if not None, the list of platforms that the function is
277 available to
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
288 return a value
290 def __init__(self,
291 parent,
292 name,
293 json,
294 namespace,
295 origin):
296 self.name = name
297 self.simple_name = _StripNamespace(self.name, namespace)
298 self.platforms = _GetPlatforms(json)
299 self.params = []
300 self.description = json.get('description')
301 self.deprecated = json.get('deprecated')
302 self.callback = None
303 self.optional = json.get('optional', False)
304 self.parent = parent
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':
321 if callback_param:
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
326 else:
327 self.params.append(GeneratePropertyFromParam(param))
329 if callback_param:
330 self.callback = Function(self,
331 callback_param['name'],
332 callback_param,
333 namespace,
334 Origin(from_client=True))
336 self.returns = None
337 if 'returns' in json:
338 self.returns = Type(self,
339 '%sReturnType' % name,
340 json['returns'],
341 namespace,
342 origin)
345 class Property(object):
346 """A property of a type OR a parameter to a function.
347 Properties:
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.
360 self.parent = parent
361 self.name = name
362 self._unix_name = UnixName(self.name)
363 self._unix_name_used = False
364 self.origin = origin
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.
372 is_allowed_value = (
373 '$ref' not in json and
374 ('type' not in json or json['type'] == 'integer'
375 or json['type'] == 'string'))
377 self.value = None
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'
387 else:
388 # TODO(kalman): support more types as necessary.
389 raise ParseException(
390 parent,
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:
408 return
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.
420 Properties:
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')
428 else:
429 self.name = json
430 self.description = None
432 def CamelName(self):
433 return CamelName(self.name)
435 class _Enum(object):
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.
439 @staticmethod
440 def GetAll(cls):
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):
446 yield prop_value
448 def __init__(self, name):
449 self.name = 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)
456 def __repr__(self):
457 return self.name
459 def __str__(self):
460 return repr(self)
463 class _PropertyTypeInfo(_Enum):
464 def __init__(self, is_fundamental, name):
465 _Enum.__init__(self, name)
466 self.is_fundamental = is_fundamental
468 def __repr__(self):
469 return self.name
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")
489 @memoize
490 def UnixName(name):
491 '''Returns the unix_style name for a given lowerCamelCase string.
493 unix_name = []
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('_')
502 if c == '.':
503 # Replace hello.world with hello_world.
504 unix_name.append('_')
505 else:
506 # Everything is lowercase.
507 unix_name.append(c.lower())
508 return ''.join(unix_name)
511 @memoize
512 def CamelName(snake):
513 ''' Converts a snake_cased_string to a camelCasedOne. '''
514 pieces = snake.split('_')
515 camel = []
516 for i, piece in enumerate(pieces):
517 if i == 0:
518 camel.append(piece)
519 else:
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 + '.'):]
527 return name
530 def _GetModelHierarchy(entity):
531 """Returns the hierarchy of the given model entity."""
532 hierarchy = []
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)
538 hierarchy.reverse()
539 return hierarchy
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_
549 return types
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'],
559 function_json,
560 namespace,
561 Origin(from_json=True))
562 functions[function.name] = function
563 return functions
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,
572 event_json['name'],
573 event_json,
574 namespace,
575 Origin(from_client=True))
576 events[event.name] = event
577 return events
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)
586 return properties
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:
606 return 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')
610 platforms = []
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)
615 break
616 return platforms