OLD | NEW |
---|---|
(Empty) | |
1 # Copyright 2016 The Chromium Authors. All rights reserved. | |
altimin
2016/04/14 12:54:12
I believe that this python code deserves some test
Sami
2016/04/15 14:43:44
Indeed, made it so.
| |
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 os.path | |
6 import sys | |
7 import optparse | |
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 cmdline_parser = optparse.OptionParser() | |
34 cmdline_parser.add_option('--output_dir') | |
35 cmdline_parser.add_option('--template_dir') | |
36 | |
37 try: | |
38 arg_options, arg_values = cmdline_parser.parse_args() | |
39 if (len(arg_values) != 1): | |
40 raise Exception('Exactly one plain argument expected (found %s)' % | |
41 len(arg_values)) | |
42 input_json_filename = arg_values[0] | |
43 output_dirname = arg_options.output_dir | |
44 if not output_dirname: | |
45 raise Exception('Output directory must be specified') | |
altimin
2016/04/14 12:54:12
ValueError?
Or, even better, consider using argpa
Sami
2016/04/15 14:43:44
Good idea, replaced with argparse.
| |
46 except Exception: | |
47 # Work with python 2 and 3 http://docs.python.org/py3k/howto/pyporting.html | |
48 exc = sys.exc_info()[1] | |
49 sys.stderr.write('Failed to parse command-line arguments: %s\n\n' % exc) | |
50 sys.stderr.write('Usage: <script> --output_dir <output_dir> protocol.json\n') | |
51 exit(1) | |
52 | |
53 input_file = open(input_json_filename, 'r') | |
altimin
2016/04/14 12:54:11
Can we please avoid opening files and doing comple
Sami
2016/04/15 14:43:44
Agreed & removed.
| |
54 json_string = input_file.read() | |
55 json_api = json.loads(json_string) | |
56 | |
57 | |
58 def ToTitleCase(name): | |
59 return name[:1].upper() + name[1:] | |
altimin
2016/04/14 12:54:11
str.title?
Sami
2016/04/15 14:43:44
str.title() turns 'fooBar' into 'Foobar' which doe
| |
60 | |
61 | |
62 def DashToCamelCase(word): | |
63 return ''.join(ToTitleCase(x) or '-' for x in word.split('-')) | |
altimin
2016/04/14 12:54:12
Do we need "or '-'" here?
Sami
2016/04/15 14:43:44
Hmm, not sure why it was there (it was there on th
| |
64 | |
65 | |
66 def CamelCaseToHackerStyle(name): | |
67 # Do two passes to insert '_' chars to deal with overlapping matches (e.g., | |
68 # 'LoLoLoL'). | |
69 name = re.sub(r'([^_])([A-Z][a-z]+?)', r'\1_\2', name) | |
70 name = re.sub(r'([^_])([A-Z][a-z]+?)', r'\1_\2', name) | |
71 return name.lower() | |
72 | |
73 | |
74 def MangleEnum(value): | |
75 # Rename NULL enumeration values to avoid a clash with the actual NULL. | |
76 return 'NONE' if value == 'NULL' else value | |
77 | |
78 | |
79 def InitializeJinjaEnv(cache_dir): | |
80 jinja_env = jinja2.Environment( | |
81 loader=jinja2.FileSystemLoader(templates_dir), | |
82 # Bytecode cache is not concurrency-safe unless pre-cached: | |
83 # if pre-cached this is read-only, but writing creates a race condition. | |
84 bytecode_cache=jinja2.FileSystemBytecodeCache(cache_dir), | |
85 keep_trailing_newline=True, # Newline-terminate generated files. | |
86 lstrip_blocks=True, # So we can indent control flow tags. | |
87 trim_blocks=True) | |
88 jinja_env.filters.update({ | |
89 'to_title_case': ToTitleCase, | |
90 'dash_to_camelcase': DashToCamelCase, | |
91 'camelcase_to_hacker_style': CamelCaseToHackerStyle, | |
92 'mangle_enum': MangleEnum, | |
93 }) | |
94 jinja_env.add_extension('jinja2.ext.loopcontrols') | |
95 return jinja_env | |
96 | |
97 | |
98 def PatchFullQualifiedRefs(): | |
99 def PatchFullQualifiedRefsInDomain(json, domain_name): | |
100 if isinstance(json, list): | |
101 for item in json: | |
102 PatchFullQualifiedRefsInDomain(item, domain_name) | |
103 | |
104 if not isinstance(json, dict): | |
105 return | |
106 for key in json: | |
107 if key != '$ref': | |
108 PatchFullQualifiedRefsInDomain(json[key], domain_name) | |
109 continue | |
110 if not '.' in json['$ref']: | |
111 json['$ref'] = domain_name + '.' + json['$ref'] | |
112 | |
113 for domain in json_api['domains']: | |
114 PatchFullQualifiedRefsInDomain(domain, domain['domain']) | |
115 | |
116 | |
117 def CreateUserTypeDefinition(domain, type): | |
118 namespace = CamelCaseToHackerStyle(domain['domain']) | |
119 """for property in type['properties']: | |
altimin
2016/04/14 12:54:12
If this is some kind of documentation, then I don'
Sami
2016/04/15 14:43:44
Sorry, another commented-out test. Removed.
| |
120 if 'enum' in property: | |
121 property['id'] = type['id'] + ToTitleCase(property['name']) | |
122 full_type = domain['domain'] + '.' + property['id'] | |
123 property['$ref'] = full_type | |
124 print 'NEW ENUM', full_type | |
125 if not full_type in type_definitions: | |
126 type_definitions[full_type] = ( | |
127 CreateEnumTypeDefinition(domain['domain'], property)) | |
128 domain['types'].append(property)""" | |
129 | |
130 return { | |
131 'return_type': 'std::unique_ptr<headless::%s::%s>' % ( | |
132 namespace, type['id']), | |
133 'pass_type': 'std::unique_ptr<headless::%s::%s>' % (namespace, type['id']), | |
134 'to_raw_type': '*%s', | |
135 'to_raw_return_type': '%s.get()', | |
136 'to_pass_type': 'std::move(%s)', | |
137 'type': 'std::unique_ptr<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 CreateEnumTypeDefinition(domain_name, type): | |
145 namespace = CamelCaseToHackerStyle(domain_name) | |
146 return { | |
147 'return_type': 'headless::%s::%s' % (namespace, type['id']), | |
148 'pass_type': 'headless::%s::%s' % (namespace, type['id']), | |
149 'to_raw_type': '%s', | |
150 'to_raw_return_type': '%s', | |
151 'to_pass_type': '%s', | |
152 'type': 'headless::%s::%s' % (namespace, type['id']), | |
153 'raw_type': 'headless::%s::%s' % (namespace, type['id']), | |
154 'raw_pass_type': 'headless::%s::%s' % (namespace, type['id']), | |
155 'raw_return_type': 'headless::%s::%s' % (namespace, type['id']), | |
156 } | |
157 | |
158 | |
159 def CreateObjectTypeDefinition(): | |
160 return { | |
161 'return_type': 'std::unique_ptr<base::DictionaryValue>', | |
162 'pass_type': 'std::unique_ptr<base::DictionaryValue>', | |
163 'to_raw_type': '*%s', | |
164 'to_raw_return_type': '%s.get()', | |
165 'to_pass_type': 'std::move(%s)', | |
166 'type': 'std::unique_ptr<base::DictionaryValue>', | |
167 'raw_type': 'base::DictionaryValue', | |
168 'raw_pass_type': 'base::DictionaryValue*', | |
169 'raw_return_type': 'base::DictionaryValue*', | |
170 } | |
171 | |
172 | |
173 def WrapObjectTypeDefinition(type): | |
174 id = type.get('id', 'base::Value') | |
175 return { | |
176 'return_type': 'std::unique_ptr<%s>' % id, | |
177 'pass_type': 'std::unique_ptr<%s>' % id, | |
178 'to_raw_type': '*%s', | |
179 'to_raw_return_type': '%s.get()', | |
180 'to_pass_type': 'std::move(%s)', | |
181 'type': 'std::unique_ptr<%s>' % id, | |
182 'raw_type': id, | |
183 'raw_pass_type': '%s*' % id, | |
184 'raw_return_type': '%s*' % id, | |
185 } | |
186 | |
187 | |
188 def CreateAnyTypeDefinition(): | |
189 return { | |
190 'return_type': 'std::unique_ptr<base::Value>', | |
191 'pass_type': 'std::unique_ptr<base::Value>', | |
192 'to_raw_type': '*%s', | |
193 'to_raw_return_type': '%s.get()', | |
194 'to_pass_type': 'std::move(%s)', | |
195 'type': 'std::unique_ptr<base::Value>', | |
196 'raw_type': 'base::Value', | |
197 'raw_pass_type': 'base::Value*', | |
198 'raw_return_type': 'base::Value*', | |
199 } | |
200 | |
201 | |
202 def CreateStringTypeDefinition(domain): | |
203 return { | |
204 'return_type': 'std::string', | |
205 'pass_type': 'const std::string&', | |
206 'to_pass_type': '%s', | |
207 'to_raw_type': '%s', | |
208 'to_raw_return_type': '%s', | |
209 'type': 'std::string', | |
210 'raw_type': 'std::string', | |
211 'raw_pass_type': 'const std::string&', | |
212 'raw_return_type': 'std::string', | |
213 } | |
214 | |
215 | |
216 def CreatePrimitiveTypeDefinition(type): | |
217 typedefs = { | |
218 'number': 'double', | |
219 'integer': 'int', | |
220 'boolean': 'bool', | |
221 'string': 'std::string', | |
222 } | |
223 return { | |
224 'return_type': typedefs[type], | |
225 'pass_type': typedefs[type], | |
226 'to_pass_type': '%s', | |
227 'to_raw_type': '%s', | |
228 'to_raw_return_type': '%s', | |
229 'type': typedefs[type], | |
230 'raw_type': typedefs[type], | |
231 'raw_pass_type': typedefs[type], | |
232 'raw_return_type': typedefs[type], | |
233 } | |
234 | |
235 | |
236 type_definitions = {} | |
237 type_definitions['number'] = CreatePrimitiveTypeDefinition('number') | |
238 type_definitions['integer'] = CreatePrimitiveTypeDefinition('integer') | |
239 type_definitions['boolean'] = CreatePrimitiveTypeDefinition('boolean') | |
240 type_definitions['string'] = CreatePrimitiveTypeDefinition('string') | |
241 type_definitions['object'] = CreateObjectTypeDefinition() | |
242 type_definitions['any'] = CreateAnyTypeDefinition() | |
243 | |
244 | |
245 def WrapArrayDefinition(type): | |
246 return { | |
247 'return_type': 'std::vector<%s>' % type['type'], | |
248 'pass_type': 'std::vector<%s>' % type['type'], | |
249 'to_raw_type': '%s', | |
250 'to_raw_return_type': '&%s', | |
251 'to_pass_type': 'std::move(%s)', | |
252 'type': 'std::vector<%s>' % type['type'], | |
253 'raw_type': 'std::vector<%s>' % type['type'], | |
254 'raw_pass_type': 'std::vector<%s>*' % type['type'], | |
255 'raw_return_type': 'std::vector<%s>*' % type['type'], | |
256 } | |
257 | |
258 | |
259 def CreateTypeDefinitions(): | |
260 for domain in json_api['domains']: | |
261 if not ('types' in domain): | |
262 continue | |
263 for type in domain['types']: | |
264 if type['type'] == 'object': | |
265 if 'properties' in type: | |
266 type_definitions[domain['domain'] + '.' + type['id']] = ( | |
267 CreateUserTypeDefinition(domain, type)) | |
268 else: | |
269 type_definitions[domain['domain'] + '.' + type['id']] = ( | |
270 CreateObjectTypeDefinition()) | |
271 elif type['type'] == 'array': | |
272 items_type = type['items']['type'] | |
273 type_definitions[domain['domain'] + '.' + type['id']] = ( | |
274 WrapArrayDefinition(type_definitions[items_type])) | |
275 elif 'enum' in type: | |
276 type_definitions[domain['domain'] + '.' + type['id']] = ( | |
277 CreateEnumTypeDefinition(domain['domain'], type)) | |
278 type['$ref'] = domain['domain'] + '.' + type['id'] | |
279 else: | |
280 type_definitions[domain['domain'] + '.' + type['id']] = ( | |
281 CreatePrimitiveTypeDefinition(type['type'])) | |
282 | |
283 | |
284 def TypeDefinition(name): | |
285 return type_definitions[name] | |
286 | |
287 | |
288 def ResolveType(property): | |
289 if '$ref' in property: | |
290 return type_definitions[property['$ref']] | |
291 elif property['type'] == 'object': | |
292 return WrapObjectTypeDefinition(property) | |
293 elif property['type'] == 'array': | |
294 return WrapArrayDefinition(ResolveType(property['items'])) | |
295 return type_definitions[property['type']] | |
296 | |
297 | |
298 def JoinArrays(dict, keys): | |
299 result = [] | |
300 for key in keys: | |
301 if key in dict: | |
302 result += dict[key] | |
303 return result | |
304 | |
305 | |
306 def OpenOutputFile(name): | |
307 return open(os.path.join(output_dirname, name), 'w') | |
308 | |
309 | |
310 def SynthesizeEnumType(domain, owner, type): | |
311 type['id'] = ToTitleCase(owner) + ToTitleCase(type['name']) | |
312 type_definitions[domain['domain'] + '.' + type['id']] = ( | |
313 CreateEnumTypeDefinition(domain['domain'], type)) | |
314 type['$ref'] = domain['domain'] + '.' + type['id'] | |
315 domain['types'].append(type) | |
316 | |
317 | |
318 def SynthesizeCommandTypes(): | |
319 """Generate types for command parameters, return values and enum | |
320 properties.""" | |
321 for domain in json_api['domains']: | |
322 if not 'types' in domain: | |
323 domain['types'] = [] | |
324 for type in domain['types']: | |
325 if type['type'] == 'object': | |
326 for property in type.get('properties', []): | |
327 if 'enum' in property and not '$ref' in property: | |
328 SynthesizeEnumType(domain, type['id'], property) | |
329 | |
330 for command in domain.get('commands', []): | |
331 if 'parameters' in command: | |
332 for parameter in command['parameters']: | |
333 if 'enum' in parameter and not '$ref' in parameter: | |
334 SynthesizeEnumType(domain, command['name'], parameter) | |
335 parameters_type = { | |
336 'id': ToTitleCase(command['name']) + 'Params', | |
337 'type': 'object', | |
338 'description': 'Parameters for the %s command.' % ToTitleCase( | |
339 command['name']), | |
340 'properties': command['parameters'] | |
341 } | |
342 domain['types'].append(parameters_type) | |
343 if 'returns' in command: | |
344 for parameter in command['returns']: | |
345 if 'enum' in parameter and not '$ref' in parameter: | |
346 SynthesizeEnumType(domain, command['name'], parameter) | |
347 result_type = { | |
348 'id': ToTitleCase(command['name']) + 'Result', | |
349 'type': 'object', | |
350 'description': 'Result for the %s command.' % ToTitleCase( | |
351 command['name']), | |
352 'properties': command['returns'] | |
353 } | |
354 domain['types'].append(result_type) | |
355 | |
356 | |
357 def Generate(class_name, file_types): | |
358 template_context = { | |
359 'api': json_api, | |
360 'join_arrays': JoinArrays, | |
361 'resolve_type': ResolveType, | |
362 'type_definition': TypeDefinition, | |
363 } | |
364 for file_type in file_types: | |
365 template = jinja_env.get_template('/%s_%s.template' % ( | |
366 class_name, file_type)) | |
367 output_file = '%s/%s.%s' % (output_dirname, class_name, file_type) | |
368 with open(output_file, 'w') as f: | |
369 f.write(template.render(template_context)) | |
370 | |
371 | |
372 def GenerateDomains(class_name, file_types): | |
373 for file_type in file_types: | |
374 template = jinja_env.get_template('/%s_%s.template' % ( | |
375 class_name, file_type)) | |
376 for domain in json_api['domains']: | |
377 template_context = { | |
378 'domain': domain, | |
379 'resolve_type': ResolveType, | |
380 } | |
381 domain_name = CamelCaseToHackerStyle(domain['domain']) | |
382 output_file = '%s/%s.%s' % (output_dirname, domain_name, file_type) | |
383 with open(output_file, 'w') as f: | |
384 f.write(template.render(template_context)) | |
385 | |
386 | |
387 if __name__ == '__main__': | |
388 jinja_env = InitializeJinjaEnv(output_dirname) | |
389 SynthesizeCommandTypes() | |
390 PatchFullQualifiedRefs() | |
391 CreateTypeDefinitions() | |
392 Generate('types', ['cc', 'h']) | |
393 Generate('type_conversions', ['h']) | |
394 GenerateDomains('domain', ['cc', 'h']) | |
OLD | NEW |