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