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
5import os.path
6
7from json_parse import OrderedDict
8from memoize import memoize
9
10
11class 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
21class 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
40def 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
46class 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
59class 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
78class 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
125class 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
144class 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_enum_prefix_override = json.get('cpp_enum_prefix_override', None)
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
252class 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
326class 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
399class 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  def CamelName(self):
414    return CamelName(self.name)
415
416class _Enum(object):
417  """Superclass for enum types with a "name" field, setting up repr/eq/ne.
418  Enums need to do this so that equality/non-equality work over pickling.
419  """
420  @staticmethod
421  def GetAll(cls):
422    """Yields all _Enum objects declared in |cls|.
423    """
424    for prop_key in dir(cls):
425      prop_value = getattr(cls, prop_key)
426      if isinstance(prop_value, _Enum):
427        yield prop_value
428
429  def __init__(self, name):
430    self.name = name
431
432  def __eq__(self, other):
433    return type(other) == type(self) and other.name == self.name
434  def __ne__(self, other):
435    return not (self == other)
436
437  def __repr__(self):
438    return self.name
439
440  def __str__(self):
441    return repr(self)
442
443
444class _PropertyTypeInfo(_Enum):
445  def __init__(self, is_fundamental, name):
446    _Enum.__init__(self, name)
447    self.is_fundamental = is_fundamental
448
449
450class PropertyType(object):
451  """Enum of different types of properties/parameters.
452  """
453  ANY = _PropertyTypeInfo(False, "any")
454  ARRAY = _PropertyTypeInfo(False, "array")
455  BINARY = _PropertyTypeInfo(False, "binary")
456  BOOLEAN = _PropertyTypeInfo(True, "boolean")
457  CHOICES = _PropertyTypeInfo(False, "choices")
458  DOUBLE = _PropertyTypeInfo(True, "double")
459  ENUM = _PropertyTypeInfo(False, "enum")
460  FUNCTION = _PropertyTypeInfo(False, "function")
461  INT64 = _PropertyTypeInfo(True, "int64")
462  INTEGER = _PropertyTypeInfo(True, "integer")
463  OBJECT = _PropertyTypeInfo(False, "object")
464  REF = _PropertyTypeInfo(False, "ref")
465  STRING = _PropertyTypeInfo(True, "string")
466
467
468@memoize
469def UnixName(name):
470  '''Returns the unix_style name for a given lowerCamelCase string.
471  '''
472  unix_name = []
473  for i, c in enumerate(name):
474    if c.isupper() and i > 0 and name[i - 1] != '_':
475      # Replace lowerUpper with lower_Upper.
476      if name[i - 1].islower():
477        unix_name.append('_')
478      # Replace ACMEWidgets with ACME_Widgets
479      elif i + 1 < len(name) and name[i + 1].islower():
480        unix_name.append('_')
481    if c == '.':
482      # Replace hello.world with hello_world.
483      unix_name.append('_')
484    else:
485      # Everything is lowercase.
486      unix_name.append(c.lower())
487  return ''.join(unix_name)
488
489
490@memoize
491def CamelName(snake):
492  ''' Converts a snake_cased_string to a camelCasedOne. '''
493  pieces = snake.split('_')
494  camel = []
495  for i, piece in enumerate(pieces):
496    if i == 0:
497      camel.append(piece)
498    else:
499      camel.append(piece.capitalize())
500  return ''.join(camel)
501
502
503def _StripNamespace(name, namespace):
504  if name.startswith(namespace.name + '.'):
505    return name[len(namespace.name + '.'):]
506  return name
507
508
509def _GetModelHierarchy(entity):
510  """Returns the hierarchy of the given model entity."""
511  hierarchy = []
512  while entity is not None:
513    hierarchy.append(getattr(entity, 'name', repr(entity)))
514    if isinstance(entity, Namespace):
515      hierarchy.insert(0, '  in %s' % entity.source_file)
516    entity = getattr(entity, 'parent', None)
517  hierarchy.reverse()
518  return hierarchy
519
520
521def _GetTypes(parent, json, namespace, origin):
522  """Creates Type objects extracted from |json|.
523  """
524  types = OrderedDict()
525  for type_json in json.get('types', []):
526    type_ = Type(parent, type_json['id'], type_json, namespace, origin)
527    types[type_.name] = type_
528  return types
529
530
531def _GetFunctions(parent, json, namespace):
532  """Creates Function objects extracted from |json|.
533  """
534  functions = OrderedDict()
535  for function_json in json.get('functions', []):
536    function = Function(parent,
537                        function_json['name'],
538                        function_json,
539                        namespace,
540                        Origin(from_json=True))
541    functions[function.name] = function
542  return functions
543
544
545def _GetEvents(parent, json, namespace):
546  """Creates Function objects generated from the events in |json|.
547  """
548  events = OrderedDict()
549  for event_json in json.get('events', []):
550    event = Function(parent,
551                     event_json['name'],
552                     event_json,
553                     namespace,
554                     Origin(from_client=True))
555    events[event.name] = event
556  return events
557
558
559def _GetProperties(parent, json, namespace, origin):
560  """Generates Property objects extracted from |json|.
561  """
562  properties = OrderedDict()
563  for name, property_json in json.get('properties', {}).items():
564    properties[name] = Property(parent, name, property_json, namespace, origin)
565  return properties
566
567
568class _PlatformInfo(_Enum):
569  def __init__(self, name):
570    _Enum.__init__(self, name)
571
572
573class Platforms(object):
574  """Enum of the possible platforms.
575  """
576  CHROMEOS = _PlatformInfo("chromeos")
577  CHROMEOS_TOUCH = _PlatformInfo("chromeos_touch")
578  LINUX = _PlatformInfo("linux")
579  MAC = _PlatformInfo("mac")
580  WIN = _PlatformInfo("win")
581
582
583def _GetPlatforms(json):
584  if 'platforms' not in json or json['platforms'] == None:
585    return None
586  # Sanity check: platforms should not be an empty list.
587  if not json['platforms']:
588    raise ValueError('"platforms" cannot be an empty list')
589  platforms = []
590  for platform_name in json['platforms']:
591    for platform_enum in _Enum.GetAll(Platforms):
592      if platform_name == platform_enum.name:
593        platforms.append(platform_enum)
594        break
595  return platforms
596