idl_schema.py revision f2477e01787aa58f445919b809d89e252beef54f
1#! /usr/bin/env python
2# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import itertools
7import json
8import os.path
9import re
10import sys
11
12from json_parse import OrderedDict
13
14# This file is a peer to json_schema.py. Each of these files understands a
15# certain format describing APIs (either JSON or IDL), reads files written
16# in that format into memory, and emits them as a Python array of objects
17# corresponding to those APIs, where the objects are formatted in a way that
18# the JSON schema compiler understands. compiler.py drives both idl_schema.py
19# and json_schema.py.
20
21# idl_parser expects to be able to import certain files in its directory,
22# so let's set things up the way it wants.
23_idl_generators_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
24                                    os.pardir, os.pardir, 'ppapi', 'generators')
25if _idl_generators_path in sys.path:
26  import idl_parser
27else:
28  sys.path.insert(0, _idl_generators_path)
29  try:
30    import idl_parser
31  finally:
32    sys.path.pop(0)
33
34def ProcessComment(comment):
35  '''
36  Convert a comment into a parent comment and a list of parameter comments.
37
38  Function comments are of the form:
39    Function documentation. May contain HTML and multiple lines.
40
41    |arg1_name|: Description of arg1. Use <var>argument</var> to refer
42    to other arguments.
43    |arg2_name|: Description of arg2...
44
45  Newlines are removed, and leading and trailing whitespace is stripped.
46
47  Args:
48    comment: The string from a Comment node.
49
50  Returns: A tuple that looks like:
51    (
52      "The processed comment, minus all |parameter| mentions.",
53      {
54        'parameter_name_1': "The comment that followed |parameter_name_1|:",
55        ...
56      }
57    )
58  '''
59  # Find all the parameter comments of the form '|name|: comment'.
60  parameter_starts = list(re.finditer(r' *\|([^|]*)\| *: *', comment))
61
62  # Get the parent comment (everything before the first parameter comment.
63  first_parameter_location = (parameter_starts[0].start()
64                              if parameter_starts else len(comment))
65  parent_comment = comment[:first_parameter_location]
66
67  # We replace \n\n with <br/><br/> here and below, because the documentation
68  # needs to know where the newlines should be, and this is easier than
69  # escaping \n.
70  parent_comment = (parent_comment.strip().replace('\n\n', '<br/><br/>')
71                                          .replace('\n', ''))
72
73  params = OrderedDict()
74  for (cur_param, next_param) in itertools.izip_longest(parameter_starts,
75                                                        parameter_starts[1:]):
76    param_name = cur_param.group(1)
77
78    # A parameter's comment goes from the end of its introduction to the
79    # beginning of the next parameter's introduction.
80    param_comment_start = cur_param.end()
81    param_comment_end = next_param.start() if next_param else len(comment)
82    params[param_name] = (comment[param_comment_start:param_comment_end
83                                  ].strip().replace('\n\n', '<br/><br/>')
84                                           .replace('\n', ''))
85  return (parent_comment, params)
86
87
88class Callspec(object):
89  '''
90  Given a Callspec node representing an IDL function declaration, converts into
91  a tuple:
92      (name, list of function parameters, return type)
93  '''
94  def __init__(self, callspec_node, comment):
95    self.node = callspec_node
96    self.comment = comment
97
98  def process(self, callbacks):
99    parameters = []
100    return_type = None
101    if self.node.GetProperty('TYPEREF') not in ('void', None):
102      return_type = Typeref(self.node.GetProperty('TYPEREF'),
103                            self.node.parent,
104                            {'name': self.node.GetName()}).process(callbacks)
105      # The IDL parser doesn't allow specifying return types as optional.
106      # Instead we infer any object return values to be optional.
107      # TODO(asargent): fix the IDL parser to support optional return types.
108      if return_type.get('type') == 'object' or '$ref' in return_type:
109        return_type['optional'] = True
110    for node in self.node.children:
111      parameter = Param(node).process(callbacks)
112      if parameter['name'] in self.comment:
113        parameter['description'] = self.comment[parameter['name']]
114      parameters.append(parameter)
115    return (self.node.GetName(), parameters, return_type)
116
117
118class Param(object):
119  '''
120  Given a Param node representing a function parameter, converts into a Python
121  dictionary that the JSON schema compiler expects to see.
122  '''
123  def __init__(self, param_node):
124    self.node = param_node
125
126  def process(self, callbacks):
127    return Typeref(self.node.GetProperty('TYPEREF'),
128                   self.node,
129                   {'name': self.node.GetName()}).process(callbacks)
130
131
132class Dictionary(object):
133  '''
134  Given an IDL Dictionary node, converts into a Python dictionary that the JSON
135  schema compiler expects to see.
136  '''
137  def __init__(self, dictionary_node):
138    self.node = dictionary_node
139
140  def process(self, callbacks):
141    properties = OrderedDict()
142    for node in self.node.children:
143      if node.cls == 'Member':
144        k, v = Member(node).process(callbacks)
145        properties[k] = v
146    result = {'id': self.node.GetName(),
147              'properties': properties,
148              'type': 'object'}
149    if self.node.GetProperty('inline_doc'):
150      result['inline_doc'] = True
151    elif self.node.GetProperty('noinline_doc'):
152      result['noinline_doc'] = True
153    return result
154
155
156
157class Member(object):
158  '''
159  Given an IDL dictionary or interface member, converts into a name/value pair
160  where the value is a Python dictionary that the JSON schema compiler expects
161  to see.
162  '''
163  def __init__(self, member_node):
164    self.node = member_node
165
166  def process(self, callbacks):
167    properties = OrderedDict()
168    name = self.node.GetName()
169    for property_name in ('OPTIONAL', 'nodoc', 'nocompile', 'nodart'):
170      if self.node.GetProperty(property_name):
171        properties[property_name.lower()] = True
172    for option_name, sanitizer in [
173        ('maxListeners', int),
174        ('supportsFilters', lambda s: s == 'true'),
175        ('supportsListeners', lambda s: s == 'true'),
176        ('supportsRules', lambda s: s == 'true')]:
177      if self.node.GetProperty(option_name):
178        if 'options' not in properties:
179          properties['options'] = {}
180        properties['options'][option_name] = sanitizer(self.node.GetProperty(
181          option_name))
182    is_function = False
183    parameter_comments = OrderedDict()
184    for node in self.node.children:
185      if node.cls == 'Comment':
186        (parent_comment, parameter_comments) = ProcessComment(node.GetName())
187        properties['description'] = parent_comment
188      elif node.cls == 'Callspec':
189        is_function = True
190        name, parameters, return_type = (Callspec(node, parameter_comments)
191                                         .process(callbacks))
192        properties['parameters'] = parameters
193        if return_type is not None:
194          properties['returns'] = return_type
195    properties['name'] = name
196    if is_function:
197      properties['type'] = 'function'
198    else:
199      properties = Typeref(self.node.GetProperty('TYPEREF'),
200                           self.node, properties).process(callbacks)
201    enum_values = self.node.GetProperty('legalValues')
202    if enum_values:
203      if properties['type'] == 'integer':
204        enum_values = map(int, enum_values)
205      elif properties['type'] == 'double':
206        enum_values = map(float, enum_values)
207      properties['enum'] = enum_values
208    return name, properties
209
210
211class Typeref(object):
212  '''
213  Given a TYPEREF property representing the type of dictionary member or
214  function parameter, converts into a Python dictionary that the JSON schema
215  compiler expects to see.
216  '''
217  def __init__(self, typeref, parent, additional_properties=OrderedDict()):
218    self.typeref = typeref
219    self.parent = parent
220    self.additional_properties = additional_properties
221
222  def process(self, callbacks):
223    properties = self.additional_properties
224    result = properties
225
226    if self.parent.GetProperty('OPTIONAL', False):
227      properties['optional'] = True
228
229    # The IDL parser denotes array types by adding a child 'Array' node onto
230    # the Param node in the Callspec.
231    for sibling in self.parent.GetChildren():
232      if sibling.cls == 'Array' and sibling.GetName() == self.parent.GetName():
233        properties['type'] = 'array'
234        properties['items'] = OrderedDict()
235        properties = properties['items']
236        break
237
238    if self.typeref == 'DOMString':
239      properties['type'] = 'string'
240    elif self.typeref == 'boolean':
241      properties['type'] = 'boolean'
242    elif self.typeref == 'double':
243      properties['type'] = 'number'
244    elif self.typeref == 'long':
245      properties['type'] = 'integer'
246    elif self.typeref == 'any':
247      properties['type'] = 'any'
248    elif self.typeref == 'object':
249      properties['type'] = 'object'
250      if 'additionalProperties' not in properties:
251        properties['additionalProperties'] = OrderedDict()
252      properties['additionalProperties']['type'] = 'any'
253      instance_of = self.parent.GetProperty('instanceOf')
254      if instance_of:
255        properties['isInstanceOf'] = instance_of
256    elif self.typeref == 'ArrayBuffer':
257      properties['type'] = 'binary'
258      properties['isInstanceOf'] = 'ArrayBuffer'
259    elif self.typeref == 'FileEntry':
260      properties['type'] = 'object'
261      properties['isInstanceOf'] = 'FileEntry'
262      if 'additionalProperties' not in properties:
263        properties['additionalProperties'] = OrderedDict()
264      properties['additionalProperties']['type'] = 'any'
265    elif self.typeref is None:
266      properties['type'] = 'function'
267    else:
268      if self.typeref in callbacks:
269        # Do not override name and description if they are already specified.
270        name = properties.get('name', None)
271        description = properties.get('description', None)
272        properties.update(callbacks[self.typeref])
273        if description is not None:
274          properties['description'] = description
275        if name is not None:
276          properties['name'] = name
277      else:
278        properties['$ref'] = self.typeref
279    return result
280
281
282class Enum(object):
283  '''
284  Given an IDL Enum node, converts into a Python dictionary that the JSON
285  schema compiler expects to see.
286  '''
287  def __init__(self, enum_node):
288    self.node = enum_node
289    self.description = ''
290
291  def process(self, callbacks):
292    enum = []
293    for node in self.node.children:
294      if node.cls == 'EnumItem':
295        enum_value = {'name': node.GetName()}
296        for child in node.children:
297          if child.cls == 'Comment':
298            enum_value['description'] = ProcessComment(child.GetName())[0]
299          else:
300            raise ValueError('Did not process %s %s' % (child.cls, child))
301        enum.append(enum_value)
302      elif node.cls == 'Comment':
303        self.description = ProcessComment(node.GetName())[0]
304      else:
305        sys.exit('Did not process %s %s' % (node.cls, node))
306    result = {'id' : self.node.GetName(),
307              'description': self.description,
308              'type': 'string',
309              'enum': enum}
310    for property_name in ('inline_doc', 'noinline_doc', 'nodoc'):
311      if self.node.GetProperty(property_name):
312        result[property_name] = True
313    return result
314
315
316class Namespace(object):
317  '''
318  Given an IDLNode representing an IDL namespace, converts into a Python
319  dictionary that the JSON schema compiler expects to see.
320  '''
321
322  def __init__(self,
323               namespace_node,
324               description,
325               nodoc=False,
326               internal=False,
327               platforms=None,
328               compiler_options=None):
329    self.namespace = namespace_node
330    self.nodoc = nodoc
331    self.internal = internal
332    self.platforms = platforms
333    self.compiler_options = compiler_options
334    self.events = []
335    self.functions = []
336    self.types = []
337    self.callbacks = OrderedDict()
338    self.description = description
339
340  def process(self):
341    for node in self.namespace.children:
342      if node.cls == 'Dictionary':
343        self.types.append(Dictionary(node).process(self.callbacks))
344      elif node.cls == 'Callback':
345        k, v = Member(node).process(self.callbacks)
346        self.callbacks[k] = v
347      elif node.cls == 'Interface' and node.GetName() == 'Functions':
348        self.functions = self.process_interface(node)
349      elif node.cls == 'Interface' and node.GetName() == 'Events':
350        self.events = self.process_interface(node)
351      elif node.cls == 'Enum':
352        self.types.append(Enum(node).process(self.callbacks))
353      else:
354        sys.exit('Did not process %s %s' % (node.cls, node))
355    if self.compiler_options is not None:
356      compiler_options = self.compiler_options
357    else:
358      compiler_options = {}
359    return {'namespace': self.namespace.GetName(),
360            'description': self.description,
361            'nodoc': self.nodoc,
362            'types': self.types,
363            'functions': self.functions,
364            'internal': self.internal,
365            'events': self.events,
366            'platforms': self.platforms,
367            'compiler_options': compiler_options}
368
369  def process_interface(self, node):
370    members = []
371    for member in node.children:
372      if member.cls == 'Member':
373        name, properties = Member(member).process(self.callbacks)
374        members.append(properties)
375    return members
376
377
378class IDLSchema(object):
379  '''
380  Given a list of IDLNodes and IDLAttributes, converts into a Python list
381  of api_defs that the JSON schema compiler expects to see.
382  '''
383
384  def __init__(self, idl):
385    self.idl = idl
386
387  def process(self):
388    namespaces = []
389    nodoc = False
390    internal = False
391    description = None
392    platforms = None
393    compiler_options = None
394    for node in self.idl:
395      if node.cls == 'Namespace':
396        if not description:
397          # TODO(kalman): Go back to throwing an error here.
398          print('%s must have a namespace-level comment. This will '
399                           'appear on the API summary page.' % node.GetName())
400          description = ''
401        namespace = Namespace(node, description, nodoc, internal,
402                              platforms=platforms,
403                              compiler_options=compiler_options)
404        namespaces.append(namespace.process())
405        nodoc = False
406        internal = False
407        platforms = None
408        compiler_options = None
409      elif node.cls == 'Copyright':
410        continue
411      elif node.cls == 'Comment':
412        description = node.GetName()
413      elif node.cls == 'ExtAttribute':
414        if node.name == 'nodoc':
415          nodoc = bool(node.value)
416        elif node.name == 'internal':
417          internal = bool(node.value)
418        elif node.name == 'platforms':
419          platforms = list(node.value)
420        elif node.name == 'implemented_in':
421          compiler_options = {'implemented_in': node.value}
422        else:
423          continue
424      else:
425        sys.exit('Did not process %s %s' % (node.cls, node))
426    return namespaces
427
428
429def Load(filename):
430  '''
431  Given the filename of an IDL file, parses it and returns an equivalent
432  Python dictionary in a format that the JSON schema compiler expects to see.
433  '''
434
435  f = open(filename, 'r')
436  contents = f.read()
437  f.close()
438
439  idl = idl_parser.IDLParser().ParseData(contents, filename)
440  idl_schema = IDLSchema(idl)
441  return idl_schema.process()
442
443
444def Main():
445  '''
446  Dump a json serialization of parse result for the IDL files whose names
447  were passed in on the command line.
448  '''
449  for filename in sys.argv[1:]:
450    schema = Load(filename)
451    print json.dumps(schema, indent=2)
452
453
454if __name__ == '__main__':
455  Main()
456