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