model.py revision f2477e01787aa58f445919b809d89e252beef54f
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  - |unix_name| the unix_name of the namespace
85  - |source_file| the file that contained the namespace definition
86  - |source_file_dir| the directory component of |source_file|
87  - |source_file_filename| the filename component of |source_file|
88  - |platforms| if not None, the list of platforms that the namespace is
89                available to
90  - |types| a map of type names to their model.Type
91  - |functions| a map of function names to their model.Function
92  - |events| a map of event names to their model.Function
93  - |properties| a map of property names to their model.Property
94  - |compiler_options| the compiler_options dict, only not empty if
95                       |include_compiler_options| is True
96  """
97  def __init__(self, json, source_file, include_compiler_options=False):
98    self.name = json['namespace']
99    if 'description' not in json:
100      # TODO(kalman): Go back to throwing an error here.
101      print('%s must have a "description" field. This will appear '
102                       'on the API summary page.' % self.name)
103      json['description'] = ''
104    self.description = json['description']
105    self.unix_name = UnixName(self.name)
106    self.source_file = source_file
107    self.source_file_dir, self.source_file_filename = os.path.split(source_file)
108    self.parent = None
109    self.platforms = _GetPlatforms(json)
110    toplevel_origin = Origin(from_client=True, from_json=True)
111    self.types = _GetTypes(self, json, self, toplevel_origin)
112    self.functions = _GetFunctions(self, json, self)
113    self.events = _GetEvents(self, json, self)
114    self.properties = _GetProperties(self, json, self, toplevel_origin)
115    if include_compiler_options:
116      self.compiler_options = json.get('compiler_options', {})
117    else:
118      self.compiler_options = {}
119    self.documentation_options = json.get('documentation_options', {})
120
121
122class Origin(object):
123  """Stores the possible origin of model object as a pair of bools. These are:
124
125  |from_client| indicating that instances can originate from users of
126                generated code (for example, function results), or
127  |from_json|   indicating that instances can originate from the JSON (for
128                example, function parameters)
129
130  It is possible for model objects to originate from both the client and json,
131  for example Types defined in the top-level schema, in which case both
132  |from_client| and |from_json| would be True.
133  """
134  def __init__(self, from_client=False, from_json=False):
135    if not from_client and not from_json:
136      raise ValueError('One of from_client or from_json must be true')
137    self.from_client = from_client
138    self.from_json = from_json
139
140
141class Type(object):
142  """A Type defined in the json.
143
144  Properties:
145  - |name| the type name
146  - |namespace| the Type's namespace
147  - |description| the description of the type (if provided)
148  - |properties| a map of property unix_names to their model.Property
149  - |functions| a map of function names to their model.Function
150  - |events| a map of event names to their model.Event
151  - |origin| the Origin of the type
152  - |property_type| the PropertyType of this Type
153  - |item_type| if this is an array, the type of items in the array
154  - |simple_name| the name of this Type without a namespace
155  - |additional_properties| the type of the additional properties, if any is
156                            specified
157  """
158  def __init__(self,
159               parent,
160               name,
161               json,
162               namespace,
163               origin):
164    self.name = name
165    self.namespace = namespace
166    self.simple_name = _StripNamespace(self.name, namespace)
167    self.unix_name = UnixName(self.name)
168    self.description = json.get('description', None)
169    self.origin = origin
170    self.parent = parent
171    self.instance_of = json.get('isInstanceOf', None)
172
173    # TODO(kalman): Only objects need functions/events/properties, but callers
174    # assume that all types have them. Fix this.
175    self.functions = _GetFunctions(self, json, namespace)
176    self.events = _GetEvents(self, json, namespace)
177    self.properties = _GetProperties(self, json, namespace, origin)
178
179    json_type = json.get('type', None)
180    if json_type == 'array':
181      self.property_type = PropertyType.ARRAY
182      self.item_type = Type(
183          self, '%sType' % name, json['items'], namespace, origin)
184    elif '$ref' in json:
185      self.property_type = PropertyType.REF
186      self.ref_type = json['$ref']
187    elif 'enum' in json and json_type == 'string':
188      self.property_type = PropertyType.ENUM
189      self.enum_values = [EnumValue(value) for value in json['enum']]
190    elif json_type == 'any':
191      self.property_type = PropertyType.ANY
192    elif json_type == 'binary':
193      self.property_type = PropertyType.BINARY
194    elif json_type == 'boolean':
195      self.property_type = PropertyType.BOOLEAN
196    elif json_type == 'integer':
197      self.property_type = PropertyType.INTEGER
198    elif (json_type == 'double' or
199          json_type == 'number'):
200      self.property_type = PropertyType.DOUBLE
201    elif json_type == 'string':
202      self.property_type = PropertyType.STRING
203    elif 'choices' in json:
204      self.property_type = PropertyType.CHOICES
205      def generate_type_name(type_json):
206        if 'items' in type_json:
207          return '%ss' % generate_type_name(type_json['items'])
208        if '$ref' in type_json:
209          return type_json['$ref']
210        if 'type' in type_json:
211          return type_json['type']
212        return None
213      self.choices = [
214          Type(self,
215               generate_type_name(choice) or 'choice%s' % i,
216               choice,
217               namespace,
218               origin)
219          for i, choice in enumerate(json['choices'])]
220    elif json_type == 'object':
221      if not (
222          'isInstanceOf' in json or
223          'properties' in json or
224          'additionalProperties' in json or
225          'functions' in json or
226          'events' in json):
227        raise ParseException(self, name + " has no properties or functions")
228      self.property_type = PropertyType.OBJECT
229      additional_properties_json = json.get('additionalProperties', None)
230      if additional_properties_json is not None:
231        self.additional_properties = Type(self,
232                                          'additionalProperties',
233                                          additional_properties_json,
234                                          namespace,
235                                          origin)
236      else:
237        self.additional_properties = None
238    elif json_type == 'function':
239      self.property_type = PropertyType.FUNCTION
240      # Sometimes we might have an unnamed function, e.g. if it's a property
241      # of an object. Use the name of the property in that case.
242      function_name = json.get('name', name)
243      self.function = Function(self, function_name, json, namespace, origin)
244    else:
245      raise ParseException(self, 'Unsupported JSON type %s' % json_type)
246
247
248class Function(object):
249  """A Function defined in the API.
250
251  Properties:
252  - |name| the function name
253  - |platforms| if not None, the list of platforms that the function is
254                available to
255  - |params| a list of parameters to the function (order matters). A separate
256             parameter is used for each choice of a 'choices' parameter
257  - |deprecated| a reason and possible alternative for a deprecated function
258  - |description| a description of the function (if provided)
259  - |callback| the callback parameter to the function. There should be exactly
260               one
261  - |optional| whether the Function is "optional"; this only makes sense to be
262               present when the Function is representing a callback property
263  - |simple_name| the name of this Function without a namespace
264  - |returns| the return type of the function; None if the function does not
265    return a value
266  """
267  def __init__(self,
268               parent,
269               name,
270               json,
271               namespace,
272               origin):
273    self.name = name
274    self.simple_name = _StripNamespace(self.name, namespace)
275    self.platforms = _GetPlatforms(json)
276    self.params = []
277    self.description = json.get('description')
278    self.deprecated = json.get('deprecated')
279    self.callback = None
280    self.optional = json.get('optional', False)
281    self.parent = parent
282    self.nocompile = json.get('nocompile')
283    options = json.get('options', {})
284    self.conditions = options.get('conditions', [])
285    self.actions = options.get('actions', [])
286    self.supports_listeners = options.get('supportsListeners', True)
287    self.supports_rules = options.get('supportsRules', False)
288    self.supports_dom = options.get('supportsDom', False)
289
290    def GeneratePropertyFromParam(p):
291      return Property(self, p['name'], p, namespace, origin)
292
293    self.filters = [GeneratePropertyFromParam(filter)
294                    for filter in json.get('filters', [])]
295    callback_param = None
296    for param in json.get('parameters', []):
297      if param.get('type') == 'function':
298        if callback_param:
299          # No ParseException because the webstore has this.
300          # Instead, pretend all intermediate callbacks are properties.
301          self.params.append(GeneratePropertyFromParam(callback_param))
302        callback_param = param
303      else:
304        self.params.append(GeneratePropertyFromParam(param))
305
306    if callback_param:
307      self.callback = Function(self,
308                               callback_param['name'],
309                               callback_param,
310                               namespace,
311                               Origin(from_client=True))
312
313    self.returns = None
314    if 'returns' in json:
315      self.returns = Type(self,
316                          '%sReturnType' % name,
317                          json['returns'],
318                          namespace,
319                          origin)
320
321
322class Property(object):
323  """A property of a type OR a parameter to a function.
324  Properties:
325  - |name| name of the property as in the json. This shouldn't change since
326    it is the key used to access DictionaryValues
327  - |unix_name| the unix_style_name of the property. Used as variable name
328  - |optional| a boolean representing whether the property is optional
329  - |description| a description of the property (if provided)
330  - |type_| the model.Type of this property
331  - |simple_name| the name of this Property without a namespace
332  - |deprecated| a reason and possible alternative for a deprecated property
333  """
334  def __init__(self, parent, name, json, namespace, origin):
335    """Creates a Property from JSON.
336    """
337    self.parent = parent
338    self.name = name
339    self._unix_name = UnixName(self.name)
340    self._unix_name_used = False
341    self.origin = origin
342    self.simple_name = _StripNamespace(self.name, namespace)
343    self.description = json.get('description', None)
344    self.optional = json.get('optional', None)
345    self.instance_of = json.get('isInstanceOf', None)
346    self.deprecated = json.get('deprecated')
347
348    # HACK: only support very specific value types.
349    is_allowed_value = (
350        '$ref' not in json and
351        ('type' not in json or json['type'] == 'integer'
352                            or json['type'] == 'string'))
353
354    self.value = None
355    if 'value' in json and is_allowed_value:
356      self.value = json['value']
357      if 'type' not in json:
358        # Sometimes the type of the value is left out, and we need to figure
359        # it out for ourselves.
360        if isinstance(self.value, int):
361          json['type'] = 'integer'
362        elif isinstance(self.value, basestring):
363          json['type'] = 'string'
364        else:
365          # TODO(kalman): support more types as necessary.
366          raise ParseException(
367              parent,
368              '"%s" is not a supported type for "value"' % type(self.value))
369
370    self.type_ = Type(parent, name, json, namespace, origin)
371
372  def GetUnixName(self):
373    """Gets the property's unix_name. Raises AttributeError if not set.
374    """
375    if not self._unix_name:
376      raise AttributeError('No unix_name set on %s' % self.name)
377    self._unix_name_used = True
378    return self._unix_name
379
380  def SetUnixName(self, unix_name):
381    """Set the property's unix_name. Raises AttributeError if the unix_name has
382    already been used (GetUnixName has been called).
383    """
384    if unix_name == self._unix_name:
385      return
386    if self._unix_name_used:
387      raise AttributeError(
388          'Cannot set the unix_name on %s; '
389          'it is already used elsewhere as %s' %
390          (self.name, self._unix_name))
391    self._unix_name = unix_name
392
393  unix_name = property(GetUnixName, SetUnixName)
394
395class EnumValue(object):
396  """A single value from an enum.
397  Properties:
398  - |name| name of the property as in the json.
399  - |description| a description of the property (if provided)
400  """
401  def __init__(self, json):
402    if isinstance(json, dict):
403      self.name = json['name']
404      self.description = json.get('description')
405    else:
406      self.name = json
407      self.description = None
408
409class _Enum(object):
410  """Superclass for enum types with a "name" field, setting up repr/eq/ne.
411  Enums need to do this so that equality/non-equality work over pickling.
412  """
413  @staticmethod
414  def GetAll(cls):
415    """Yields all _Enum objects declared in |cls|.
416    """
417    for prop_key in dir(cls):
418      prop_value = getattr(cls, prop_key)
419      if isinstance(prop_value, _Enum):
420        yield prop_value
421
422  def __init__(self, name):
423    self.name = name
424
425  def __eq__(self, other):
426    return type(other) == type(self) and other.name == self.name
427  def __ne__(self, other):
428    return not (self == other)
429
430  def __repr__(self):
431    return self.name
432
433  def __str__(self):
434    return repr(self)
435
436
437class _PropertyTypeInfo(_Enum):
438  def __init__(self, is_fundamental, name):
439    _Enum.__init__(self, name)
440    self.is_fundamental = is_fundamental
441
442
443class PropertyType(object):
444  """Enum of different types of properties/parameters.
445  """
446  ANY = _PropertyTypeInfo(False, "any")
447  ARRAY = _PropertyTypeInfo(False, "array")
448  BINARY = _PropertyTypeInfo(False, "binary")
449  BOOLEAN = _PropertyTypeInfo(True, "boolean")
450  CHOICES = _PropertyTypeInfo(False, "choices")
451  DOUBLE = _PropertyTypeInfo(True, "double")
452  ENUM = _PropertyTypeInfo(False, "enum")
453  FUNCTION = _PropertyTypeInfo(False, "function")
454  INT64 = _PropertyTypeInfo(True, "int64")
455  INTEGER = _PropertyTypeInfo(True, "integer")
456  OBJECT = _PropertyTypeInfo(False, "object")
457  REF = _PropertyTypeInfo(False, "ref")
458  STRING = _PropertyTypeInfo(True, "string")
459
460
461@memoize
462def UnixName(name):
463  '''Returns the unix_style name for a given lowerCamelCase string.
464  '''
465  unix_name = []
466  for i, c in enumerate(name):
467    if c.isupper() and i > 0 and name[i - 1] != '_':
468      # Replace lowerUpper with lower_Upper.
469      if name[i - 1].islower():
470        unix_name.append('_')
471      # Replace ACMEWidgets with ACME_Widgets
472      elif i + 1 < len(name) and name[i + 1].islower():
473        unix_name.append('_')
474    if c == '.':
475      # Replace hello.world with hello_world.
476      unix_name.append('_')
477    else:
478      # Everything is lowercase.
479      unix_name.append(c.lower())
480  return ''.join(unix_name)
481
482
483def _StripNamespace(name, namespace):
484  if name.startswith(namespace.name + '.'):
485    return name[len(namespace.name + '.'):]
486  return name
487
488
489def _GetModelHierarchy(entity):
490  """Returns the hierarchy of the given model entity."""
491  hierarchy = []
492  while entity is not None:
493    hierarchy.append(getattr(entity, 'name', repr(entity)))
494    if isinstance(entity, Namespace):
495      hierarchy.insert(0, '  in %s' % entity.source_file)
496    entity = getattr(entity, 'parent', None)
497  hierarchy.reverse()
498  return hierarchy
499
500
501def _GetTypes(parent, json, namespace, origin):
502  """Creates Type objects extracted from |json|.
503  """
504  types = OrderedDict()
505  for type_json in json.get('types', []):
506    type_ = Type(parent, type_json['id'], type_json, namespace, origin)
507    types[type_.name] = type_
508  return types
509
510
511def _GetFunctions(parent, json, namespace):
512  """Creates Function objects extracted from |json|.
513  """
514  functions = OrderedDict()
515  for function_json in json.get('functions', []):
516    function = Function(parent,
517                        function_json['name'],
518                        function_json,
519                        namespace,
520                        Origin(from_json=True))
521    functions[function.name] = function
522  return functions
523
524
525def _GetEvents(parent, json, namespace):
526  """Creates Function objects generated from the events in |json|.
527  """
528  events = OrderedDict()
529  for event_json in json.get('events', []):
530    event = Function(parent,
531                     event_json['name'],
532                     event_json,
533                     namespace,
534                     Origin(from_client=True))
535    events[event.name] = event
536  return events
537
538
539def _GetProperties(parent, json, namespace, origin):
540  """Generates Property objects extracted from |json|.
541  """
542  properties = OrderedDict()
543  for name, property_json in json.get('properties', {}).items():
544    properties[name] = Property(parent, name, property_json, namespace, origin)
545  return properties
546
547
548class _PlatformInfo(_Enum):
549  def __init__(self, name):
550    _Enum.__init__(self, name)
551
552
553class Platforms(object):
554  """Enum of the possible platforms.
555  """
556  CHROMEOS = _PlatformInfo("chromeos")
557  CHROMEOS_TOUCH = _PlatformInfo("chromeos_touch")
558  LINUX = _PlatformInfo("linux")
559  MAC = _PlatformInfo("mac")
560  WIN = _PlatformInfo("win")
561
562
563def _GetPlatforms(json):
564  if 'platforms' not in json or json['platforms'] == None:
565    return None
566  # Sanity check: platforms should not be an empty list.
567  if not json['platforms']:
568    raise ValueError('"platforms" cannot be an empty list')
569  platforms = []
570  for platform_name in json['platforms']:
571    for platform_enum in _Enum.GetAll(Platforms):
572      if platform_name == platform_enum.name:
573        platforms.append(platform_enum)
574        break
575  return platforms
576