Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(80)

Side by Side Diff: headless/lib/browser/client_api_generator.py

Issue 1805983002: headless: Implement client API generation (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Documentation Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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'])
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698