• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.GetChildren():
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.GetChildren():
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('nodoc'):
150      result['nodoc'] = True
151    elif self.node.GetProperty('inline_doc'):
152      result['inline_doc'] = True
153    elif self.node.GetProperty('noinline_doc'):
154      result['noinline_doc'] = True
155    return result
156
157
158
159class Member(object):
160  '''
161  Given an IDL dictionary or interface member, converts into a name/value pair
162  where the value is a Python dictionary that the JSON schema compiler expects
163  to see.
164  '''
165  def __init__(self, member_node):
166    self.node = member_node
167
168  def process(self, callbacks):
169    properties = OrderedDict()
170    name = self.node.GetName()
171    if self.node.GetProperty('deprecated'):
172      properties['deprecated'] = self.node.GetProperty('deprecated')
173    for property_name in ('OPTIONAL', 'nodoc', 'nocompile', 'nodart'):
174      if self.node.GetProperty(property_name):
175        properties[property_name.lower()] = True
176    for option_name, sanitizer in [
177        ('maxListeners', int),
178        ('supportsFilters', lambda s: s == 'true'),
179        ('supportsListeners', lambda s: s == 'true'),
180        ('supportsRules', lambda s: s == 'true')]:
181      if self.node.GetProperty(option_name):
182        if 'options' not in properties:
183          properties['options'] = {}
184        properties['options'][option_name] = sanitizer(self.node.GetProperty(
185          option_name))
186    is_function = False
187    parameter_comments = OrderedDict()
188    for node in self.node.GetChildren():
189      if node.cls == 'Comment':
190        (parent_comment, parameter_comments) = ProcessComment(node.GetName())
191        properties['description'] = parent_comment
192      elif node.cls == 'Callspec':
193        is_function = True
194        name, parameters, return_type = (Callspec(node, parameter_comments)
195                                         .process(callbacks))
196        properties['parameters'] = parameters
197        if return_type is not None:
198          properties['returns'] = return_type
199    properties['name'] = name
200    if is_function:
201      properties['type'] = 'function'
202    else:
203      properties = Typeref(self.node.GetProperty('TYPEREF'),
204                           self.node, properties).process(callbacks)
205    enum_values = self.node.GetProperty('legalValues')
206    if enum_values:
207      if properties['type'] == 'integer':
208        enum_values = map(int, enum_values)
209      elif properties['type'] == 'double':
210        enum_values = map(float, enum_values)
211      properties['enum'] = enum_values
212    return name, properties
213
214
215class Typeref(object):
216  '''
217  Given a TYPEREF property representing the type of dictionary member or
218  function parameter, converts into a Python dictionary that the JSON schema
219  compiler expects to see.
220  '''
221  def __init__(self, typeref, parent, additional_properties):
222    self.typeref = typeref
223    self.parent = parent
224    self.additional_properties = additional_properties
225
226  def process(self, callbacks):
227    properties = self.additional_properties
228    result = properties
229
230    if self.parent.GetPropertyLocal('OPTIONAL'):
231      properties['optional'] = True
232
233    # The IDL parser denotes array types by adding a child 'Array' node onto
234    # the Param node in the Callspec.
235    for sibling in self.parent.GetChildren():
236      if sibling.cls == 'Array' and sibling.GetName() == self.parent.GetName():
237        properties['type'] = 'array'
238        properties['items'] = OrderedDict()
239        properties = properties['items']
240        break
241
242    if self.typeref == 'DOMString':
243      properties['type'] = 'string'
244    elif self.typeref == 'boolean':
245      properties['type'] = 'boolean'
246    elif self.typeref == 'double':
247      properties['type'] = 'number'
248    elif self.typeref == 'long':
249      properties['type'] = 'integer'
250    elif self.typeref == 'any':
251      properties['type'] = 'any'
252    elif self.typeref == 'object':
253      properties['type'] = 'object'
254      if 'additionalProperties' not in properties:
255        properties['additionalProperties'] = OrderedDict()
256      properties['additionalProperties']['type'] = 'any'
257      instance_of = self.parent.GetProperty('instanceOf')
258      if instance_of:
259        properties['isInstanceOf'] = instance_of
260    elif self.typeref == 'ArrayBuffer':
261      properties['type'] = 'binary'
262      properties['isInstanceOf'] = 'ArrayBuffer'
263    elif self.typeref == 'FileEntry':
264      properties['type'] = 'object'
265      properties['isInstanceOf'] = 'FileEntry'
266      if 'additionalProperties' not in properties:
267        properties['additionalProperties'] = OrderedDict()
268      properties['additionalProperties']['type'] = 'any'
269    elif self.parent.GetPropertyLocal('Union'):
270      choices = []
271      properties['choices'] = [Typeref(node.GetProperty('TYPEREF'),
272                                       node,
273                                       OrderedDict()).process(callbacks)
274                               for node in self.parent.GetChildren()
275                               if node.cls == 'Option']
276    elif self.typeref is None:
277      properties['type'] = 'function'
278    else:
279      if self.typeref in callbacks:
280        # Do not override name and description if they are already specified.
281        name = properties.get('name', None)
282        description = properties.get('description', None)
283        properties.update(callbacks[self.typeref])
284        if description is not None:
285          properties['description'] = description
286        if name is not None:
287          properties['name'] = name
288      else:
289        properties['$ref'] = self.typeref
290    return result
291
292
293class Enum(object):
294  '''
295  Given an IDL Enum node, converts into a Python dictionary that the JSON
296  schema compiler expects to see.
297  '''
298  def __init__(self, enum_node):
299    self.node = enum_node
300    self.description = ''
301
302  def process(self, callbacks):
303    enum = []
304    for node in self.node.GetChildren():
305      if node.cls == 'EnumItem':
306        enum_value = {'name': node.GetName()}
307        for child in node.GetChildren():
308          if child.cls == 'Comment':
309            enum_value['description'] = ProcessComment(child.GetName())[0]
310          else:
311            raise ValueError('Did not process %s %s' % (child.cls, child))
312        enum.append(enum_value)
313      elif node.cls == 'Comment':
314        self.description = ProcessComment(node.GetName())[0]
315      else:
316        sys.exit('Did not process %s %s' % (node.cls, node))
317    result = {'id' : self.node.GetName(),
318              'description': self.description,
319              'type': 'string',
320              'enum': enum}
321    for property_name in (
322        'inline_doc', 'noinline_doc', 'nodoc', 'cpp_enum_prefix_override',):
323      if self.node.GetProperty(property_name):
324        result[property_name] = self.node.GetProperty(property_name)
325    if self.node.GetProperty('deprecated'):
326        result[deprecated] = self.node.GetProperty('deprecated')
327    return result
328
329
330class Namespace(object):
331  '''
332  Given an IDLNode representing an IDL namespace, converts into a Python
333  dictionary that the JSON schema compiler expects to see.
334  '''
335
336  def __init__(self,
337               namespace_node,
338               description,
339               nodoc=False,
340               internal=False,
341               platforms=None,
342               compiler_options=None,
343               deprecated=None):
344    self.namespace = namespace_node
345    self.nodoc = nodoc
346    self.internal = internal
347    self.platforms = platforms
348    self.compiler_options = compiler_options
349    self.events = []
350    self.functions = []
351    self.types = []
352    self.callbacks = OrderedDict()
353    self.description = description
354    self.deprecated = deprecated
355
356  def process(self):
357    for node in self.namespace.GetChildren():
358      if node.cls == 'Dictionary':
359        self.types.append(Dictionary(node).process(self.callbacks))
360      elif node.cls == 'Callback':
361        k, v = Member(node).process(self.callbacks)
362        self.callbacks[k] = v
363      elif node.cls == 'Interface' and node.GetName() == 'Functions':
364        self.functions = self.process_interface(node)
365      elif node.cls == 'Interface' and node.GetName() == 'Events':
366        self.events = self.process_interface(node)
367      elif node.cls == 'Enum':
368        self.types.append(Enum(node).process(self.callbacks))
369      else:
370        sys.exit('Did not process %s %s' % (node.cls, node))
371    if self.compiler_options is not None:
372      compiler_options = self.compiler_options
373    else:
374      compiler_options = {}
375    return {'namespace': self.namespace.GetName(),
376            'description': self.description,
377            'nodoc': self.nodoc,
378            'types': self.types,
379            'functions': self.functions,
380            'internal': self.internal,
381            'events': self.events,
382            'platforms': self.platforms,
383            'compiler_options': compiler_options,
384            'deprecated': self.deprecated}
385
386  def process_interface(self, node):
387    members = []
388    for member in node.GetChildren():
389      if member.cls == 'Member':
390        name, properties = Member(member).process(self.callbacks)
391        members.append(properties)
392    return members
393
394
395class IDLSchema(object):
396  '''
397  Given a list of IDLNodes and IDLAttributes, converts into a Python list
398  of api_defs that the JSON schema compiler expects to see.
399  '''
400
401  def __init__(self, idl):
402    self.idl = idl
403
404  def process(self):
405    namespaces = []
406    nodoc = False
407    internal = False
408    description = None
409    platforms = None
410    compiler_options = {}
411    deprecated = None
412    for node in self.idl:
413      if node.cls == 'Namespace':
414        if not description:
415          # TODO(kalman): Go back to throwing an error here.
416          print('%s must have a namespace-level comment. This will '
417                           'appear on the API summary page.' % node.GetName())
418          description = ''
419        namespace = Namespace(node, description, nodoc, internal,
420                              platforms=platforms,
421                              compiler_options=compiler_options or None,
422                              deprecated=deprecated)
423        namespaces.append(namespace.process())
424        nodoc = False
425        internal = False
426        platforms = None
427        compiler_options = None
428      elif node.cls == 'Copyright':
429        continue
430      elif node.cls == 'Comment':
431        description = node.GetName()
432      elif node.cls == 'ExtAttribute':
433        if node.name == 'nodoc':
434          nodoc = bool(node.value)
435        elif node.name == 'internal':
436          internal = bool(node.value)
437        elif node.name == 'platforms':
438          platforms = list(node.value)
439        elif node.name == 'implemented_in':
440          compiler_options['implemented_in'] = node.value
441        elif node.name == 'camel_case_enum_to_string':
442          compiler_options['camel_case_enum_to_string'] = node.value
443        elif node.name == 'deprecated':
444          deprecated = str(node.value)
445        else:
446          continue
447      else:
448        sys.exit('Did not process %s %s' % (node.cls, node))
449    return namespaces
450
451
452def Load(filename):
453  '''
454  Given the filename of an IDL file, parses it and returns an equivalent
455  Python dictionary in a format that the JSON schema compiler expects to see.
456  '''
457
458  f = open(filename, 'r')
459  contents = f.read()
460  f.close()
461
462  idl = idl_parser.IDLParser().ParseData(contents, filename)
463  idl_schema = IDLSchema(idl)
464  return idl_schema.process()
465
466
467def Main():
468  '''
469  Dump a json serialization of parse result for the IDL files whose names
470  were passed in on the command line.
471  '''
472  if len(sys.argv) > 1:
473    for filename in sys.argv[1:]:
474      schema = Load(filename)
475      print json.dumps(schema, indent=2)
476  else:
477    contents = sys.stdin.read()
478    idl = idl_parser.IDLParser().ParseData(contents, '<stdin>')
479    schema = IDLSchema(idl).process()
480    print json.dumps(schema, indent=2)
481
482
483if __name__ == '__main__':
484  Main()
485