OLD | NEW |
| (Empty) |
1 # Copyright 2016 The Chromium Authors. All rights reserved. | |
2 # Use of this source code is governed by a BSD-style license that can be | |
3 # found in the LICENSE file. | |
4 | |
5 import argparse | |
6 import os.path | |
7 import sys | |
8 import re | |
9 try: | |
10 import json | |
11 except ImportError: | |
12 import simplejson as json | |
13 | |
14 # Path handling for libraries and templates | |
15 # Paths have to be normalized because Jinja uses the exact template path to | |
16 # determine the hash used in the cache filename, and we need a pre-caching step | |
17 # to be concurrency-safe. Use absolute path because __file__ is absolute if | |
18 # module is imported, and relative if executed directly. | |
19 # If paths differ between pre-caching and individual file compilation, the cache | |
20 # is regenerated, which causes a race condition and breaks concurrent build, | |
21 # since some compile processes will try to read the partially written cache. | |
22 module_path, module_filename = os.path.split(os.path.realpath(__file__)) | |
23 third_party_dir = os.path.normpath(os.path.join( | |
24 module_path, os.pardir, os.pardir, os.pardir, 'third_party')) | |
25 templates_dir = module_path | |
26 | |
27 # jinja2 is in chromium's third_party directory. | |
28 # Insert at 1 so at front to override system libraries, and | |
29 # after path[0] == invoking script dir | |
30 sys.path.insert(1, third_party_dir) | |
31 import jinja2 | |
32 | |
33 | |
34 def ParseArguments(args): | |
35 """Parses command line arguments and returns a (json_api, output_dir) tuple. | |
36 """ | |
37 cmdline_parser = argparse.ArgumentParser() | |
38 cmdline_parser.add_argument('--protocol', required=True) | |
39 cmdline_parser.add_argument('--output_dir', required=True) | |
40 | |
41 args = cmdline_parser.parse_args(args) | |
42 with open(args.protocol, 'r') as f: | |
43 return json.load(f), args.output_dir | |
44 | |
45 | |
46 def ToTitleCase(name): | |
47 return name[:1].upper() + name[1:] | |
48 | |
49 | |
50 def DashToCamelCase(word): | |
51 return ''.join(ToTitleCase(x) for x in word.split('-')) | |
52 | |
53 | |
54 def CamelCaseToHackerStyle(name): | |
55 # Do two passes to insert '_' chars to deal with overlapping matches (e.g., | |
56 # 'LoLoLoL'). | |
57 name = re.sub(r'([^_])([A-Z][a-z]+?)', r'\1_\2', name) | |
58 name = re.sub(r'([^_])([A-Z][a-z]+?)', r'\1_\2', name) | |
59 return name.lower() | |
60 | |
61 | |
62 def SanitizeLiteral(literal): | |
63 return { | |
64 # Rename null enumeration values to avoid a clash with the NULL macro. | |
65 'null': 'none', | |
66 # Rename mathematical constants to avoid colliding with C macros. | |
67 'Infinity': 'InfinityValue', | |
68 '-Infinity': 'NegativeInfinityValue', | |
69 'NaN': 'NaNValue', | |
70 # Turn negative zero into a safe identifier. | |
71 '-0': 'NegativeZeroValue', | |
72 }.get(literal, literal) | |
73 | |
74 | |
75 def InitializeJinjaEnv(cache_dir): | |
76 jinja_env = jinja2.Environment( | |
77 loader=jinja2.FileSystemLoader(templates_dir), | |
78 # Bytecode cache is not concurrency-safe unless pre-cached: | |
79 # if pre-cached this is read-only, but writing creates a race condition. | |
80 bytecode_cache=jinja2.FileSystemBytecodeCache(cache_dir), | |
81 keep_trailing_newline=True, # Newline-terminate generated files. | |
82 lstrip_blocks=True, # So we can indent control flow tags. | |
83 trim_blocks=True) | |
84 jinja_env.filters.update({ | |
85 'to_title_case': ToTitleCase, | |
86 'dash_to_camelcase': DashToCamelCase, | |
87 'camelcase_to_hacker_style': CamelCaseToHackerStyle, | |
88 'sanitize_literal': SanitizeLiteral, | |
89 }) | |
90 jinja_env.add_extension('jinja2.ext.loopcontrols') | |
91 return jinja_env | |
92 | |
93 | |
94 def PatchFullQualifiedRefs(json_api): | |
95 def PatchFullQualifiedRefsInDomain(json, domain_name): | |
96 if isinstance(json, list): | |
97 for item in json: | |
98 PatchFullQualifiedRefsInDomain(item, domain_name) | |
99 | |
100 if not isinstance(json, dict): | |
101 return | |
102 for key in json: | |
103 if key != '$ref': | |
104 PatchFullQualifiedRefsInDomain(json[key], domain_name) | |
105 continue | |
106 if not '.' in json['$ref']: | |
107 json['$ref'] = domain_name + '.' + json['$ref'] | |
108 | |
109 for domain in json_api['domains']: | |
110 PatchFullQualifiedRefsInDomain(domain, domain['domain']) | |
111 | |
112 | |
113 def CreateUserTypeDefinition(domain, type): | |
114 namespace = CamelCaseToHackerStyle(domain['domain']) | |
115 return { | |
116 'return_type': 'std::unique_ptr<headless::%s::%s>' % ( | |
117 namespace, type['id']), | |
118 'pass_type': 'std::unique_ptr<headless::%s::%s>' % (namespace, type['id']), | |
119 'to_raw_type': '*%s', | |
120 'to_raw_return_type': '%s.get()', | |
121 'to_pass_type': 'std::move(%s)', | |
122 'type': 'std::unique_ptr<headless::%s::%s>' % (namespace, type['id']), | |
123 'raw_type': 'headless::%s::%s' % (namespace, type['id']), | |
124 'raw_pass_type': 'headless::%s::%s*' % (namespace, type['id']), | |
125 'raw_return_type': 'const headless::%s::%s*' % (namespace, type['id']), | |
126 } | |
127 | |
128 | |
129 def CreateEnumTypeDefinition(domain_name, type): | |
130 namespace = CamelCaseToHackerStyle(domain_name) | |
131 return { | |
132 'return_type': 'headless::%s::%s' % (namespace, type['id']), | |
133 'pass_type': 'headless::%s::%s' % (namespace, type['id']), | |
134 'to_raw_type': '%s', | |
135 'to_raw_return_type': '%s', | |
136 'to_pass_type': '%s', | |
137 'type': 'headless::%s::%s' % (namespace, type['id']), | |
138 'raw_type': 'headless::%s::%s' % (namespace, type['id']), | |
139 'raw_pass_type': 'headless::%s::%s' % (namespace, type['id']), | |
140 'raw_return_type': 'headless::%s::%s' % (namespace, type['id']), | |
141 } | |
142 | |
143 | |
144 def CreateObjectTypeDefinition(): | |
145 return { | |
146 'return_type': 'std::unique_ptr<base::DictionaryValue>', | |
147 'pass_type': 'std::unique_ptr<base::DictionaryValue>', | |
148 'to_raw_type': '*%s', | |
149 'to_raw_return_type': '%s.get()', | |
150 'to_pass_type': 'std::move(%s)', | |
151 'type': 'std::unique_ptr<base::DictionaryValue>', | |
152 'raw_type': 'base::DictionaryValue', | |
153 'raw_pass_type': 'base::DictionaryValue*', | |
154 'raw_return_type': 'const base::DictionaryValue*', | |
155 } | |
156 | |
157 | |
158 def WrapObjectTypeDefinition(type): | |
159 id = type.get('id', 'base::Value') | |
160 return { | |
161 'return_type': 'std::unique_ptr<%s>' % id, | |
162 'pass_type': 'std::unique_ptr<%s>' % id, | |
163 'to_raw_type': '*%s', | |
164 'to_raw_return_type': '%s.get()', | |
165 'to_pass_type': 'std::move(%s)', | |
166 'type': 'std::unique_ptr<%s>' % id, | |
167 'raw_type': id, | |
168 'raw_pass_type': '%s*' % id, | |
169 'raw_return_type': 'const %s*' % id, | |
170 } | |
171 | |
172 | |
173 def CreateAnyTypeDefinition(): | |
174 return { | |
175 'return_type': 'std::unique_ptr<base::Value>', | |
176 'pass_type': 'std::unique_ptr<base::Value>', | |
177 'to_raw_type': '*%s', | |
178 'to_raw_return_type': '%s.get()', | |
179 'to_pass_type': 'std::move(%s)', | |
180 'type': 'std::unique_ptr<base::Value>', | |
181 'raw_type': 'base::Value', | |
182 'raw_pass_type': 'base::Value*', | |
183 'raw_return_type': 'const base::Value*', | |
184 } | |
185 | |
186 | |
187 def CreateStringTypeDefinition(domain): | |
188 return { | |
189 'return_type': 'std::string', | |
190 'pass_type': 'const std::string&', | |
191 'to_pass_type': '%s', | |
192 'to_raw_type': '%s', | |
193 'to_raw_return_type': '%s', | |
194 'type': 'std::string', | |
195 'raw_type': 'std::string', | |
196 'raw_pass_type': 'const std::string&', | |
197 'raw_return_type': 'std::string', | |
198 } | |
199 | |
200 | |
201 def CreatePrimitiveTypeDefinition(type): | |
202 typedefs = { | |
203 'number': 'double', | |
204 'integer': 'int', | |
205 'boolean': 'bool', | |
206 'string': 'std::string', | |
207 } | |
208 return { | |
209 'return_type': typedefs[type], | |
210 'pass_type': typedefs[type], | |
211 'to_pass_type': '%s', | |
212 'to_raw_type': '%s', | |
213 'to_raw_return_type': '%s', | |
214 'type': typedefs[type], | |
215 'raw_type': typedefs[type], | |
216 'raw_pass_type': typedefs[type], | |
217 'raw_return_type': typedefs[type], | |
218 } | |
219 | |
220 | |
221 type_definitions = {} | |
222 type_definitions['number'] = CreatePrimitiveTypeDefinition('number') | |
223 type_definitions['integer'] = CreatePrimitiveTypeDefinition('integer') | |
224 type_definitions['boolean'] = CreatePrimitiveTypeDefinition('boolean') | |
225 type_definitions['string'] = CreatePrimitiveTypeDefinition('string') | |
226 type_definitions['object'] = CreateObjectTypeDefinition() | |
227 type_definitions['any'] = CreateAnyTypeDefinition() | |
228 | |
229 | |
230 def WrapArrayDefinition(type): | |
231 return { | |
232 'return_type': 'std::vector<%s>' % type['type'], | |
233 'pass_type': 'std::vector<%s>' % type['type'], | |
234 'to_raw_type': '%s', | |
235 'to_raw_return_type': '&%s', | |
236 'to_pass_type': 'std::move(%s)', | |
237 'type': 'std::vector<%s>' % type['type'], | |
238 'raw_type': 'std::vector<%s>' % type['type'], | |
239 'raw_pass_type': 'std::vector<%s>*' % type['type'], | |
240 'raw_return_type': 'const std::vector<%s>*' % type['type'], | |
241 } | |
242 | |
243 | |
244 def CreateTypeDefinitions(json_api): | |
245 for domain in json_api['domains']: | |
246 if not ('types' in domain): | |
247 continue | |
248 for type in domain['types']: | |
249 if type['type'] == 'object': | |
250 if 'properties' in type: | |
251 type_definitions[domain['domain'] + '.' + type['id']] = ( | |
252 CreateUserTypeDefinition(domain, type)) | |
253 else: | |
254 type_definitions[domain['domain'] + '.' + type['id']] = ( | |
255 CreateObjectTypeDefinition()) | |
256 elif type['type'] == 'array': | |
257 items_type = type['items']['type'] | |
258 type_definitions[domain['domain'] + '.' + type['id']] = ( | |
259 WrapArrayDefinition(type_definitions[items_type])) | |
260 elif 'enum' in type: | |
261 type_definitions[domain['domain'] + '.' + type['id']] = ( | |
262 CreateEnumTypeDefinition(domain['domain'], type)) | |
263 type['$ref'] = domain['domain'] + '.' + type['id'] | |
264 elif type['type'] == 'any': | |
265 type_definitions[domain['domain'] + '.' + type['id']] = ( | |
266 CreateAnyTypeDefinition()) | |
267 else: | |
268 type_definitions[domain['domain'] + '.' + type['id']] = ( | |
269 CreatePrimitiveTypeDefinition(type['type'])) | |
270 | |
271 | |
272 def TypeDefinition(name): | |
273 return type_definitions[name] | |
274 | |
275 | |
276 def ResolveType(property): | |
277 if '$ref' in property: | |
278 return type_definitions[property['$ref']] | |
279 elif property['type'] == 'object': | |
280 return WrapObjectTypeDefinition(property) | |
281 elif property['type'] == 'array': | |
282 return WrapArrayDefinition(ResolveType(property['items'])) | |
283 return type_definitions[property['type']] | |
284 | |
285 | |
286 def JoinArrays(dict, keys): | |
287 result = [] | |
288 for key in keys: | |
289 if key in dict: | |
290 result += dict[key] | |
291 return result | |
292 | |
293 | |
294 def SynthesizeEnumType(domain, owner, type): | |
295 type['id'] = ToTitleCase(owner) + ToTitleCase(type['name']) | |
296 type_definitions[domain['domain'] + '.' + type['id']] = ( | |
297 CreateEnumTypeDefinition(domain['domain'], type)) | |
298 type['$ref'] = domain['domain'] + '.' + type['id'] | |
299 domain['types'].append(type) | |
300 | |
301 | |
302 def SynthesizeCommandTypes(json_api): | |
303 """Generate types for command parameters, return values and enum | |
304 properties.""" | |
305 for domain in json_api['domains']: | |
306 if not 'types' in domain: | |
307 domain['types'] = [] | |
308 for type in domain['types']: | |
309 if type['type'] == 'object': | |
310 for property in type.get('properties', []): | |
311 if 'enum' in property and not '$ref' in property: | |
312 SynthesizeEnumType(domain, type['id'], property) | |
313 | |
314 for command in domain.get('commands', []): | |
315 if 'parameters' in command: | |
316 for parameter in command['parameters']: | |
317 if 'enum' in parameter and not '$ref' in parameter: | |
318 SynthesizeEnumType(domain, command['name'], parameter) | |
319 parameters_type = { | |
320 'id': ToTitleCase(command['name']) + 'Params', | |
321 'type': 'object', | |
322 'description': 'Parameters for the %s command.' % ToTitleCase( | |
323 command['name']), | |
324 'properties': command['parameters'] | |
325 } | |
326 domain['types'].append(parameters_type) | |
327 if 'returns' in command: | |
328 for parameter in command['returns']: | |
329 if 'enum' in parameter and not '$ref' in parameter: | |
330 SynthesizeEnumType(domain, command['name'], parameter) | |
331 result_type = { | |
332 'id': ToTitleCase(command['name']) + 'Result', | |
333 'type': 'object', | |
334 'description': 'Result for the %s command.' % ToTitleCase( | |
335 command['name']), | |
336 'properties': command['returns'] | |
337 } | |
338 domain['types'].append(result_type) | |
339 | |
340 | |
341 def SynthesizeEventTypes(json_api): | |
342 """Generate types for events and their properties. | |
343 | |
344 Note that parameter objects are also created for events without parameters to | |
345 make it easier to introduce parameters later. | |
346 """ | |
347 for domain in json_api['domains']: | |
348 if not 'types' in domain: | |
349 domain['types'] = [] | |
350 for event in domain.get('events', []): | |
351 for parameter in event.get('parameters', []): | |
352 if 'enum' in parameter and not '$ref' in parameter: | |
353 SynthesizeEnumType(domain, event['name'], parameter) | |
354 event_type = { | |
355 'id': ToTitleCase(event['name']) + 'Params', | |
356 'type': 'object', | |
357 'description': 'Parameters for the %s event.' % ToTitleCase( | |
358 event['name']), | |
359 'properties': event.get('parameters', []) | |
360 } | |
361 domain['types'].append(event_type) | |
362 | |
363 | |
364 def PatchExperimentalCommandsAndEvents(json_api): | |
365 """ | |
366 Mark all commands and events in experimental domains as experimental | |
367 and make sure experimental commands have at least empty parameters | |
368 and return values. | |
369 """ | |
370 for domain in json_api['domains']: | |
371 if domain.get('experimental', False): | |
372 for command in domain.get('commands', []): | |
373 command['experimental'] = True | |
374 for event in domain.get('events', []): | |
375 event['experimental'] = True | |
376 | |
377 | |
378 def EnsureCommandsHaveParametersAndReturnTypes(json_api): | |
379 """ | |
380 Make sure all commands have at least empty parameters and return values. This | |
381 guarantees API compatibility if a previously experimental command is made | |
382 stable. | |
383 """ | |
384 for domain in json_api['domains']: | |
385 for command in domain.get('commands', []): | |
386 if not 'parameters' in command: | |
387 command['parameters'] = [] | |
388 if not 'returns' in command: | |
389 command['returns'] = [] | |
390 for event in domain.get('events', []): | |
391 if not 'parameters' in event: | |
392 event['parameters'] = [] | |
393 | |
394 | |
395 def Generate(jinja_env, output_dirname, json_api, class_name, file_types): | |
396 template_context = { | |
397 'api': json_api, | |
398 'join_arrays': JoinArrays, | |
399 'resolve_type': ResolveType, | |
400 'type_definition': TypeDefinition, | |
401 } | |
402 for file_type in file_types: | |
403 template = jinja_env.get_template('/%s_%s.template' % ( | |
404 class_name, file_type)) | |
405 output_file = '%s/%s.%s' % (output_dirname, class_name, file_type) | |
406 with open(output_file, 'w') as f: | |
407 f.write(template.render(template_context)) | |
408 | |
409 | |
410 def GenerateDomains(jinja_env, output_dirname, json_api, class_name, | |
411 file_types): | |
412 for file_type in file_types: | |
413 template = jinja_env.get_template('/%s_%s.template' % ( | |
414 class_name, file_type)) | |
415 for domain in json_api['domains']: | |
416 template_context = { | |
417 'domain': domain, | |
418 'resolve_type': ResolveType, | |
419 } | |
420 domain_name = CamelCaseToHackerStyle(domain['domain']) | |
421 output_file = '%s/%s.%s' % (output_dirname, domain_name, file_type) | |
422 with open(output_file, 'w') as f: | |
423 f.write(template.render(template_context)) | |
424 | |
425 | |
426 if __name__ == '__main__': | |
427 json_api, output_dirname = ParseArguments(sys.argv[1:]) | |
428 jinja_env = InitializeJinjaEnv(output_dirname) | |
429 PatchExperimentalCommandsAndEvents(json_api) | |
430 EnsureCommandsHaveParametersAndReturnTypes(json_api) | |
431 SynthesizeCommandTypes(json_api) | |
432 SynthesizeEventTypes(json_api) | |
433 PatchFullQualifiedRefs(json_api) | |
434 CreateTypeDefinitions(json_api) | |
435 Generate(jinja_env, output_dirname, json_api, 'types', ['cc', 'h']) | |
436 Generate(jinja_env, output_dirname, json_api, 'type_conversions', ['h']) | |
437 GenerateDomains(jinja_env, output_dirname, json_api, 'domain', ['cc', 'h']) | |
OLD | NEW |