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