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,
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
45def 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
51class 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
64class 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
83class 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
135class 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
154class 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
262class 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
336class 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
409class 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
426class _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
454class _PropertyTypeInfo(_Enum):
455  def __init__(self, is_fundamental, name):
456    _Enum.__init__(self, name)
457    self.is_fundamental = is_fundamental
458
459
460class 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
479def 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
501def 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
513def _StripNamespace(name, namespace):
514  if name.startswith(namespace.name + '.'):
515    return name[len(namespace.name + '.'):]
516  return name
517
518
519def _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
531def _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
541def _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
555def _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
569def _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
578class _PlatformInfo(_Enum):
579  def __init__(self, name):
580    _Enum.__init__(self, name)
581
582
583class 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
593def _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
606