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 import 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')
     26 if _idl_generators_path in sys.path:
     27   import idl_parser
     28 else:
     29   sys.path.insert(0, _idl_generators_path)
     30   try:
     31     import idl_parser
     32   finally:
     33     sys.path.pop(0)
     34 
     35 def 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 
     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,
    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 
    117 class 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 
    130 class 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 
    154 class 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 
    207 class 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 
    278 class 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 
    306 class 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 
    353 class 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 
    393 def 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 
    407 def 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 
    416 if __name__ == '__main__':
    417   Main()
    418