1# Copyright 2013 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# TODO(vtl): "data" is a pretty vague name. Rename it?
6
7import copy
8
9import module as mojom
10
11# This module provides a mechanism to turn mojom Modules to dictionaries and
12# back again. This can be used to persist a mojom Module created progromatically
13# or to read a dictionary from code or a file.
14# Example:
15# test_dict = {
16#   'name': 'test',
17#   'namespace': 'testspace',
18#   'structs': [{
19#     'name': 'teststruct',
20#     'fields': [
21#       {'name': 'testfield1', 'kind': 'i32'},
22#       {'name': 'testfield2', 'kind': 'a:i32', 'ordinal': 42}]}],
23#   'interfaces': [{
24#     'name': 'Server',
25#     'methods': [{
26#       'name': 'Foo',
27#       'parameters': [{
28#         'name': 'foo', 'kind': 'i32'},
29#         {'name': 'bar', 'kind': 'a:x:teststruct'}],
30#     'ordinal': 42}]}]
31# }
32# test_module = data.ModuleFromData(test_dict)
33
34# Used to create a subclass of str that supports sorting by index, to make
35# pretty printing maintain the order.
36def istr(index, string):
37  class IndexedString(str):
38    def __lt__(self, other):
39      return self.__index__ < other.__index__
40
41  rv = IndexedString(string)
42  rv.__index__ = index
43  return rv
44
45builtin_values = frozenset([
46    "double.INFINITY",
47    "double.NEGATIVE_INFINITY",
48    "double.NAN",
49    "float.INFINITY",
50    "float.NEGATIVE_INFINITY",
51    "float.NAN"])
52
53def IsBuiltinValue(value):
54  return value in builtin_values
55
56def LookupKind(kinds, spec, scope):
57  """Tries to find which Kind a spec refers to, given the scope in which its
58  referenced. Starts checking from the narrowest scope to most general. For
59  example, given a struct field like
60    Foo.Bar x;
61  Foo.Bar could refer to the type 'Bar' in the 'Foo' namespace, or an inner
62  type 'Bar' in the struct 'Foo' in the current namespace.
63
64  |scope| is a tuple that looks like (namespace, struct/interface), referring
65  to the location where the type is referenced."""
66  if spec.startswith('x:'):
67    name = spec[2:]
68    for i in xrange(len(scope), -1, -1):
69      test_spec = 'x:'
70      if i > 0:
71        test_spec += '.'.join(scope[:i]) + '.'
72      test_spec += name
73      kind = kinds.get(test_spec)
74      if kind:
75        return kind
76
77  return kinds.get(spec)
78
79def LookupValue(values, name, scope, kind):
80  """Like LookupKind, but for constant values."""
81  # If the type is an enum, the value can be specified as a qualified name, in
82  # which case the form EnumName.ENUM_VALUE must be used. We use the presence
83  # of a '.' in the requested name to identify this. Otherwise, we prepend the
84  # enum name.
85  if isinstance(kind, mojom.Enum) and '.' not in name:
86    name = '%s.%s' % (kind.spec.split(':', 1)[1], name)
87  for i in reversed(xrange(len(scope) + 1)):
88    test_spec = '.'.join(scope[:i])
89    if test_spec:
90      test_spec += '.'
91    test_spec += name
92    value = values.get(test_spec)
93    if value:
94      return value
95
96  return values.get(name)
97
98def FixupExpression(module, value, scope, kind):
99  """Translates an IDENTIFIER into a built-in value or structured NamedValue
100     object."""
101  if isinstance(value, tuple) and value[0] == 'IDENTIFIER':
102    # Allow user defined values to shadow builtins.
103    result = LookupValue(module.values, value[1], scope, kind)
104    if result:
105      if isinstance(result, tuple):
106        raise Exception('Unable to resolve expression: %r' % value[1])
107      return result
108    if IsBuiltinValue(value[1]):
109      return mojom.BuiltinValue(value[1])
110  return value
111
112def KindToData(kind):
113  return kind.spec
114
115def KindFromData(kinds, data, scope):
116  kind = LookupKind(kinds, data, scope)
117  if kind:
118    return kind
119
120  if data.startswith('?'):
121    kind = KindFromData(kinds, data[1:], scope).MakeNullableKind()
122  elif data.startswith('a:'):
123    kind = mojom.Array(KindFromData(kinds, data[2:], scope))
124  elif data.startswith('r:'):
125    kind = mojom.InterfaceRequest(KindFromData(kinds, data[2:], scope))
126  elif data.startswith('a'):
127    colon = data.find(':')
128    length = int(data[1:colon])
129    kind = mojom.FixedArray(length, KindFromData(kinds, data[colon+1:], scope))
130  else:
131    kind = mojom.Kind(data)
132
133  kinds[data] = kind
134  return kind
135
136def KindFromImport(original_kind, imported_from):
137  """Used with 'import module' - clones the kind imported from the given
138  module's namespace. Only used with Structs, Interfaces and Enums."""
139  kind = copy.copy(original_kind)
140  # |shared_definition| is used to store various properties (see
141  # |AddSharedProperty()| in module.py), including |imported_from|. We don't
142  # want the copy to share these with the original, so copy it if necessary.
143  if hasattr(original_kind, 'shared_definition'):
144    kind.shared_definition = copy.copy(original_kind.shared_definition)
145  kind.imported_from = imported_from
146  return kind
147
148def ImportFromData(module, data):
149  import_module = data['module']
150
151  import_item = {}
152  import_item['module_name'] = import_module.name
153  import_item['namespace'] = import_module.namespace
154  import_item['module'] = import_module
155
156  # Copy the struct kinds from our imports into the current module.
157  for kind in import_module.kinds.itervalues():
158    if (isinstance(kind, (mojom.Struct, mojom.Enum, mojom.Interface)) and
159        kind.imported_from is None):
160      kind = KindFromImport(kind, import_item)
161      module.kinds[kind.spec] = kind
162  # Ditto for values.
163  for value in import_module.values.itervalues():
164    if value.imported_from is None:
165      # Values don't have shared definitions (since they're not nullable), so no
166      # need to do anything special.
167      value = copy.copy(value)
168      value.imported_from = import_item
169      module.values[value.GetSpec()] = value
170
171  return import_item
172
173def StructToData(struct):
174  return {
175    istr(0, 'name'): struct.name,
176    istr(1, 'fields'): map(FieldToData, struct.fields)
177  }
178
179def StructFromData(module, data):
180  struct = mojom.Struct(module=module)
181  struct.name = data['name']
182  struct.attributes = data['attributes']
183  struct.spec = 'x:' + module.namespace + '.' + struct.name
184  module.kinds[struct.spec] = struct
185  struct.enums = map(lambda enum:
186      EnumFromData(module, enum, struct), data['enums'])
187  struct.constants = map(lambda constant:
188      ConstantFromData(module, constant, struct), data['constants'])
189  # Stash fields data here temporarily.
190  struct.fields_data = data['fields']
191  return struct
192
193def FieldToData(field):
194  data = {
195    istr(0, 'name'): field.name,
196    istr(1, 'kind'): KindToData(field.kind)
197  }
198  if field.ordinal != None:
199    data[istr(2, 'ordinal')] = field.ordinal
200  if field.default != None:
201    data[istr(3, 'default')] = field.default
202  return data
203
204def FieldFromData(module, data, struct):
205  field = mojom.Field()
206  field.name = data['name']
207  field.kind = KindFromData(
208      module.kinds, data['kind'], (module.namespace, struct.name))
209  field.ordinal = data.get('ordinal')
210  field.default = FixupExpression(
211      module, data.get('default'), (module.namespace, struct.name), field.kind)
212  return field
213
214def ParameterToData(parameter):
215  data = {
216    istr(0, 'name'): parameter.name,
217    istr(1, 'kind'): parameter.kind.spec
218  }
219  if parameter.ordinal != None:
220    data[istr(2, 'ordinal')] = parameter.ordinal
221  if parameter.default != None:
222    data[istr(3, 'default')] = parameter.default
223  return data
224
225def ParameterFromData(module, data, interface):
226  parameter = mojom.Parameter()
227  parameter.name = data['name']
228  parameter.kind = KindFromData(
229      module.kinds, data['kind'], (module.namespace, interface.name))
230  parameter.ordinal = data.get('ordinal')
231  parameter.default = data.get('default')
232  return parameter
233
234def MethodToData(method):
235  data = {
236    istr(0, 'name'):       method.name,
237    istr(1, 'parameters'): map(ParameterToData, method.parameters)
238  }
239  if method.ordinal != None:
240    data[istr(2, 'ordinal')] = method.ordinal
241  if method.response_parameters != None:
242    data[istr(3, 'response_parameters')] = map(
243        ParameterToData, method.response_parameters)
244  return data
245
246def MethodFromData(module, data, interface):
247  method = mojom.Method(interface, data['name'], ordinal=data.get('ordinal'))
248  method.default = data.get('default')
249  method.parameters = map(lambda parameter:
250      ParameterFromData(module, parameter, interface), data['parameters'])
251  if data.has_key('response_parameters'):
252    method.response_parameters = map(
253        lambda parameter: ParameterFromData(module, parameter, interface),
254                          data['response_parameters'])
255  return method
256
257def InterfaceToData(interface):
258  return {
259    istr(0, 'name'):    interface.name,
260    istr(1, 'client'):  interface.client,
261    istr(2, 'methods'): map(MethodToData, interface.methods)
262  }
263
264def InterfaceFromData(module, data):
265  interface = mojom.Interface(module=module)
266  interface.name = data['name']
267  interface.spec = 'x:' + module.namespace + '.' + interface.name
268  interface.client = data['client'] if data.has_key('client') else None
269  module.kinds[interface.spec] = interface
270  interface.enums = map(lambda enum:
271      EnumFromData(module, enum, interface), data['enums'])
272  interface.constants = map(lambda constant:
273      ConstantFromData(module, constant, interface), data['constants'])
274  # Stash methods data here temporarily.
275  interface.methods_data = data['methods']
276  return interface
277
278def EnumFieldFromData(module, enum, data, parent_kind):
279  field = mojom.EnumField()
280  field.name = data['name']
281  # TODO(mpcomplete): FixupExpression should be done in the second pass,
282  # so constants and enums can refer to each other.
283  # TODO(mpcomplete): But then, what if constants are initialized to an enum? Or
284  # vice versa?
285  if parent_kind:
286    field.value = FixupExpression(
287        module, data['value'], (module.namespace, parent_kind.name), enum)
288  else:
289    field.value = FixupExpression(
290        module, data['value'], (module.namespace, ), enum)
291  value = mojom.EnumValue(module, enum, field)
292  module.values[value.GetSpec()] = value
293  return field
294
295def EnumFromData(module, data, parent_kind):
296  enum = mojom.Enum(module=module)
297  enum.name = data['name']
298  name = enum.name
299  if parent_kind:
300    name = parent_kind.name + '.' + name
301  enum.spec = 'x:%s.%s' % (module.namespace, name)
302  enum.parent_kind = parent_kind
303
304  enum.fields = map(
305      lambda field: EnumFieldFromData(module, enum, field, parent_kind),
306      data['fields'])
307  module.kinds[enum.spec] = enum
308  return enum
309
310def ConstantFromData(module, data, parent_kind):
311  constant = mojom.Constant()
312  constant.name = data['name']
313  if parent_kind:
314    scope = (module.namespace, parent_kind.name)
315  else:
316    scope = (module.namespace, )
317  # TODO(mpcomplete): maybe we should only support POD kinds.
318  constant.kind = KindFromData(module.kinds, data['kind'], scope)
319  constant.value = FixupExpression(module, data.get('value'), scope, None)
320
321  value = mojom.ConstantValue(module, parent_kind, constant)
322  module.values[value.GetSpec()] = value
323  return constant
324
325def ModuleToData(module):
326  return {
327    istr(0, 'name'):       module.name,
328    istr(1, 'namespace'):  module.namespace,
329    istr(2, 'structs'):    map(StructToData, module.structs),
330    istr(3, 'interfaces'): map(InterfaceToData, module.interfaces)
331  }
332
333def ModuleFromData(data):
334  module = mojom.Module()
335  module.kinds = {}
336  for kind in mojom.PRIMITIVES:
337    module.kinds[kind.spec] = kind
338
339  module.values = {}
340
341  module.name = data['name']
342  module.namespace = data['namespace']
343  module.attributes = data['attributes']
344  # Imports must come first, because they add to module.kinds which is used
345  # by by the others.
346  module.imports = map(
347      lambda import_data: ImportFromData(module, import_data),
348      data['imports'])
349
350  # First pass collects kinds.
351  module.enums = map(
352      lambda enum: EnumFromData(module, enum, None), data['enums'])
353  module.structs = map(
354      lambda struct: StructFromData(module, struct), data['structs'])
355  module.interfaces = map(
356      lambda interface: InterfaceFromData(module, interface),
357      data['interfaces'])
358  module.constants = map(
359      lambda constant: ConstantFromData(module, constant, None),
360      data['constants'])
361
362  # Second pass expands fields and methods. This allows fields and parameters
363  # to refer to kinds defined anywhere in the mojom.
364  for struct in module.structs:
365    struct.fields = map(lambda field:
366        FieldFromData(module, field, struct), struct.fields_data)
367    del struct.fields_data
368  for interface in module.interfaces:
369    interface.methods = map(lambda method:
370        MethodFromData(module, method, interface), interface.methods_data)
371    del interface.methods_data
372
373  return module
374
375def OrderedModuleFromData(data):
376  module = ModuleFromData(data)
377  for interface in module.interfaces:
378    next_ordinal = 0
379    for method in interface.methods:
380      if method.ordinal is None:
381        method.ordinal = next_ordinal
382      next_ordinal = method.ordinal + 1
383  return module
384