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.
13 from json_parse
import OrderedDict
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
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
:
29 sys
.path
.insert(0, _idl_generators_path
)
35 def ProcessComment(comment
):
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
44 |arg2_name|: Description of arg2...
46 Newlines are removed, and leading and trailing whitespace is stripped.
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|:",
60 def add_paragraphs(content
):
61 paragraphs
= content
.split('\n\n')
62 if len(paragraphs
) < 2:
64 return '<p>' + '</p><p>'.join(p
.strip() for p
in paragraphs
) + '</p>'
66 # Find all the parameter comments of the form '|name|: comment'.
67 parameter_starts
= list(re
.finditer(r
' *\|([^|]*)\| *: *', comment
))
69 # Get the parent comment (everything before the first parameter comment.
70 first_parameter_location
= (parameter_starts
[0].start()
71 if parameter_starts
else len(comment
))
72 parent_comment
= (add_paragraphs(comment
[:first_parameter_location
].strip())
75 params
= OrderedDict()
76 for (cur_param
, next_param
) in itertools
.izip_longest(parameter_starts
,
77 parameter_starts
[1:]):
78 param_name
= cur_param
.group(1)
80 # A parameter's comment goes from the end of its introduction to the
81 # beginning of the next parameter's introduction.
82 param_comment_start
= cur_param
.end()
83 param_comment_end
= next_param
.start() if next_param
else len(comment
)
84 params
[param_name
] = (
85 add_paragraphs(comment
[param_comment_start
:param_comment_end
].strip())
88 return (parent_comment
, params
)
91 class Callspec(object):
93 Given a Callspec node representing an IDL function declaration, converts into
95 (name, list of function parameters, return type)
97 def __init__(self
, callspec_node
, comment
):
98 self
.node
= callspec_node
99 self
.comment
= comment
101 def process(self
, callbacks
):
104 if self
.node
.GetProperty('TYPEREF') not in ('void', None):
105 return_type
= Typeref(self
.node
.GetProperty('TYPEREF'),
107 {'name': self
.node
.GetName()}).process(callbacks
)
108 # The IDL parser doesn't allow specifying return types as optional.
109 # Instead we infer any object return values to be optional.
110 # TODO(asargent): fix the IDL parser to support optional return types.
111 if return_type
.get('type') == 'object' or '$ref' in return_type
:
112 return_type
['optional'] = True
113 for node
in self
.node
.GetChildren():
114 parameter
= Param(node
).process(callbacks
)
115 if parameter
['name'] in self
.comment
:
116 parameter
['description'] = self
.comment
[parameter
['name']]
117 parameters
.append(parameter
)
118 return (self
.node
.GetName(), parameters
, return_type
)
123 Given a Param node representing a function parameter, converts into a Python
124 dictionary that the JSON schema compiler expects to see.
126 def __init__(self
, param_node
):
127 self
.node
= param_node
129 def process(self
, callbacks
):
130 return Typeref(self
.node
.GetProperty('TYPEREF'),
132 {'name': self
.node
.GetName()}).process(callbacks
)
135 class Dictionary(object):
137 Given an IDL Dictionary node, converts into a Python dictionary that the JSON
138 schema compiler expects to see.
140 def __init__(self
, dictionary_node
):
141 self
.node
= dictionary_node
143 def process(self
, callbacks
):
144 properties
= OrderedDict()
145 for node
in self
.node
.GetChildren():
146 if node
.cls
== 'Member':
147 k
, v
= Member(node
).process(callbacks
)
149 result
= {'id': self
.node
.GetName(),
150 'properties': properties
,
152 if self
.node
.GetProperty('nodoc'):
153 result
['nodoc'] = True
154 elif self
.node
.GetProperty('inline_doc'):
155 result
['inline_doc'] = True
156 elif self
.node
.GetProperty('noinline_doc'):
157 result
['noinline_doc'] = True
162 class Member(object):
164 Given an IDL dictionary or interface member, converts into a name/value pair
165 where the value is a Python dictionary that the JSON schema compiler expects
168 def __init__(self
, member_node
):
169 self
.node
= member_node
171 def process(self
, callbacks
, functions_are_properties
=False):
172 properties
= OrderedDict()
173 name
= self
.node
.GetName()
174 if self
.node
.GetProperty('deprecated'):
175 properties
['deprecated'] = self
.node
.GetProperty('deprecated')
176 if self
.node
.GetProperty('allowAmbiguousOptionalArguments'):
177 properties
['allowAmbiguousOptionalArguments'] = True
178 for property_name
in ('OPTIONAL', 'nodoc', 'nocompile', 'nodart'):
179 if self
.node
.GetProperty(property_name
):
180 properties
[property_name
.lower()] = True
181 for option_name
, sanitizer
in [
182 ('maxListeners', int),
183 ('supportsFilters', lambda s
: s
== 'true'),
184 ('supportsListeners', lambda s
: s
== 'true'),
185 ('supportsRules', lambda s
: s
== 'true')]:
186 if self
.node
.GetProperty(option_name
):
187 if 'options' not in properties
:
188 properties
['options'] = {}
189 properties
['options'][option_name
] = sanitizer(self
.node
.GetProperty(
192 parameter_comments
= OrderedDict()
193 for node
in self
.node
.GetChildren():
194 if node
.cls
== 'Comment':
195 (parent_comment
, parameter_comments
) = ProcessComment(node
.GetName())
196 properties
['description'] = parent_comment
197 elif node
.cls
== 'Callspec':
198 name
, parameters
, return_type
= (Callspec(node
, parameter_comments
)
200 if functions_are_properties
:
201 # If functions are treated as properties (which will happen if the
202 # interface is named Properties) then this isn't a function, it's a
203 # property which is encoded as a function with no arguments. The
204 # property type is the return type. This is an egregious hack in lieu
205 # of the IDL parser supporting 'const'.
206 assert parameters
== [], (
207 'Property "%s" must be no-argument functions '
208 'with a non-void return type' % name
)
209 assert return_type
is not None, (
210 'Property "%s" must be no-argument functions '
211 'with a non-void return type' % name
)
212 assert 'type' in return_type
, (
213 'Property return type "%s" from "%s" must specify a '
214 'fundamental IDL type.' % (pprint
.pformat(return_type
), name
))
215 type_override
= return_type
['type']
217 type_override
= 'function'
218 properties
['parameters'] = parameters
219 if return_type
is not None:
220 properties
['returns'] = return_type
221 properties
['name'] = name
222 if type_override
is not None:
223 properties
['type'] = type_override
225 properties
= Typeref(self
.node
.GetProperty('TYPEREF'),
226 self
.node
, properties
).process(callbacks
)
227 value
= self
.node
.GetProperty('value')
228 if value
is not None:
229 # IDL always returns values as strings, so cast to their real type.
230 properties
['value'] = self
.cast_from_json_type(properties
['type'], value
)
231 enum_values
= self
.node
.GetProperty('legalValues')
233 # IDL always returns enum values as strings, so cast to their real type.
234 properties
['enum'] = [self
.cast_from_json_type(properties
['type'], enum
)
235 for enum
in enum_values
]
236 return name
, properties
238 def cast_from_json_type(self
, json_type
, string_value
):
239 '''Casts from string |string_value| to a real Python type based on a JSON
240 Schema type |json_type|. For example, a string value of '42' and a JSON
241 Schema type 'integer' will cast to int('42') ==> 42.
243 if json_type
== 'integer':
244 return int(string_value
)
245 if json_type
== 'number':
246 return float(string_value
)
247 # Add more as necessary.
248 assert json_type
== 'string', (
249 'No rule exists to cast JSON Schema type "%s" to its equivalent '
250 'Python type for value "%s". You must add a new rule here.' %
251 (json_type
, string_value
))
255 class Typeref(object):
257 Given a TYPEREF property representing the type of dictionary member or
258 function parameter, converts into a Python dictionary that the JSON schema
259 compiler expects to see.
261 def __init__(self
, typeref
, parent
, additional_properties
):
262 self
.typeref
= typeref
264 self
.additional_properties
= additional_properties
266 def process(self
, callbacks
):
267 properties
= self
.additional_properties
270 if self
.parent
.GetPropertyLocal('OPTIONAL'):
271 properties
['optional'] = True
273 # The IDL parser denotes array types by adding a child 'Array' node onto
274 # the Param node in the Callspec.
275 for sibling
in self
.parent
.GetChildren():
276 if sibling
.cls
== 'Array' and sibling
.GetName() == self
.parent
.GetName():
277 properties
['type'] = 'array'
278 properties
['items'] = OrderedDict()
279 properties
= properties
['items']
282 if self
.typeref
== 'DOMString':
283 properties
['type'] = 'string'
284 elif self
.typeref
== 'boolean':
285 properties
['type'] = 'boolean'
286 elif self
.typeref
== 'double':
287 properties
['type'] = 'number'
288 elif self
.typeref
== 'long':
289 properties
['type'] = 'integer'
290 elif self
.typeref
== 'any':
291 properties
['type'] = 'any'
292 elif self
.typeref
== 'object':
293 properties
['type'] = 'object'
294 if 'additionalProperties' not in properties
:
295 properties
['additionalProperties'] = OrderedDict()
296 properties
['additionalProperties']['type'] = 'any'
297 instance_of
= self
.parent
.GetProperty('instanceOf')
299 properties
['isInstanceOf'] = instance_of
300 elif self
.typeref
== 'ArrayBuffer':
301 properties
['type'] = 'binary'
302 properties
['isInstanceOf'] = 'ArrayBuffer'
303 elif self
.typeref
== 'FileEntry':
304 properties
['type'] = 'object'
305 properties
['isInstanceOf'] = 'FileEntry'
306 if 'additionalProperties' not in properties
:
307 properties
['additionalProperties'] = OrderedDict()
308 properties
['additionalProperties']['type'] = 'any'
309 elif self
.parent
.GetPropertyLocal('Union'):
310 properties
['choices'] = [Typeref(node
.GetProperty('TYPEREF'),
312 OrderedDict()).process(callbacks
)
313 for node
in self
.parent
.GetChildren()
314 if node
.cls
== 'Option']
315 elif self
.typeref
is None:
316 properties
['type'] = 'function'
318 if self
.typeref
in callbacks
:
319 # Do not override name and description if they are already specified.
320 name
= properties
.get('name', None)
321 description
= properties
.get('description', None)
322 properties
.update(callbacks
[self
.typeref
])
323 if description
is not None:
324 properties
['description'] = description
326 properties
['name'] = name
328 properties
['$ref'] = self
.typeref
334 Given an IDL Enum node, converts into a Python dictionary that the JSON
335 schema compiler expects to see.
337 def __init__(self
, enum_node
):
338 self
.node
= enum_node
339 self
.description
= ''
343 for node
in self
.node
.GetChildren():
344 if node
.cls
== 'EnumItem':
345 enum_value
= {'name': node
.GetName()}
346 for child
in node
.GetChildren():
347 if child
.cls
== 'Comment':
348 enum_value
['description'] = ProcessComment(child
.GetName())[0]
350 raise ValueError('Did not process %s %s' % (child
.cls
, child
))
351 enum
.append(enum_value
)
352 elif node
.cls
== 'Comment':
353 self
.description
= ProcessComment(node
.GetName())[0]
355 sys
.exit('Did not process %s %s' % (node
.cls
, node
))
356 result
= {'id' : self
.node
.GetName(),
357 'description': self
.description
,
360 for property_name
in (
361 'inline_doc', 'noinline_doc', 'nodoc', 'cpp_enum_prefix_override',):
362 if self
.node
.GetProperty(property_name
):
363 result
[property_name
] = self
.node
.GetProperty(property_name
)
364 if self
.node
.GetProperty('deprecated'):
365 result
['deprecated'] = self
.node
.GetProperty('deprecated')
369 class Namespace(object):
371 Given an IDLNode representing an IDL namespace, converts into a Python
372 dictionary that the JSON schema compiler expects to see.
381 compiler_options
=None,
383 documentation_options
=None):
384 self
.namespace
= namespace_node
386 self
.internal
= internal
387 self
.platforms
= platforms
388 self
.compiler_options
= compiler_options
391 self
.properties
= OrderedDict()
393 self
.callbacks
= OrderedDict()
394 self
.description
= description
395 self
.deprecated
= deprecated
396 self
.documentation_options
= documentation_options
399 for node
in self
.namespace
.GetChildren():
400 if node
.cls
== 'Dictionary':
401 self
.types
.append(Dictionary(node
).process(self
.callbacks
))
402 elif node
.cls
== 'Callback':
403 k
, v
= Member(node
).process(self
.callbacks
)
404 self
.callbacks
[k
] = v
405 elif node
.cls
== 'Interface' and node
.GetName() == 'Functions':
406 self
.functions
= self
.process_interface(node
)
407 elif node
.cls
== 'Interface' and node
.GetName() == 'Events':
408 self
.events
= self
.process_interface(node
)
409 elif node
.cls
== 'Interface' and node
.GetName() == 'Properties':
410 properties_as_list
= self
.process_interface(
411 node
, functions_are_properties
=True)
412 for prop
in properties_as_list
:
413 # Properties are given as key-value pairs, but IDL will parse
414 # it as a list. Convert back to key-value pairs.
415 prop_name
= prop
.pop('name')
416 assert not self
.properties
.has_key(prop_name
), (
417 'Property "%s" cannot be specified more than once.' %
419 self
.properties
[prop_name
] = prop
420 elif node
.cls
== 'Enum':
421 self
.types
.append(Enum(node
).process())
423 sys
.exit('Did not process %s %s' % (node
.cls
, node
))
424 compiler_options
= self
.compiler_options
or {}
425 documentation_options
= self
.documentation_options
or {}
426 return {'namespace': self
.namespace
.GetName(),
427 'description': self
.description
,
430 'functions': self
.functions
,
431 'properties': self
.properties
,
432 'internal': self
.internal
,
433 'events': self
.events
,
434 'platforms': self
.platforms
,
435 'compiler_options': compiler_options
,
436 'deprecated': self
.deprecated
,
437 'documentation_options': documentation_options
}
439 def process_interface(self
, node
, functions_are_properties
=False):
441 for member
in node
.GetChildren():
442 if member
.cls
== 'Member':
443 _
, properties
= Member(member
).process(
445 functions_are_properties
=functions_are_properties
)
446 members
.append(properties
)
450 class IDLSchema(object):
452 Given a list of IDLNodes and IDLAttributes, converts into a Python list
453 of api_defs that the JSON schema compiler expects to see.
456 def __init__(self
, idl
):
465 compiler_options
= {}
467 documentation_options
= {}
468 for node
in self
.idl
:
469 if node
.cls
== 'Namespace':
471 # TODO(kalman): Go back to throwing an error here.
472 print('%s must have a namespace-level comment. This will '
473 'appear on the API summary page.' % node
.GetName())
475 namespace
= Namespace(node
, description
, nodoc
, internal
,
477 compiler_options
=compiler_options
or None,
478 deprecated
=deprecated
,
479 documentation_options
=documentation_options
)
480 namespaces
.append(namespace
.process())
484 compiler_options
= None
485 elif node
.cls
== 'Copyright':
487 elif node
.cls
== 'Comment':
488 description
= node
.GetName()
489 elif node
.cls
== 'ExtAttribute':
490 if node
.name
== 'nodoc':
491 nodoc
= bool(node
.value
)
492 elif node
.name
== 'internal':
493 internal
= bool(node
.value
)
494 elif node
.name
== 'platforms':
495 platforms
= list(node
.value
)
496 elif node
.name
== 'implemented_in':
497 compiler_options
['implemented_in'] = node
.value
498 elif node
.name
== 'camel_case_enum_to_string':
499 compiler_options
['camel_case_enum_to_string'] = node
.value
500 elif node
.name
== 'deprecated':
501 deprecated
= str(node
.value
)
502 elif node
.name
== 'documentation_title':
503 documentation_options
['title'] = node
.value
504 elif node
.name
== 'documentation_namespace':
505 documentation_options
['namespace'] = node
.value
506 elif node
.name
== 'documented_in':
507 documentation_options
['documented_in'] = node
.value
511 sys
.exit('Did not process %s %s' % (node
.cls
, node
))
517 Given the filename of an IDL file, parses it and returns an equivalent
518 Python dictionary in a format that the JSON schema compiler expects to see.
521 f
= open(filename
, 'r')
525 return Process(contents
, filename
)
528 def Process(contents
, filename
):
530 Processes the contents of a file and returns an equivalent Python dictionary
531 in a format that the JSON schema compiler expects to see. (Separate from
532 Load primarily for testing purposes.)
535 idl
= idl_parser
.IDLParser().ParseData(contents
, filename
)
536 idl_schema
= IDLSchema(idl
)
537 return idl_schema
.process()
542 Dump a json serialization of parse result for the IDL files whose names
543 were passed in on the command line.
545 if len(sys
.argv
) > 1:
546 for filename
in sys
.argv
[1:]:
547 schema
= Load(filename
)
548 print json
.dumps(schema
, indent
=2)
550 contents
= sys
.stdin
.read()
551 idl
= idl_parser
.IDLParser().ParseData(contents
, '<stdin>')
552 schema
= IDLSchema(idl
).process()
553 print json
.dumps(schema
, indent
=2)
556 if __name__
== '__main__':