Upstream version 10.39.225.0
[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,
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
43
44
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])
49
50
51 class ComplexFeature(object):
52   """A complex feature which may be made of several simple features.
53
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
63
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.
67
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')
81
82
83 class Namespace(object):
84   """An API namespace.
85
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
102   """
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', {})
133
134
135 class Origin(object):
136   """Stores the possible origin of model object as a pair of bools. These are:
137
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)
142
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.
146   """
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
152
153
154 class Type(object):
155   """A Type defined in the json.
156
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
170   """
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)
185
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)
191
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)
260
261
262 class Function(object):
263   """A Function defined in the API.
264
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
274                one
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
280   """
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)
303
304     def GeneratePropertyFromParam(p):
305       return Property(self, p['name'], p, namespace, origin)
306
307     self.filters = [GeneratePropertyFromParam(filter)
308                     for filter 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))
319
320     if callback_param:
321       self.callback = Function(self,
322                                callback_param['name'],
323                                callback_param,
324                                namespace,
325                                Origin(from_client=True))
326
327     self.returns = None
328     if 'returns' in json:
329       self.returns = Type(self,
330                           '%sReturnType' % name,
331                           json['returns'],
332                           namespace,
333                           origin)
334
335
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
347   """
348   def __init__(self, parent, name, json, namespace, origin):
349     """Creates a Property from JSON.
350     """
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')
361
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'))
367
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))
383
384     self.type_ = Type(parent, name, json, namespace, origin)
385
386   def GetUnixName(self):
387     """Gets the property's unix_name. Raises AttributeError if not set.
388     """
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
393
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).
397     """
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
406
407   unix_name = property(GetUnixName, SetUnixName)
408
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)
414   """
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
422
423   def CamelName(self):
424     return CamelName(self.name)
425
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.
429   """
430   @staticmethod
431   def GetAll(cls):
432     """Yields all _Enum objects declared in |cls|.
433     """
434     for prop_key in dir(cls):
435       prop_value = getattr(cls, prop_key)
436       if isinstance(prop_value, _Enum):
437         yield prop_value
438
439   def __init__(self, name):
440     self.name = name
441
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)
446
447   def __repr__(self):
448     return self.name
449
450   def __str__(self):
451     return repr(self)
452
453
454 class _PropertyTypeInfo(_Enum):
455   def __init__(self, is_fundamental, name):
456     _Enum.__init__(self, name)
457     self.is_fundamental = is_fundamental
458
459
460 class PropertyType(object):
461   """Enum of different types of properties/parameters.
462   """
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")
476
477
478 @memoize
479 def UnixName(name):
480   '''Returns the unix_style name for a given lowerCamelCase string.
481   '''
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)
498
499
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)
511
512
513 def _StripNamespace(name, namespace):
514   if name.startswith(namespace.name + '.'):
515     return name[len(namespace.name + '.'):]
516   return name
517
518
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
529
530
531 def _GetTypes(parent, json, namespace, origin):
532   """Creates Type objects extracted from |json|.
533   """
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
539
540
541 def _GetFunctions(parent, json, namespace):
542   """Creates Function objects extracted from |json|.
543   """
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
553
554
555 def _GetEvents(parent, json, namespace):
556   """Creates Function objects generated from the events in |json|.
557   """
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
567
568
569 def _GetProperties(parent, json, namespace, origin):
570   """Generates Property objects extracted from |json|.
571   """
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
576
577
578 class _PlatformInfo(_Enum):
579   def __init__(self, name):
580     _Enum.__init__(self, name)
581
582
583 class Platforms(object):
584   """Enum of the possible platforms.
585   """
586   CHROMEOS = _PlatformInfo("chromeos")
587   CHROMEOS_TOUCH = _PlatformInfo("chromeos_touch")
588   LINUX = _PlatformInfo("linux")
589   MAC = _PlatformInfo("mac")
590   WIN = _PlatformInfo("win")
591
592
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