Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / tools / json_schema_compiler / idl_schema.py
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
6 import itertools
7 import json
8 import os.path
9 import re
10 import sys
11
12 from 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')
25 if _idl_generators_path in sys.path:
26   import idl_parser
27 else:
28   sys.path.insert(0, _idl_generators_path)
29   try:
30     import idl_parser
31   finally:
32     sys.path.pop(0)
33
34 def 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
90 class 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
120 class 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
134 class 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
161 class 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
219 class 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
297 class 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
334 class 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
399 class 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
456 def 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
471 def 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
487 if __name__ == '__main__':
488   Main()