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  def add_paragraphs(content):
60    paragraphs = content.split('\n\n')
61    if len(paragraphs) < 2:
62      return content
63    return '<p>' + '</p><p>'.join(p.strip() for p in paragraphs) + '</p>'
64
65  # Find all the parameter comments of the form '|name|: comment'.
66  parameter_starts = list(re.finditer(r' *\|([^|]*)\| *: *', comment))
67
68  # Get the parent comment (everything before the first parameter comment.
69  first_parameter_location = (parameter_starts[0].start()
70                              if parameter_starts else len(comment))
71  parent_comment = (add_paragraphs(comment[:first_parameter_location].strip())
72                    .replace('\n', ''))
73
74  params = OrderedDict()
75  for (cur_param, next_param) in itertools.izip_longest(parameter_starts,
76                                                        parameter_starts[1:]):
77    param_name = cur_param.group(1)
78
79    # A parameter's comment goes from the end of its introduction to the
80    # beginning of the next parameter's introduction.
81    param_comment_start = cur_param.end()
82    param_comment_end = next_param.start() if next_param else len(comment)
83    params[param_name] = (
84        add_paragraphs(comment[param_comment_start:param_comment_end].strip())
85        .replace('\n', ''))
86
87  return (parent_comment, params)
88
89
90class Callspec(object):
91  '''
92  Given a Callspec node representing an IDL function declaration, converts into
93  a tuple:
94      (name, list of function parameters, return type)
95  '''
96  def __init__(self, callspec_node, comment):
97    self.node = callspec_node
98    self.comment = comment
99
100  def process(self, callbacks):
101    parameters = []
102    return_type = None
103    if self.node.GetProperty('TYPEREF') not in ('void', None):
104      return_type = Typeref(self.node.GetProperty('TYPEREF'),
105                            self.node.parent,
106                            {'name': self.node.GetName()}).process(callbacks)
107      # The IDL parser doesn't allow specifying return types as optional.
108      # Instead we infer any object return values to be optional.
109      # TODO(asargent): fix the IDL parser to support optional return types.
110      if return_type.get('type') == 'object' or '$ref' in return_type:
111        return_type['optional'] = True
112    for node in self.node.GetChildren():
113      parameter = Param(node).process(callbacks)
114      if parameter['name'] in self.comment:
115        parameter['description'] = self.comment[parameter['name']]
116      parameters.append(parameter)
117    return (self.node.GetName(), parameters, return_type)
118
119
120class Param(object):
121  '''
122  Given a Param node representing a function parameter, converts into a Python
123  dictionary that the JSON schema compiler expects to see.
124  '''
125  def __init__(self, param_node):
126    self.node = param_node
127
128  def process(self, callbacks):
129    return Typeref(self.node.GetProperty('TYPEREF'),
130                   self.node,
131                   {'name': self.node.GetName()}).process(callbacks)
132
133
134class Dictionary(object):
135  '''
136  Given an IDL Dictionary node, converts into a Python dictionary that the JSON
137  schema compiler expects to see.
138  '''
139  def __init__(self, dictionary_node):
140    self.node = dictionary_node
141
142  def process(self, callbacks):
143    properties = OrderedDict()
144    for node in self.node.GetChildren():
145      if node.cls == 'Member':
146        k, v = Member(node).process(callbacks)
147        properties[k] = v
148    result = {'id': self.node.GetName(),
149              'properties': properties,
150              'type': 'object'}
151    if self.node.GetProperty('nodoc'):
152      result['nodoc'] = True
153    elif self.node.GetProperty('inline_doc'):
154      result['inline_doc'] = True
155    elif self.node.GetProperty('noinline_doc'):
156      result['noinline_doc'] = True
157    return result
158
159
160
161class Member(object):
162  '''
163  Given an IDL dictionary or interface member, converts into a name/value pair
164  where the value is a Python dictionary that the JSON schema compiler expects
165  to see.
166  '''
167  def __init__(self, member_node):
168    self.node = member_node
169
170  def process(self, callbacks):
171    properties = OrderedDict()
172    name = self.node.GetName()
173    if self.node.GetProperty('deprecated'):
174      properties['deprecated'] = self.node.GetProperty('deprecated')
175    if self.node.GetProperty('allowAmbiguousOptionalArguments'):
176      properties['allowAmbiguousOptionalArguments'] = True
177    for property_name in ('OPTIONAL', 'nodoc', 'nocompile', 'nodart'):
178      if self.node.GetProperty(property_name):
179        properties[property_name.lower()] = True
180    for option_name, sanitizer in [
181        ('maxListeners', int),
182        ('supportsFilters', lambda s: s == 'true'),
183        ('supportsListeners', lambda s: s == 'true'),
184        ('supportsRules', lambda s: s == 'true')]:
185      if self.node.GetProperty(option_name):
186        if 'options' not in properties:
187          properties['options'] = {}
188        properties['options'][option_name] = sanitizer(self.node.GetProperty(
189          option_name))
190    is_function = False
191    parameter_comments = OrderedDict()
192    for node in self.node.GetChildren():
193      if node.cls == 'Comment':
194        (parent_comment, parameter_comments) = ProcessComment(node.GetName())
195        properties['description'] = parent_comment
196      elif node.cls == 'Callspec':
197        is_function = True
198        name, parameters, return_type = (Callspec(node, parameter_comments)
199                                         .process(callbacks))
200        properties['parameters'] = parameters
201        if return_type is not None:
202          properties['returns'] = return_type
203    properties['name'] = name
204    if is_function:
205      properties['type'] = 'function'
206    else:
207      properties = Typeref(self.node.GetProperty('TYPEREF'),
208                           self.node, properties).process(callbacks)
209    enum_values = self.node.GetProperty('legalValues')
210    if enum_values:
211      if properties['type'] == 'integer':
212        enum_values = map(int, enum_values)
213      elif properties['type'] == 'double':
214        enum_values = map(float, enum_values)
215      properties['enum'] = enum_values
216    return name, properties
217
218
219class Typeref(object):
220  '''
221  Given a TYPEREF property representing the type of dictionary member or
222  function parameter, converts into a Python dictionary that the JSON schema
223  compiler expects to see.
224  '''
225  def __init__(self, typeref, parent, additional_properties):
226    self.typeref = typeref
227    self.parent = parent
228    self.additional_properties = additional_properties
229
230  def process(self, callbacks):
231    properties = self.additional_properties
232    result = properties
233
234    if self.parent.GetPropertyLocal('OPTIONAL'):
235      properties['optional'] = True
236
237    # The IDL parser denotes array types by adding a child 'Array' node onto
238    # the Param node in the Callspec.
239    for sibling in self.parent.GetChildren():
240      if sibling.cls == 'Array' and sibling.GetName() == self.parent.GetName():
241        properties['type'] = 'array'
242        properties['items'] = OrderedDict()
243        properties = properties['items']
244        break
245
246    if self.typeref == 'DOMString':
247      properties['type'] = 'string'
248    elif self.typeref == 'boolean':
249      properties['type'] = 'boolean'
250    elif self.typeref == 'double':
251      properties['type'] = 'number'
252    elif self.typeref == 'long':
253      properties['type'] = 'integer'
254    elif self.typeref == 'any':
255      properties['type'] = 'any'
256    elif self.typeref == 'object':
257      properties['type'] = 'object'
258      if 'additionalProperties' not in properties:
259        properties['additionalProperties'] = OrderedDict()
260      properties['additionalProperties']['type'] = 'any'
261      instance_of = self.parent.GetProperty('instanceOf')
262      if instance_of:
263        properties['isInstanceOf'] = instance_of
264    elif self.typeref == 'ArrayBuffer':
265      properties['type'] = 'binary'
266      properties['isInstanceOf'] = 'ArrayBuffer'
267    elif self.typeref == 'FileEntry':
268      properties['type'] = 'object'
269      properties['isInstanceOf'] = 'FileEntry'
270      if 'additionalProperties' not in properties:
271        properties['additionalProperties'] = OrderedDict()
272      properties['additionalProperties']['type'] = 'any'
273    elif self.parent.GetPropertyLocal('Union'):
274      choices = []
275      properties['choices'] = [Typeref(node.GetProperty('TYPEREF'),
276                                       node,
277                                       OrderedDict()).process(callbacks)
278                               for node in self.parent.GetChildren()
279                               if node.cls == 'Option']
280    elif self.typeref is None:
281      properties['type'] = 'function'
282    else:
283      if self.typeref in callbacks:
284        # Do not override name and description if they are already specified.
285        name = properties.get('name', None)
286        description = properties.get('description', None)
287        properties.update(callbacks[self.typeref])
288        if description is not None:
289          properties['description'] = description
290        if name is not None:
291          properties['name'] = name
292      else:
293        properties['$ref'] = self.typeref
294    return result
295
296
297class Enum(object):
298  '''
299  Given an IDL Enum node, converts into a Python dictionary that the JSON
300  schema compiler expects to see.
301  '''
302  def __init__(self, enum_node):
303    self.node = enum_node
304    self.description = ''
305
306  def process(self, callbacks):
307    enum = []
308    for node in self.node.GetChildren():
309      if node.cls == 'EnumItem':
310        enum_value = {'name': node.GetName()}
311        for child in node.GetChildren():
312          if child.cls == 'Comment':
313            enum_value['description'] = ProcessComment(child.GetName())[0]
314          else:
315            raise ValueError('Did not process %s %s' % (child.cls, child))
316        enum.append(enum_value)
317      elif node.cls == 'Comment':
318        self.description = ProcessComment(node.GetName())[0]
319      else:
320        sys.exit('Did not process %s %s' % (node.cls, node))
321    result = {'id' : self.node.GetName(),
322              'description': self.description,
323              'type': 'string',
324              'enum': enum}
325    for property_name in (
326        'inline_doc', 'noinline_doc', 'nodoc', 'cpp_enum_prefix_override',):
327      if self.node.GetProperty(property_name):
328        result[property_name] = self.node.GetProperty(property_name)
329    if self.node.GetProperty('deprecated'):
330        result[deprecated] = self.node.GetProperty('deprecated')
331    return result
332
333
334class Namespace(object):
335  '''
336  Given an IDLNode representing an IDL namespace, converts into a Python
337  dictionary that the JSON schema compiler expects to see.
338  '''
339
340  def __init__(self,
341               namespace_node,
342               description,
343               nodoc=False,
344               internal=False,
345               platforms=None,
346               compiler_options=None,
347               deprecated=None):
348    self.namespace = namespace_node
349    self.nodoc = nodoc
350    self.internal = internal
351    self.platforms = platforms
352    self.compiler_options = compiler_options
353    self.events = []
354    self.functions = []
355    self.types = []
356    self.callbacks = OrderedDict()
357    self.description = description
358    self.deprecated = deprecated
359
360  def process(self):
361    for node in self.namespace.GetChildren():
362      if node.cls == 'Dictionary':
363        self.types.append(Dictionary(node).process(self.callbacks))
364      elif node.cls == 'Callback':
365        k, v = Member(node).process(self.callbacks)
366        self.callbacks[k] = v
367      elif node.cls == 'Interface' and node.GetName() == 'Functions':
368        self.functions = self.process_interface(node)
369      elif node.cls == 'Interface' and node.GetName() == 'Events':
370        self.events = self.process_interface(node)
371      elif node.cls == 'Enum':
372        self.types.append(Enum(node).process(self.callbacks))
373      else:
374        sys.exit('Did not process %s %s' % (node.cls, node))
375    if self.compiler_options is not None:
376      compiler_options = self.compiler_options
377    else:
378      compiler_options = {}
379    return {'namespace': self.namespace.GetName(),
380            'description': self.description,
381            'nodoc': self.nodoc,
382            'types': self.types,
383            'functions': self.functions,
384            'internal': self.internal,
385            'events': self.events,
386            'platforms': self.platforms,
387            'compiler_options': compiler_options,
388            'deprecated': self.deprecated}
389
390  def process_interface(self, node):
391    members = []
392    for member in node.GetChildren():
393      if member.cls == 'Member':
394        name, properties = Member(member).process(self.callbacks)
395        members.append(properties)
396    return members
397
398
399class IDLSchema(object):
400  '''
401  Given a list of IDLNodes and IDLAttributes, converts into a Python list
402  of api_defs that the JSON schema compiler expects to see.
403  '''
404
405  def __init__(self, idl):
406    self.idl = idl
407
408  def process(self):
409    namespaces = []
410    nodoc = False
411    internal = False
412    description = None
413    platforms = None
414    compiler_options = {}
415    deprecated = None
416    for node in self.idl:
417      if node.cls == 'Namespace':
418        if not description:
419          # TODO(kalman): Go back to throwing an error here.
420          print('%s must have a namespace-level comment. This will '
421                           'appear on the API summary page.' % node.GetName())
422          description = ''
423        namespace = Namespace(node, description, nodoc, internal,
424                              platforms=platforms,
425                              compiler_options=compiler_options or None,
426                              deprecated=deprecated)
427        namespaces.append(namespace.process())
428        nodoc = False
429        internal = False
430        platforms = None
431        compiler_options = None
432      elif node.cls == 'Copyright':
433        continue
434      elif node.cls == 'Comment':
435        description = node.GetName()
436      elif node.cls == 'ExtAttribute':
437        if node.name == 'nodoc':
438          nodoc = bool(node.value)
439        elif node.name == 'internal':
440          internal = bool(node.value)
441        elif node.name == 'platforms':
442          platforms = list(node.value)
443        elif node.name == 'implemented_in':
444          compiler_options['implemented_in'] = node.value
445        elif node.name == 'camel_case_enum_to_string':
446          compiler_options['camel_case_enum_to_string'] = node.value
447        elif node.name == 'deprecated':
448          deprecated = str(node.value)
449        else:
450          continue
451      else:
452        sys.exit('Did not process %s %s' % (node.cls, node))
453    return namespaces
454
455
456def Load(filename):
457  '''
458  Given the filename of an IDL file, parses it and returns an equivalent
459  Python dictionary in a format that the JSON schema compiler expects to see.
460  '''
461
462  f = open(filename, 'r')
463  contents = f.read()
464  f.close()
465
466  idl = idl_parser.IDLParser().ParseData(contents, filename)
467  idl_schema = IDLSchema(idl)
468  return idl_schema.process()
469
470
471def Main():
472  '''
473  Dump a json serialization of parse result for the IDL files whose names
474  were passed in on the command line.
475  '''
476  if len(sys.argv) > 1:
477    for filename in sys.argv[1:]:
478      schema = Load(filename)
479      print json.dumps(schema, indent=2)
480  else:
481    contents = sys.stdin.read()
482    idl = idl_parser.IDLParser().ParseData(contents, '<stdin>')
483    schema = IDLSchema(idl).process()
484    print json.dumps(schema, indent=2)
485
486
487if __name__ == '__main__':
488  Main()
489