967e9abec0041ef9d2d7c7de6378d7abbd7ee1e3
[platform/framework/web/crosswalk.git] / src / tools / json_schema_compiler / model.py
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.
4
5 import os.path
6
7 from json_parse import OrderedDict
8 from memoize import memoize
9
10
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))
19
20
21 class Model(object):
22   """Model of all namespaces that comprise an API.
23
24   Properties:
25   - |namespaces| a map of a namespace name to its model.Namespace
26   """
27   def __init__(self):
28     self.namespaces = {}
29
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
38
39
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])
44
45
46 class ComplexFeature(object):
47   """A complex feature which may be made of several simple features.
48
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
58
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.
62
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')
76
77
78 class Namespace(object):
79   """An API namespace.
80
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', {})
123
124
125 class Origin(object):
126   """Stores the possible origin of model object as a pair of bools. These are:
127
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)
132
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.
136   """
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
142
143
144 class Type(object):
145   """A Type defined in the json.
146
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
160   """
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)
175
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)
181
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)
250
251
252 class Function(object):
253   """A Function defined in the API.
254
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
264                one
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
270   """
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)
293
294     def GeneratePropertyFromParam(p):
295       return Property(self, p['name'], p, namespace, origin)
296
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))
309
310     if callback_param:
311       self.callback = Function(self,
312                                callback_param['name'],
313                                callback_param,
314                                namespace,
315                                Origin(from_client=True))
316
317     self.returns = None
318     if 'returns' in json:
319       self.returns = Type(self,
320                           '%sReturnType' % name,
321                           json['returns'],
322                           namespace,
323                           origin)
324
325
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
337   """
338   def __init__(self, parent, name, json, namespace, origin):
339     """Creates a Property from JSON.
340     """
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')
351
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'))
357
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))
373
374     self.type_ = Type(parent, name, json, namespace, origin)
375
376   def GetUnixName(self):
377     """Gets the property's unix_name. Raises AttributeError if not set.
378     """
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
383
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).
387     """
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
396
397   unix_name = property(GetUnixName, SetUnixName)
398
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)
404   """
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
412
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.
416   """
417   @staticmethod
418   def GetAll(cls):
419     """Yields all _Enum objects declared in |cls|.
420     """
421     for prop_key in dir(cls):
422       prop_value = getattr(cls, prop_key)
423       if isinstance(prop_value, _Enum):
424         yield prop_value
425
426   def __init__(self, name):
427     self.name = name
428
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)
433
434   def __repr__(self):
435     return self.name
436
437   def __str__(self):
438     return repr(self)
439
440
441 class _PropertyTypeInfo(_Enum):
442   def __init__(self, is_fundamental, name):
443     _Enum.__init__(self, name)
444     self.is_fundamental = is_fundamental
445
446
447 class PropertyType(object):
448   """Enum of different types of properties/parameters.
449   """
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")
463
464
465 @memoize
466 def UnixName(name):
467   '''Returns the unix_style name for a given lowerCamelCase string.
468   '''
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)
485
486
487 def _StripNamespace(name, namespace):
488   if name.startswith(namespace.name + '.'):
489     return name[len(namespace.name + '.'):]
490   return name
491
492
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
503
504
505 def _GetTypes(parent, json, namespace, origin):
506   """Creates Type objects extracted from |json|.
507   """
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
513
514
515 def _GetFunctions(parent, json, namespace):
516   """Creates Function objects extracted from |json|.
517   """
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
527
528
529 def _GetEvents(parent, json, namespace):
530   """Creates Function objects generated from the events in |json|.
531   """
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
541
542
543 def _GetProperties(parent, json, namespace, origin):
544   """Generates Property objects extracted from |json|.
545   """
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
550
551
552 class _PlatformInfo(_Enum):
553   def __init__(self, name):
554     _Enum.__init__(self, name)
555
556
557 class Platforms(object):
558   """Enum of the possible platforms.
559   """
560   CHROMEOS = _PlatformInfo("chromeos")
561   CHROMEOS_TOUCH = _PlatformInfo("chromeos_touch")
562   LINUX = _PlatformInfo("linux")
563   MAC = _PlatformInfo("mac")
564   WIN = _PlatformInfo("win")
565
566
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