Upstream version 6.35.121.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   # 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
88 class 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
118 class 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
132 class 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
159 class 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
215 class 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
293 class 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_omit_enum_type',):
323       if self.node.GetProperty(property_name):
324         result[property_name] = True
325     if self.node.GetProperty('deprecated'):
326         result[deprecated] = self.node.GetProperty('deprecated')
327     return result
328
329
330 class 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
395 class 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 = None
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,
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 == 'deprecated':
442           deprecated = str(node.value)
443         else:
444           continue
445       else:
446         sys.exit('Did not process %s %s' % (node.cls, node))
447     return namespaces
448
449
450 def Load(filename):
451   '''
452   Given the filename of an IDL file, parses it and returns an equivalent
453   Python dictionary in a format that the JSON schema compiler expects to see.
454   '''
455
456   f = open(filename, 'r')
457   contents = f.read()
458   f.close()
459
460   idl = idl_parser.IDLParser().ParseData(contents, filename)
461   idl_schema = IDLSchema(idl)
462   return idl_schema.process()
463
464
465 def Main():
466   '''
467   Dump a json serialization of parse result for the IDL files whose names
468   were passed in on the command line.
469   '''
470   for filename in sys.argv[1:]:
471     schema = Load(filename)
472     print json.dumps(schema, indent=2)
473
474
475 if __name__ == '__main__':
476   Main()