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

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

Issue 2473073003: [headless] Refactor headless devtools client API. (Closed)
Patch Set: Address nits Created 4 years, 1 month 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
« no previous file with comments | « headless/app/headless_shell.cc ('k') | headless/lib/browser/client_api_generator_unittest.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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'])
OLDNEW
« no previous file with comments | « headless/app/headless_shell.cc ('k') | headless/lib/browser/client_api_generator_unittest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698