Allow offsets in blobs larger than 2GB on 32 bit Chromium builds.
[chromium-blink-merge.git] / tools / json_schema_compiler / model.py
blob7745dcc4fcc1cf1624de72a57ea495ff1d43ecda
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,
31 json,
32 source_file,
33 include_compiler_options=False,
34 environment=None):
35 """Add a namespace's json to the model and returns the namespace.
36 """
37 namespace = Namespace(json,
38 source_file,
39 include_compiler_options=include_compiler_options,
40 environment=environment)
41 self.namespaces[namespace.name] = namespace
42 return 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.
54 Properties:
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
58 """
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.
68 Properties:
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
74 """
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):
84 """An API namespace.
86 Properties:
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
95 available to
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
103 def __init__(self,
104 json,
105 source_file,
106 include_compiler_options=False,
107 environment=None):
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]
120 self.parent = None
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', {})
129 else:
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
154 class Type(object):
155 """A Type defined in the json.
157 Properties:
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
169 specified
171 def __init__(self,
172 parent,
173 name,
174 json,
175 namespace,
176 origin):
177 self.name = name
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)
182 self.origin = origin
183 self.parent = parent
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)
197 elif '$ref' in json:
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']
226 return None
227 self.choices = [
228 Type(self,
229 generate_type_name(choice) or 'choice%s' % i,
230 choice,
231 namespace,
232 origin)
233 for i, choice in enumerate(json['choices'])]
234 elif json_type == 'object':
235 if not (
236 'isInstanceOf' in json or
237 'properties' in json or
238 'additionalProperties' in json or
239 'functions' in json or
240 'events' in json):
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,
248 namespace,
249 origin)
250 else:
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)
258 else:
259 raise ParseException(self, 'Unsupported JSON type %s' % json_type)
262 class Function(object):
263 """A Function defined in the API.
265 Properties:
266 - |name| the function name
267 - |platforms| if not None, the list of platforms that the function is
268 available to
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
279 return a value
281 def __init__(self,
282 parent,
283 name,
284 json,
285 namespace,
286 origin):
287 self.name = name
288 self.simple_name = _StripNamespace(self.name, namespace)
289 self.platforms = _GetPlatforms(json)
290 self.params = []
291 self.description = json.get('description')
292 self.deprecated = json.get('deprecated')
293 self.callback = None
294 self.optional = json.get('optional', False)
295 self.parent = parent
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':
312 if callback_param:
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
317 else:
318 self.params.append(GeneratePropertyFromParam(param))
320 if callback_param:
321 self.callback = Function(self,
322 callback_param['name'],
323 callback_param,
324 namespace,
325 Origin(from_client=True))
327 self.returns = None
328 if 'returns' in json:
329 self.returns = Type(self,
330 '%sReturnType' % name,
331 json['returns'],
332 namespace,
333 origin)
336 class Property(object):
337 """A property of a type OR a parameter to a function.
338 Properties:
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.
351 self.parent = parent
352 self.name = name
353 self._unix_name = UnixName(self.name)
354 self._unix_name_used = False
355 self.origin = origin
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.
363 is_allowed_value = (
364 '$ref' not in json and
365 ('type' not in json or json['type'] == 'integer'
366 or json['type'] == 'string'))
368 self.value = None
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'
378 else:
379 # TODO(kalman): support more types as necessary.
380 raise ParseException(
381 parent,
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:
399 return
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.
411 Properties:
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')
419 else:
420 self.name = json
421 self.description = None
423 def CamelName(self):
424 return CamelName(self.name)
426 class _Enum(object):
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.
430 @staticmethod
431 def GetAll(cls):
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):
437 yield prop_value
439 def __init__(self, name):
440 self.name = 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)
447 def __repr__(self):
448 return self.name
450 def __str__(self):
451 return repr(self)
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")
478 @memoize
479 def UnixName(name):
480 '''Returns the unix_style name for a given lowerCamelCase string.
482 unix_name = []
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('_')
491 if c == '.':
492 # Replace hello.world with hello_world.
493 unix_name.append('_')
494 else:
495 # Everything is lowercase.
496 unix_name.append(c.lower())
497 return ''.join(unix_name)
500 @memoize
501 def CamelName(snake):
502 ''' Converts a snake_cased_string to a camelCasedOne. '''
503 pieces = snake.split('_')
504 camel = []
505 for i, piece in enumerate(pieces):
506 if i == 0:
507 camel.append(piece)
508 else:
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 + '.'):]
516 return name
519 def _GetModelHierarchy(entity):
520 """Returns the hierarchy of the given model entity."""
521 hierarchy = []
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)
527 hierarchy.reverse()
528 return hierarchy
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_
538 return types
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'],
548 function_json,
549 namespace,
550 Origin(from_json=True))
551 functions[function.name] = function
552 return functions
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,
561 event_json['name'],
562 event_json,
563 namespace,
564 Origin(from_client=True))
565 events[event.name] = event
566 return events
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)
575 return properties
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:
595 return 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')
599 platforms = []
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)
604 break
605 return platforms