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