Don't kill key events even when it's VKEY_UNKNOWN.
[chromium-blink-merge.git] / tools / json_schema_compiler / model.py
blob967e9abec0041ef9d2d7c7de6378d7abbd7ee1e3
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):
28 self.namespaces = {}
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
40 def CreateFeature(name, model):
41 if isinstance(model, dict):
42 return SimpleFeature(name, model)
43 return ComplexFeature(name, [SimpleFeature(name, child) for child in model])
46 class ComplexFeature(object):
47 """A complex feature which may be made of several simple features.
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
59 class 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.
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')
78 class Namespace(object):
79 """An API namespace.
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', {})
125 class Origin(object):
126 """Stores the possible origin of model object as a pair of bools. These are:
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)
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.
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
144 class Type(object):
145 """A Type defined in the json.
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
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)
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)
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_omit_enum_type = 'cpp_omit_enum_type' in json
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)
252 class Function(object):
253 """A Function defined in the API.
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
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
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)
294 def GeneratePropertyFromParam(p):
295 return Property(self, p['name'], p, namespace, origin)
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))
310 if callback_param:
311 self.callback = Function(self,
312 callback_param['name'],
313 callback_param,
314 namespace,
315 Origin(from_client=True))
317 self.returns = None
318 if 'returns' in json:
319 self.returns = Type(self,
320 '%sReturnType' % name,
321 json['returns'],
322 namespace,
323 origin)
326 class 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
338 def __init__(self, parent, name, json, namespace, origin):
339 """Creates a Property from JSON.
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')
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'))
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))
374 self.type_ = Type(parent, name, json, namespace, origin)
376 def GetUnixName(self):
377 """Gets the property's unix_name. Raises AttributeError if not set.
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
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).
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
397 unix_name = property(GetUnixName, SetUnixName)
399 class 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)
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
413 class _Enum(object):
414 """Superclass for enum types with a "name" field, setting up repr/eq/ne.
415 Enums need to do this so that equality/non-equality work over pickling.
417 @staticmethod
418 def GetAll(cls):
419 """Yields all _Enum objects declared in |cls|.
421 for prop_key in dir(cls):
422 prop_value = getattr(cls, prop_key)
423 if isinstance(prop_value, _Enum):
424 yield prop_value
426 def __init__(self, name):
427 self.name = name
429 def __eq__(self, other):
430 return type(other) == type(self) and other.name == self.name
431 def __ne__(self, other):
432 return not (self == other)
434 def __repr__(self):
435 return self.name
437 def __str__(self):
438 return repr(self)
441 class _PropertyTypeInfo(_Enum):
442 def __init__(self, is_fundamental, name):
443 _Enum.__init__(self, name)
444 self.is_fundamental = is_fundamental
447 class PropertyType(object):
448 """Enum of different types of properties/parameters.
450 ANY = _PropertyTypeInfo(False, "any")
451 ARRAY = _PropertyTypeInfo(False, "array")
452 BINARY = _PropertyTypeInfo(False, "binary")
453 BOOLEAN = _PropertyTypeInfo(True, "boolean")
454 CHOICES = _PropertyTypeInfo(False, "choices")
455 DOUBLE = _PropertyTypeInfo(True, "double")
456 ENUM = _PropertyTypeInfo(False, "enum")
457 FUNCTION = _PropertyTypeInfo(False, "function")
458 INT64 = _PropertyTypeInfo(True, "int64")
459 INTEGER = _PropertyTypeInfo(True, "integer")
460 OBJECT = _PropertyTypeInfo(False, "object")
461 REF = _PropertyTypeInfo(False, "ref")
462 STRING = _PropertyTypeInfo(True, "string")
465 @memoize
466 def UnixName(name):
467 '''Returns the unix_style name for a given lowerCamelCase string.
469 unix_name = []
470 for i, c in enumerate(name):
471 if c.isupper() and i > 0 and name[i - 1] != '_':
472 # Replace lowerUpper with lower_Upper.
473 if name[i - 1].islower():
474 unix_name.append('_')
475 # Replace ACMEWidgets with ACME_Widgets
476 elif i + 1 < len(name) and name[i + 1].islower():
477 unix_name.append('_')
478 if c == '.':
479 # Replace hello.world with hello_world.
480 unix_name.append('_')
481 else:
482 # Everything is lowercase.
483 unix_name.append(c.lower())
484 return ''.join(unix_name)
487 def _StripNamespace(name, namespace):
488 if name.startswith(namespace.name + '.'):
489 return name[len(namespace.name + '.'):]
490 return name
493 def _GetModelHierarchy(entity):
494 """Returns the hierarchy of the given model entity."""
495 hierarchy = []
496 while entity is not None:
497 hierarchy.append(getattr(entity, 'name', repr(entity)))
498 if isinstance(entity, Namespace):
499 hierarchy.insert(0, ' in %s' % entity.source_file)
500 entity = getattr(entity, 'parent', None)
501 hierarchy.reverse()
502 return hierarchy
505 def _GetTypes(parent, json, namespace, origin):
506 """Creates Type objects extracted from |json|.
508 types = OrderedDict()
509 for type_json in json.get('types', []):
510 type_ = Type(parent, type_json['id'], type_json, namespace, origin)
511 types[type_.name] = type_
512 return types
515 def _GetFunctions(parent, json, namespace):
516 """Creates Function objects extracted from |json|.
518 functions = OrderedDict()
519 for function_json in json.get('functions', []):
520 function = Function(parent,
521 function_json['name'],
522 function_json,
523 namespace,
524 Origin(from_json=True))
525 functions[function.name] = function
526 return functions
529 def _GetEvents(parent, json, namespace):
530 """Creates Function objects generated from the events in |json|.
532 events = OrderedDict()
533 for event_json in json.get('events', []):
534 event = Function(parent,
535 event_json['name'],
536 event_json,
537 namespace,
538 Origin(from_client=True))
539 events[event.name] = event
540 return events
543 def _GetProperties(parent, json, namespace, origin):
544 """Generates Property objects extracted from |json|.
546 properties = OrderedDict()
547 for name, property_json in json.get('properties', {}).items():
548 properties[name] = Property(parent, name, property_json, namespace, origin)
549 return properties
552 class _PlatformInfo(_Enum):
553 def __init__(self, name):
554 _Enum.__init__(self, name)
557 class Platforms(object):
558 """Enum of the possible platforms.
560 CHROMEOS = _PlatformInfo("chromeos")
561 CHROMEOS_TOUCH = _PlatformInfo("chromeos_touch")
562 LINUX = _PlatformInfo("linux")
563 MAC = _PlatformInfo("mac")
564 WIN = _PlatformInfo("win")
567 def _GetPlatforms(json):
568 if 'platforms' not in json or json['platforms'] == None:
569 return None
570 # Sanity check: platforms should not be an empty list.
571 if not json['platforms']:
572 raise ValueError('"platforms" cannot be an empty list')
573 platforms = []
574 for platform_name in json['platforms']:
575 for platform_enum in _Enum.GetAll(Platforms):
576 if platform_name == platform_enum.name:
577 platforms.append(platform_enum)
578 break
579 return platforms