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_enum_prefix_override',): 323 if self.node.GetProperty(property_name): 324 result[property_name] = self.node.GetProperty(property_name) 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 = {} 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 or None, 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 == 'camel_case_enum_to_string': 442 compiler_options['camel_case_enum_to_string'] = node.value 443 elif node.name == 'deprecated': 444 deprecated = str(node.value) 445 else: 446 continue 447 else: 448 sys.exit('Did not process %s %s' % (node.cls, node)) 449 return namespaces 450 451 452 def Load(filename): 453 ''' 454 Given the filename of an IDL file, parses it and returns an equivalent 455 Python dictionary in a format that the JSON schema compiler expects to see. 456 ''' 457 458 f = open(filename, 'r') 459 contents = f.read() 460 f.close() 461 462 idl = idl_parser.IDLParser().ParseData(contents, filename) 463 idl_schema = IDLSchema(idl) 464 return idl_schema.process() 465 466 467 def Main(): 468 ''' 469 Dump a json serialization of parse result for the IDL files whose names 470 were passed in on the command line. 471 ''' 472 if len(sys.argv) > 1: 473 for filename in sys.argv[1:]: 474 schema = Load(filename) 475 print json.dumps(schema, indent=2) 476 else: 477 contents = sys.stdin.read() 478 idl = idl_parser.IDLParser().ParseData(contents, '<stdin>') 479 schema = IDLSchema(idl).process() 480 print json.dumps(schema, indent=2) 481 482 483 if __name__ == '__main__': 484 Main() 485