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, 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 
     40 def 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 
     46 class 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 
     59 class 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 
     78 class 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 
    122 class 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 
    141 class 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 
    248 class 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 
    322 class 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 
    395 class 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 
    409 class _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 
    437 class _PropertyTypeInfo(_Enum):
    438   def __init__(self, is_fundamental, name):
    439     _Enum.__init__(self, name)
    440     self.is_fundamental = is_fundamental
    441 
    442 
    443 class 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
    462 def 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 
    483 def _StripNamespace(name, namespace):
    484   if name.startswith(namespace.name + '.'):
    485     return name[len(namespace.name + '.'):]
    486   return name
    487 
    488 
    489 def _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 
    501 def _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 
    511 def _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 
    525 def _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 
    539 def _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 
    548 class _PlatformInfo(_Enum):
    549   def __init__(self, name):
    550     _Enum.__init__(self, name)
    551 
    552 
    553 class 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 
    563 def _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