model.py revision 5821806d5e7f356e8fa4b058a389a808ea183019
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 copy
6import os.path
7import re
8
9class ParseException(Exception):
10  """Thrown when data in the model is invalid.
11  """
12  def __init__(self, parent, message):
13    hierarchy = _GetModelHierarchy(parent)
14    hierarchy.append(message)
15    Exception.__init__(
16        self, 'Model parse exception at:\n' + '\n'.join(hierarchy))
17
18class Model(object):
19  """Model of all namespaces that comprise an API.
20
21  Properties:
22  - |namespaces| a map of a namespace name to its model.Namespace
23  """
24  def __init__(self):
25    self.namespaces = {}
26
27  def AddNamespace(self, json, source_file):
28    """Add a namespace's json to the model and returns the namespace.
29    """
30    namespace = Namespace(json, source_file)
31    self.namespaces[namespace.name] = namespace
32    return namespace
33
34class Namespace(object):
35  """An API namespace.
36
37  Properties:
38  - |name| the name of the namespace
39  - |unix_name| the unix_name of the namespace
40  - |source_file| the file that contained the namespace definition
41  - |source_file_dir| the directory component of |source_file|
42  - |source_file_filename| the filename component of |source_file|
43  - |types| a map of type names to their model.Type
44  - |functions| a map of function names to their model.Function
45  - |events| a map of event names to their model.Function
46  - |properties| a map of property names to their model.Property
47  """
48  def __init__(self, json, source_file):
49    self.name = json['namespace']
50    self.unix_name = UnixName(self.name)
51    self.source_file = source_file
52    self.source_file_dir, self.source_file_filename = os.path.split(source_file)
53    self.parent = None
54    _AddTypes(self, json, self)
55    _AddFunctions(self, json, self)
56    _AddEvents(self, json, self)
57    _AddProperties(self, json, self)
58
59class Type(object):
60  """A Type defined in the json.
61
62  Properties:
63  - |name| the type name
64  - |description| the description of the type (if provided)
65  - |properties| a map of property unix_names to their model.Property
66  - |functions| a map of function names to their model.Function
67  - |events| a map of event names to their model.Event
68  - |from_client| indicates that instances of the Type can originate from the
69    users of generated code, such as top-level types and function results
70  - |from_json| indicates that instances of the Type can originate from the
71    JSON (as described by the schema), such as top-level types and function
72    parameters
73  - |type_| the PropertyType of this Type
74  - |item_type| if this is an array, the type of items in the array
75  - |simple_name| the name of this Type without a namespace
76  """
77  def __init__(self, parent, name, json, namespace):
78    if json.get('type') == 'array':
79      self.type_ = PropertyType.ARRAY
80      self.item_type = Property(self,
81                                name + "Element",
82                                json['items'],
83                                namespace,
84                                from_json=True,
85                                from_client=True)
86    elif 'enum' in json:
87      self.enum_values = []
88      for value in json['enum']:
89        self.enum_values.append(value)
90      self.type_ = PropertyType.ENUM
91    elif json.get('type') == 'string':
92      self.type_ = PropertyType.STRING
93    else:
94      if not (
95          'properties' in json or
96          'additionalProperties' in json or
97          'functions' in json or
98          'events' in json):
99        raise ParseException(self, name + " has no properties or functions")
100      self.type_ = PropertyType.OBJECT
101    self.name = name
102    self.simple_name = _StripNamespace(self.name, namespace)
103    self.unix_name = UnixName(self.name)
104    self.description = json.get('description')
105    self.from_json = True
106    self.from_client = True
107    self.parent = parent
108    self.instance_of = json.get('isInstanceOf', None)
109    _AddFunctions(self, json, namespace)
110    _AddEvents(self, json, namespace)
111    _AddProperties(self, json, namespace, from_json=True, from_client=True)
112
113    additional_properties_key = 'additionalProperties'
114    additional_properties = json.get(additional_properties_key)
115    if additional_properties:
116      self.properties[additional_properties_key] = Property(
117          self,
118          additional_properties_key,
119          additional_properties,
120          namespace,
121          is_additional_properties=True)
122
123class Function(object):
124  """A Function defined in the API.
125
126  Properties:
127  - |name| the function name
128  - |params| a list of parameters to the function (order matters). A separate
129    parameter is used for each choice of a 'choices' parameter.
130  - |description| a description of the function (if provided)
131  - |callback| the callback parameter to the function. There should be exactly
132    one
133  - |optional| whether the Function is "optional"; this only makes sense to be
134    present when the Function is representing a callback property.
135  - |simple_name| the name of this Function without a namespace
136  """
137  def __init__(self,
138               parent,
139               json,
140               namespace,
141               from_json=False,
142               from_client=False):
143    self.name = json['name']
144    self.simple_name = _StripNamespace(self.name, namespace)
145    self.params = []
146    self.description = json.get('description')
147    self.callback = None
148    self.optional = json.get('optional', False)
149    self.parent = parent
150    self.nocompile = json.get('nocompile')
151    options = json.get('options', {})
152    self.conditions = options.get('conditions', [])
153    self.actions = options.get('actions', [])
154    self.supports_listeners = options.get('supportsListeners', True)
155    self.supports_rules = options.get('supportsRules', False)
156    def GeneratePropertyFromParam(p):
157      return Property(self,
158                      p['name'], p,
159                      namespace,
160                      from_json=from_json,
161                      from_client=from_client)
162
163    self.filters = [GeneratePropertyFromParam(filter)
164                    for filter in json.get('filters', [])]
165    callback_param = None
166    for param in json.get('parameters', []):
167
168      if param.get('type') == 'function':
169        if callback_param:
170          # No ParseException because the webstore has this.
171          # Instead, pretend all intermediate callbacks are properties.
172          self.params.append(GeneratePropertyFromParam(callback_param))
173        callback_param = param
174      else:
175        self.params.append(GeneratePropertyFromParam(param))
176
177    if callback_param:
178      self.callback = Function(self,
179                               callback_param,
180                               namespace,
181                               from_client=True)
182
183    self.returns = None
184    if 'returns' in json:
185      self.returns = Property(self, 'return', json['returns'], namespace)
186
187class Property(object):
188  """A property of a type OR a parameter to a function.
189
190  Properties:
191  - |name| name of the property as in the json. This shouldn't change since
192    it is the key used to access DictionaryValues
193  - |unix_name| the unix_style_name of the property. Used as variable name
194  - |optional| a boolean representing whether the property is optional
195  - |description| a description of the property (if provided)
196  - |type_| the model.PropertyType of this property
197  - |compiled_type| the model.PropertyType that this property should be
198    compiled to from the JSON. Defaults to |type_|.
199  - |ref_type| the type that the REF property is referencing. Can be used to
200    map to its model.Type
201  - |item_type| a model.Property representing the type of each element in an
202    ARRAY
203  - |properties| the properties of an OBJECT parameter
204  - |from_client| indicates that instances of the Type can originate from the
205    users of generated code, such as top-level types and function results
206  - |from_json| indicates that instances of the Type can originate from the
207    JSON (as described by the schema), such as top-level types and function
208    parameters
209  - |simple_name| the name of this Property without a namespace
210  """
211
212  def __init__(self,
213               parent,
214               name,
215               json,
216               namespace,
217               is_additional_properties=False,
218               from_json=False,
219               from_client=False):
220    self.name = name
221    self.simple_name = _StripNamespace(self.name, namespace)
222    self._unix_name = UnixName(self.name)
223    self._unix_name_used = False
224    self.optional = json.get('optional', False)
225    self.functions = {}
226    self.has_value = False
227    self.description = json.get('description')
228    self.parent = parent
229    self.from_json = from_json
230    self.from_client = from_client
231    self.instance_of = json.get('isInstanceOf', None)
232    self.params = []
233    self.returns = None
234    _AddProperties(self, json, namespace)
235    if is_additional_properties:
236      self.type_ = PropertyType.ADDITIONAL_PROPERTIES
237    elif '$ref' in json:
238      self.ref_type = json['$ref']
239      self.type_ = PropertyType.REF
240    elif 'enum' in json and json.get('type') == 'string':
241      # Non-string enums (as in the case of [legalValues=(1,2)]) should fall
242      # through to the next elif.
243      self.enum_values = []
244      for value in json['enum']:
245        self.enum_values.append(value)
246      self.type_ = PropertyType.ENUM
247    elif 'type' in json:
248      self.type_ = self._JsonTypeToPropertyType(json['type'])
249      if self.type_ == PropertyType.ARRAY:
250        self.item_type = Property(self,
251                                  name + "Element",
252                                  json['items'],
253                                  namespace,
254                                  from_json=from_json,
255                                  from_client=from_client)
256      elif self.type_ == PropertyType.OBJECT:
257        # These members are read when this OBJECT Property is used as a Type
258        type_ = Type(self, self.name, json, namespace)
259        # self.properties will already have some value from |_AddProperties|.
260        self.properties.update(type_.properties)
261        self.functions = type_.functions
262      elif self.type_ == PropertyType.FUNCTION:
263        for p in json.get('parameters', []):
264          self.params.append(Property(self,
265                                      p['name'],
266                                      p,
267                                      namespace,
268                                      from_json=from_json,
269                                      from_client=from_client))
270        if 'returns' in json:
271          self.returns = Property(self, 'return', json['returns'], namespace)
272    elif 'choices' in json:
273      if not json['choices'] or len(json['choices']) == 0:
274        raise ParseException(self, 'Choices has no choices')
275      self.choices = {}
276      self.type_ = PropertyType.CHOICES
277      self.compiled_type = self.type_
278      for choice_json in json['choices']:
279        choice = Property(self,
280                          self.name,
281                          choice_json,
282                          namespace,
283                          from_json=from_json,
284                          from_client=from_client)
285        choice.unix_name = UnixName(self.name + choice.type_.name)
286        # The existence of any single choice is optional
287        choice.optional = True
288        self.choices[choice.type_] = choice
289    elif 'value' in json:
290      self.has_value = True
291      self.value = json['value']
292      if type(self.value) == int:
293        self.type_ = PropertyType.INTEGER
294        self.compiled_type = self.type_
295      else:
296        # TODO(kalman): support more types as necessary.
297        raise ParseException(
298            self, '"%s" is not a supported type' % type(self.value))
299    else:
300      raise ParseException(
301          self, 'Property has no type, $ref, choices, or value')
302    if 'compiled_type' in json:
303      if 'type' in json:
304        self.compiled_type = self._JsonTypeToPropertyType(json['compiled_type'])
305      else:
306        raise ParseException(self, 'Property has compiled_type but no type')
307    else:
308      self.compiled_type = self.type_
309
310  def _JsonTypeToPropertyType(self, json_type):
311    try:
312      return {
313        'any': PropertyType.ANY,
314        'array': PropertyType.ARRAY,
315        'binary': PropertyType.BINARY,
316        'boolean': PropertyType.BOOLEAN,
317        'integer': PropertyType.INTEGER,
318        'int64': PropertyType.INT64,
319        'function': PropertyType.FUNCTION,
320        'number': PropertyType.DOUBLE,
321        'object': PropertyType.OBJECT,
322        'string': PropertyType.STRING,
323      }[json_type]
324    except KeyError:
325      raise NotImplementedError('Type %s not recognized' % json_type)
326
327  def GetUnixName(self):
328    """Gets the property's unix_name. Raises AttributeError if not set.
329    """
330    if not self._unix_name:
331      raise AttributeError('No unix_name set on %s' % self.name)
332    self._unix_name_used = True
333    return self._unix_name
334
335  def SetUnixName(self, unix_name):
336    """Set the property's unix_name. Raises AttributeError if the unix_name has
337    already been used (GetUnixName has been called).
338    """
339    if unix_name == self._unix_name:
340      return
341    if self._unix_name_used:
342      raise AttributeError(
343          'Cannot set the unix_name on %s; '
344          'it is already used elsewhere as %s' %
345          (self.name, self._unix_name))
346    self._unix_name = unix_name
347
348  def Copy(self):
349    """Makes a copy of this model.Property object and allow the unix_name to be
350    set again.
351    """
352    property_copy = copy.copy(self)
353    property_copy._unix_name_used = False
354    return property_copy
355
356  unix_name = property(GetUnixName, SetUnixName)
357
358class _PropertyTypeInfo(object):
359  """This class is not an inner class of |PropertyType| so it can be pickled.
360  """
361  def __init__(self, is_fundamental, name):
362    self.is_fundamental = is_fundamental
363    self.name = name
364
365  def __repr__(self):
366    return self.name
367
368  def __eq__(self, other):
369    return isinstance(other, _PropertyTypeInfo) and self.name == other.name
370
371  def __ne__(self, other):
372    # Yes. You seriously do need this.
373    return not (self == other)
374
375class PropertyType(object):
376  """Enum of different types of properties/parameters.
377  """
378  INTEGER = _PropertyTypeInfo(True, "INTEGER")
379  INT64 = _PropertyTypeInfo(True, "INT64")
380  DOUBLE = _PropertyTypeInfo(True, "DOUBLE")
381  BOOLEAN = _PropertyTypeInfo(True, "BOOLEAN")
382  STRING = _PropertyTypeInfo(True, "STRING")
383  ENUM = _PropertyTypeInfo(False, "ENUM")
384  ARRAY = _PropertyTypeInfo(False, "ARRAY")
385  REF = _PropertyTypeInfo(False, "REF")
386  CHOICES = _PropertyTypeInfo(False, "CHOICES")
387  OBJECT = _PropertyTypeInfo(False, "OBJECT")
388  FUNCTION = _PropertyTypeInfo(False, "FUNCTION")
389  BINARY = _PropertyTypeInfo(False, "BINARY")
390  ANY = _PropertyTypeInfo(False, "ANY")
391  ADDITIONAL_PROPERTIES = _PropertyTypeInfo(False, "ADDITIONAL_PROPERTIES")
392
393def UnixName(name):
394  """Returns the unix_style name for a given lowerCamelCase string.
395  """
396  # First replace any lowerUpper patterns with lower_Upper.
397  s1 = re.sub('([a-z])([A-Z])', r'\1_\2', name)
398  # Now replace any ACMEWidgets patterns with ACME_Widgets
399  s2 = re.sub('([A-Z]+)([A-Z][a-z])', r'\1_\2', s1)
400  # Finally, replace any remaining periods, and make lowercase.
401  return s2.replace('.', '_').lower()
402
403def _StripNamespace(name, namespace):
404  if name.startswith(namespace.name + '.'):
405    return name[len(namespace.name + '.'):]
406  return name
407
408def _GetModelHierarchy(entity):
409  """Returns the hierarchy of the given model entity."""
410  hierarchy = []
411  while entity:
412    try:
413      hierarchy.append(entity.name)
414    except AttributeError:
415      hierarchy.append(repr(entity))
416    entity = entity.parent
417  hierarchy.reverse()
418  return hierarchy
419
420def _AddTypes(model, json, namespace):
421  """Adds Type objects to |model| contained in the 'types' field of |json|.
422  """
423  model.types = {}
424  for type_json in json.get('types', []):
425    type_ = Type(model, type_json['id'], type_json, namespace)
426    model.types[type_.name] = type_
427
428def _AddFunctions(model, json, namespace):
429  """Adds Function objects to |model| contained in the 'functions' field of
430  |json|.
431  """
432  model.functions = {}
433  for function_json in json.get('functions', []):
434    function = Function(model, function_json, namespace, from_json=True)
435    model.functions[function.name] = function
436
437def _AddEvents(model, json, namespace):
438  """Adds Function objects to |model| contained in the 'events' field of |json|.
439  """
440  model.events = {}
441  for event_json in json.get('events', []):
442    event = Function(model, event_json, namespace, from_client=True)
443    model.events[event.name] = event
444
445def _AddProperties(model,
446                   json,
447                   namespace,
448                   from_json=False,
449                   from_client=False):
450  """Adds model.Property objects to |model| contained in the 'properties' field
451  of |json|.
452  """
453  model.properties = {}
454  for name, property_json in json.get('properties', {}).items():
455    model.properties[name] = Property(
456        model,
457        name,
458        property_json,
459        namespace,
460        from_json=from_json,
461        from_client=from_client)
462