Fix a type mismatch on Windows caused by r201738.
[chromium-blink-merge.git] / tools / json_schema_compiler / idl_schema.py
blob4c79c30efaa159b17bc41951407e6ee624f83c9c
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.
6 import itertools
7 import json
8 import os.path
9 import re
10 import sys
12 from json_parse import OrderedDict
13 import schema_util
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.
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)
35 def ProcessComment(comment):
36 '''
37 Convert a comment into a parent comment and a list of parameter comments.
39 Function comments are of the form:
40 Function documentation. May contain HTML and multiple lines.
42 |arg1_name|: Description of arg1. Use <var>argument</var> to refer
43 to other arguments.
44 |arg2_name|: Description of arg2...
46 Newlines are removed, and leading and trailing whitespace is stripped.
48 Args:
49 comment: The string from a Comment node.
51 Returns: A tuple that looks like:
53 "The processed comment, minus all |parameter| mentions.",
55 'parameter_name_1': "The comment that followed |parameter_name_1|:",
56 ...
59 '''
60 # Find all the parameter comments of the form '|name|: comment'.
61 parameter_starts = list(re.finditer(r' *\|([^|]*)\| *: *', comment))
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]
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', ''))
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)
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)
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
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)
117 class Param(object):
119 Given a Param node representing a function parameter, converts into a Python
120 dictionary that the JSON schema compiler expects to see.
122 def __init__(self, param_node):
123 self.node = param_node
125 def process(self, callbacks):
126 return Typeref(self.node.GetProperty('TYPEREF'),
127 self.node,
128 {'name': self.node.GetName()}).process(callbacks)
130 class Dictionary(object):
132 Given an IDL Dictionary node, converts into a Python dictionary that the JSON
133 schema compiler expects to see.
135 def __init__(self, dictionary_node):
136 self.node = dictionary_node
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 return result
152 class Member(object):
154 Given an IDL dictionary or interface member, converts into a name/value pair
155 where the value is a Python dictionary that the JSON schema compiler expects
156 to see.
158 def __init__(self, member_node):
159 self.node = member_node
161 def process(self, callbacks):
162 properties = OrderedDict()
163 name = self.node.GetName()
164 for property_name in ('OPTIONAL', 'nodoc', 'nocompile', 'nodart'):
165 if self.node.GetProperty(property_name):
166 properties[property_name.lower()] = True
167 for option_name, sanitizer in [
168 ('maxListeners', int),
169 ('supportsFilters', lambda s: s == 'true'),
170 ('supportsListeners', lambda s: s == 'true'),
171 ('supportsRules', lambda s: s == 'true')]:
172 if self.node.GetProperty(option_name):
173 if 'options' not in properties:
174 properties['options'] = {}
175 properties['options'][option_name] = sanitizer(self.node.GetProperty(
176 option_name))
177 is_function = False
178 parameter_comments = OrderedDict()
179 for node in self.node.children:
180 if node.cls == 'Comment':
181 (parent_comment, parameter_comments) = ProcessComment(node.GetName())
182 properties['description'] = parent_comment
183 elif node.cls == 'Callspec':
184 is_function = True
185 name, parameters, return_type = (Callspec(node, parameter_comments)
186 .process(callbacks))
187 properties['parameters'] = parameters
188 if return_type is not None:
189 properties['returns'] = return_type
190 properties['name'] = name
191 if is_function:
192 properties['type'] = 'function'
193 else:
194 properties = Typeref(self.node.GetProperty('TYPEREF'),
195 self.node, properties).process(callbacks)
196 enum_values = self.node.GetProperty('legalValues')
197 if enum_values:
198 if properties['type'] == 'integer':
199 enum_values = map(int, enum_values)
200 elif properties['type'] == 'double':
201 enum_values = map(float, enum_values)
202 properties['enum'] = enum_values
203 return name, properties
205 class Typeref(object):
207 Given a TYPEREF property representing the type of dictionary member or
208 function parameter, converts into a Python dictionary that the JSON schema
209 compiler expects to see.
211 def __init__(self, typeref, parent, additional_properties=OrderedDict()):
212 self.typeref = typeref
213 self.parent = parent
214 self.additional_properties = additional_properties
216 def process(self, callbacks):
217 properties = self.additional_properties
218 result = properties
220 if self.parent.GetProperty('OPTIONAL', False):
221 properties['optional'] = True
223 # The IDL parser denotes array types by adding a child 'Array' node onto
224 # the Param node in the Callspec.
225 for sibling in self.parent.GetChildren():
226 if sibling.cls == 'Array' and sibling.GetName() == self.parent.GetName():
227 properties['type'] = 'array'
228 properties['items'] = OrderedDict()
229 properties = properties['items']
230 break
232 if self.typeref == 'DOMString':
233 properties['type'] = 'string'
234 elif self.typeref == 'boolean':
235 properties['type'] = 'boolean'
236 elif self.typeref == 'double':
237 properties['type'] = 'number'
238 elif self.typeref == 'long':
239 properties['type'] = 'integer'
240 elif self.typeref == 'any':
241 properties['type'] = 'any'
242 elif self.typeref == 'object':
243 properties['type'] = 'object'
244 if 'additionalProperties' not in properties:
245 properties['additionalProperties'] = OrderedDict()
246 properties['additionalProperties']['type'] = 'any'
247 instance_of = self.parent.GetProperty('instanceOf')
248 if instance_of:
249 properties['isInstanceOf'] = instance_of
250 elif self.typeref == 'ArrayBuffer':
251 properties['type'] = 'binary'
252 properties['isInstanceOf'] = 'ArrayBuffer'
253 elif self.typeref == 'FileEntry':
254 properties['type'] = 'object'
255 properties['isInstanceOf'] = 'FileEntry'
256 if 'additionalProperties' not in properties:
257 properties['additionalProperties'] = OrderedDict()
258 properties['additionalProperties']['type'] = 'any'
259 elif self.typeref is None:
260 properties['type'] = 'function'
261 else:
262 if self.typeref in callbacks:
263 # Do not override name and description if they are already specified.
264 name = properties.get('name', None)
265 description = properties.get('description', None)
266 properties.update(callbacks[self.typeref])
267 if description is not None:
268 properties['description'] = description
269 if name is not None:
270 properties['name'] = name
271 else:
272 properties['$ref'] = self.typeref
273 return result
276 class Enum(object):
278 Given an IDL Enum node, converts into a Python dictionary that the JSON
279 schema compiler expects to see.
281 def __init__(self, enum_node):
282 self.node = enum_node
283 self.description = ''
285 def process(self, callbacks):
286 enum = []
287 for node in self.node.children:
288 if node.cls == 'EnumItem':
289 enum.append(node.GetName())
290 elif node.cls == 'Comment':
291 self.description = ProcessComment(node.GetName())[0]
292 else:
293 sys.exit('Did not process %s %s' % (node.cls, node))
294 result = {'id' : self.node.GetName(),
295 'description': self.description,
296 'type': 'string',
297 'enum': enum}
298 for property_name in ('inline_doc', 'nodoc'):
299 if self.node.GetProperty(property_name):
300 result[property_name] = True
301 return result
304 class Namespace(object):
306 Given an IDLNode representing an IDL namespace, converts into a Python
307 dictionary that the JSON schema compiler expects to see.
310 def __init__(self, namespace_node, nodoc=False, permissions=None,
311 internal=False):
312 self.namespace = namespace_node
313 self.nodoc = nodoc
314 self.internal = internal
315 self.events = []
316 self.functions = []
317 self.types = []
318 self.callbacks = OrderedDict()
319 self.permissions = permissions or []
321 def process(self):
322 for node in self.namespace.children:
323 if node.cls == 'Dictionary':
324 self.types.append(Dictionary(node).process(self.callbacks))
325 elif node.cls == 'Callback':
326 k, v = Member(node).process(self.callbacks)
327 self.callbacks[k] = v
328 elif node.cls == 'Interface' and node.GetName() == 'Functions':
329 self.functions = self.process_interface(node)
330 elif node.cls == 'Interface' and node.GetName() == 'Events':
331 self.events = self.process_interface(node)
332 elif node.cls == 'Enum':
333 self.types.append(Enum(node).process(self.callbacks))
334 else:
335 sys.exit('Did not process %s %s' % (node.cls, node))
336 return {'namespace': self.namespace.GetName(),
337 'nodoc': self.nodoc,
338 'documentation_permissions_required': self.permissions,
339 'types': self.types,
340 'functions': self.functions,
341 'internal': self.internal,
342 'events': self.events}
344 def process_interface(self, node):
345 members = []
346 for member in node.children:
347 if member.cls == 'Member':
348 name, properties = Member(member).process(self.callbacks)
349 members.append(properties)
350 return members
352 class IDLSchema(object):
354 Given a list of IDLNodes and IDLAttributes, converts into a Python list
355 of api_defs that the JSON schema compiler expects to see.
358 def __init__(self, idl):
359 self.idl = idl
361 def process(self):
362 namespaces = []
363 nodoc = False
364 internal = False
365 permissions = None
366 for node in self.idl:
367 if node.cls == 'Namespace':
368 namespace = Namespace(node, nodoc, permissions, internal)
369 namespaces.append(namespace.process())
370 nodoc = False
371 internal = False
372 elif node.cls == 'Copyright':
373 continue
374 elif node.cls == 'Comment':
375 continue
376 elif node.cls == 'ExtAttribute':
377 if node.name == 'nodoc':
378 nodoc = bool(node.value)
379 elif node.name == 'permissions':
380 permission = node.value.split(',')
381 elif node.name == 'internal':
382 internal = bool(node.value)
383 else:
384 continue
385 else:
386 sys.exit('Did not process %s %s' % (node.cls, node))
387 return namespaces
389 def Load(filename):
391 Given the filename of an IDL file, parses it and returns an equivalent
392 Python dictionary in a format that the JSON schema compiler expects to see.
395 f = open(filename, 'r')
396 contents = f.read()
397 f.close()
399 idl = idl_parser.IDLParser().ParseData(contents, filename)
400 idl_schema = IDLSchema(idl)
401 return idl_schema.process()
403 def Main():
405 Dump a json serialization of parse result for the IDL files whose names
406 were passed in on the command line.
408 for filename in sys.argv[1:]:
409 schema = Load(filename)
410 print json.dumps(schema, indent=2)
412 if __name__ == '__main__':
413 Main()