blob: 38a70acd65912dfc39ec0614f81b94f458677a88 [file] [log] [blame]
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import copy
import os.path
import re
class ParseException(Exception):
"""Thrown when data in the model is invalid.
"""
def __init__(self, parent, message):
hierarchy = _GetModelHierarchy(parent)
hierarchy.append(message)
Exception.__init__(
self, 'Model parse exception at:\n' + '\n'.join(hierarchy))
class Model(object):
"""Model of all namespaces that comprise an API.
Properties:
- |namespaces| a map of a namespace name to its model.Namespace
"""
def __init__(self):
self.namespaces = {}
def AddNamespace(self, json, source_file):
"""Add a namespace's json to the model and returns the namespace.
"""
namespace = Namespace(json, source_file)
self.namespaces[namespace.name] = namespace
return namespace
class Namespace(object):
"""An API namespace.
Properties:
- |name| the name of the namespace
- |unix_name| the unix_name of the namespace
- |source_file| the file that contained the namespace definition
- |source_file_dir| the directory component of |source_file|
- |source_file_filename| the filename component of |source_file|
- |types| a map of type names to their model.Type
- |functions| a map of function names to their model.Function
- |events| a map of event names to their model.Function
- |properties| a map of property names to their model.Property
"""
def __init__(self, json, source_file):
self.name = json['namespace']
self.unix_name = UnixName(self.name)
self.source_file = source_file
self.source_file_dir, self.source_file_filename = os.path.split(source_file)
self.parent = None
_AddTypes(self, json)
_AddFunctions(self, json)
_AddEvents(self, json)
_AddProperties(self, json)
class Type(object):
"""A Type defined in the json.
Properties:
- |name| the type name
- |description| the description of the type (if provided)
- |properties| a map of property unix_names to their model.Property
- |functions| a map of function names to their model.Function
- |events| a map of event names to their model.Event
- |from_client| indicates that instances of the Type can originate from the
users of generated code, such as top-level types and function results
- |from_json| indicates that instances of the Type can originate from the
JSON (as described by the schema), such as top-level types and function
parameters
- |type_| the PropertyType of this Type
- |item_type| if this is an array, the type of items in the array
"""
def __init__(self, parent, name, json):
if json.get('type') == 'array':
self.type_ = PropertyType.ARRAY
self.item_type = Property(self, name + "Element", json['items'],
from_json=True,
from_client=True)
elif 'enum' in json:
self.enum_values = []
for value in json['enum']:
self.enum_values.append(value)
self.type_ = PropertyType.ENUM
elif json.get('type') == 'string':
self.type_ = PropertyType.STRING
else:
if not (
'properties' in json or
'additionalProperties' in json or
'functions' in json or
'events' in json):
raise ParseException(self, name + " has no properties or functions")
self.type_ = PropertyType.OBJECT
self.name = name
self.unix_name = UnixName(self.name)
self.description = json.get('description')
self.from_json = True
self.from_client = True
self.parent = parent
self.instance_of = json.get('isInstanceOf', None)
_AddFunctions(self, json)
_AddEvents(self, json)
_AddProperties(self, json, from_json=True, from_client=True)
additional_properties_key = 'additionalProperties'
additional_properties = json.get(additional_properties_key)
if additional_properties:
self.properties[additional_properties_key] = Property(
self,
additional_properties_key,
additional_properties,
is_additional_properties=True)
class Function(object):
"""A Function defined in the API.
Properties:
- |name| the function name
- |params| a list of parameters to the function (order matters). A separate
parameter is used for each choice of a 'choices' parameter.
- |description| a description of the function (if provided)
- |callback| the callback parameter to the function. There should be exactly
one
- |optional| whether the Function is "optional"; this only makes sense to be
present when the Function is representing a callback property.
"""
def __init__(self, parent, json, from_json=False, from_client=False):
self.name = json['name']
self.params = []
self.description = json.get('description')
self.callback = None
self.optional = json.get('optional', False)
self.parent = parent
self.nocompile = json.get('nocompile')
options = json.get('options', {})
self.conditions = options.get('conditions', [])
self.actions = options.get('actions', [])
self.supports_listeners = options.get('supportsListeners', True)
self.supports_rules = options.get('supportsRules', False)
def GeneratePropertyFromParam(p):
return Property(self,
p['name'], p,
from_json=from_json,
from_client=from_client)
self.filters = [GeneratePropertyFromParam(filter)
for filter in json.get('filters', [])]
callback_param = None
for param in json.get('parameters', []):
if param.get('type') == 'function':
if callback_param:
# No ParseException because the webstore has this.
# Instead, pretend all intermediate callbacks are properties.
self.params.append(GeneratePropertyFromParam(callback_param))
callback_param = param
else:
self.params.append(GeneratePropertyFromParam(param))
if callback_param:
self.callback = Function(self, callback_param, from_client=True)
self.returns = None
if 'returns' in json:
self.returns = Property(self, 'return', json['returns'])
class Property(object):
"""A property of a type OR a parameter to a function.
Properties:
- |name| name of the property as in the json. This shouldn't change since
it is the key used to access DictionaryValues
- |unix_name| the unix_style_name of the property. Used as variable name
- |optional| a boolean representing whether the property is optional
- |description| a description of the property (if provided)
- |type_| the model.PropertyType of this property
- |compiled_type| the model.PropertyType that this property should be
compiled to from the JSON. Defaults to |type_|.
- |ref_type| the type that the REF property is referencing. Can be used to
map to its model.Type
- |item_type| a model.Property representing the type of each element in an
ARRAY
- |properties| the properties of an OBJECT parameter
- |from_client| indicates that instances of the Type can originate from the
users of generated code, such as top-level types and function results
- |from_json| indicates that instances of the Type can originate from the
JSON (as described by the schema), such as top-level types and function
parameters
"""
def __init__(self, parent, name, json, is_additional_properties=False,
from_json=False, from_client=False):
self.name = name
self._unix_name = UnixName(self.name)
self._unix_name_used = False
self.optional = json.get('optional', False)
self.functions = {}
self.has_value = False
self.description = json.get('description')
self.parent = parent
self.from_json = from_json
self.from_client = from_client
self.instance_of = json.get('isInstanceOf', None)
_AddProperties(self, json)
if is_additional_properties:
self.type_ = PropertyType.ADDITIONAL_PROPERTIES
elif '$ref' in json:
self.ref_type = json['$ref']
self.type_ = PropertyType.REF
elif 'enum' in json and json.get('type') == 'string':
# Non-string enums (as in the case of [legalValues=(1,2)]) should fall
# through to the next elif.
self.enum_values = []
for value in json['enum']:
self.enum_values.append(value)
self.type_ = PropertyType.ENUM
elif 'type' in json:
self.type_ = self._JsonTypeToPropertyType(json['type'])
if self.type_ == PropertyType.ARRAY:
self.item_type = Property(self, name + "Element", json['items'],
from_json=from_json,
from_client=from_client)
elif self.type_ == PropertyType.OBJECT:
# These members are read when this OBJECT Property is used as a Type
type_ = Type(self, self.name, json)
# self.properties will already have some value from |_AddProperties|.
self.properties.update(type_.properties)
self.functions = type_.functions
elif 'choices' in json:
if not json['choices'] or len(json['choices']) == 0:
raise ParseException(self, 'Choices has no choices')
self.choices = {}
self.type_ = PropertyType.CHOICES
self.compiled_type = self.type_
for choice_json in json['choices']:
choice = Property(self, self.name, choice_json,
from_json=from_json,
from_client=from_client)
choice.unix_name = UnixName(self.name + choice.type_.name)
# The existence of any single choice is optional
choice.optional = True
self.choices[choice.type_] = choice
elif 'value' in json:
self.has_value = True
self.value = json['value']
if type(self.value) == int:
self.type_ = PropertyType.INTEGER
self.compiled_type = self.type_
else:
# TODO(kalman): support more types as necessary.
raise ParseException(
self, '"%s" is not a supported type' % type(self.value))
else:
raise ParseException(
self, 'Property has no type, $ref, choices, or value')
if 'compiled_type' in json:
if 'type' in json:
self.compiled_type = self._JsonTypeToPropertyType(json['compiled_type'])
else:
raise ParseException(self, 'Property has compiled_type but no type')
else:
self.compiled_type = self.type_
def _JsonTypeToPropertyType(self, json_type):
try:
return {
'any': PropertyType.ANY,
'array': PropertyType.ARRAY,
'binary': PropertyType.BINARY,
'boolean': PropertyType.BOOLEAN,
'integer': PropertyType.INTEGER,
'int64': PropertyType.INT64,
'function': PropertyType.FUNCTION,
'number': PropertyType.DOUBLE,
'object': PropertyType.OBJECT,
'string': PropertyType.STRING,
}[json_type]
except KeyError:
raise NotImplementedError('Type %s not recognized' % json_type)
def GetUnixName(self):
"""Gets the property's unix_name. Raises AttributeError if not set.
"""
if not self._unix_name:
raise AttributeError('No unix_name set on %s' % self.name)
self._unix_name_used = True
return self._unix_name
def SetUnixName(self, unix_name):
"""Set the property's unix_name. Raises AttributeError if the unix_name has
already been used (GetUnixName has been called).
"""
if unix_name == self._unix_name:
return
if self._unix_name_used:
raise AttributeError(
'Cannot set the unix_name on %s; '
'it is already used elsewhere as %s' %
(self.name, self._unix_name))
self._unix_name = unix_name
def Copy(self):
"""Makes a copy of this model.Property object and allow the unix_name to be
set again.
"""
property_copy = copy.copy(self)
property_copy._unix_name_used = False
return property_copy
unix_name = property(GetUnixName, SetUnixName)
class _PropertyTypeInfo(object):
"""This class is not an inner class of |PropertyType| so it can be pickled.
"""
def __init__(self, is_fundamental, name):
self.is_fundamental = is_fundamental
self.name = name
def __repr__(self):
return self.name
def __eq__(self, other):
return isinstance(other, _PropertyTypeInfo) and self.name == other.name
class PropertyType(object):
"""Enum of different types of properties/parameters.
"""
INTEGER = _PropertyTypeInfo(True, "INTEGER")
INT64 = _PropertyTypeInfo(True, "INT64")
DOUBLE = _PropertyTypeInfo(True, "DOUBLE")
BOOLEAN = _PropertyTypeInfo(True, "BOOLEAN")
STRING = _PropertyTypeInfo(True, "STRING")
ENUM = _PropertyTypeInfo(False, "ENUM")
ARRAY = _PropertyTypeInfo(False, "ARRAY")
REF = _PropertyTypeInfo(False, "REF")
CHOICES = _PropertyTypeInfo(False, "CHOICES")
OBJECT = _PropertyTypeInfo(False, "OBJECT")
FUNCTION = _PropertyTypeInfo(False, "FUNCTION")
BINARY = _PropertyTypeInfo(False, "BINARY")
ANY = _PropertyTypeInfo(False, "ANY")
ADDITIONAL_PROPERTIES = _PropertyTypeInfo(False, "ADDITIONAL_PROPERTIES")
def UnixName(name):
"""Returns the unix_style name for a given lowerCamelCase string.
"""
# First replace any lowerUpper patterns with lower_Upper.
s1 = re.sub('([a-z])([A-Z])', r'\1_\2', name)
# Now replace any ACMEWidgets patterns with ACME_Widgets
s2 = re.sub('([A-Z]+)([A-Z][a-z])', r'\1_\2', s1)
# Finally, replace any remaining periods, and make lowercase.
return s2.replace('.', '_').lower()
def _GetModelHierarchy(entity):
"""Returns the hierarchy of the given model entity."""
hierarchy = []
while entity:
try:
hierarchy.append(entity.name)
except AttributeError:
hierarchy.append(repr(entity))
entity = entity.parent
hierarchy.reverse()
return hierarchy
def _AddTypes(model, json):
"""Adds Type objects to |model| contained in the 'types' field of |json|.
"""
model.types = {}
for type_json in json.get('types', []):
type_ = Type(model, type_json['id'], type_json)
model.types[type_.name] = type_
def _AddFunctions(model, json):
"""Adds Function objects to |model| contained in the 'functions' field of
|json|.
"""
model.functions = {}
for function_json in json.get('functions', []):
function = Function(model, function_json, from_json=True)
model.functions[function.name] = function
def _AddEvents(model, json):
"""Adds Function objects to |model| contained in the 'events' field of |json|.
"""
model.events = {}
for event_json in json.get('events', []):
event = Function(model, event_json, from_client=True)
model.events[event.name] = event
def _AddProperties(model, json, from_json=False, from_client=False):
"""Adds model.Property objects to |model| contained in the 'properties' field
of |json|.
"""
model.properties = {}
for name, property_json in json.get('properties', {}).items():
model.properties[name] = Property(
model,
name,
property_json,
from_json=from_json,
from_client=from_client)