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