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