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