Chromium Code Reviews| Index: tools/json_schema_compiler/dart_generator.py |
| diff --git a/tools/json_schema_compiler/dart_generator.py b/tools/json_schema_compiler/dart_generator.py |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..a3b1135d1ae2f9fa97a2ff27afcf0cbf437c6cf4 |
| --- /dev/null |
| +++ b/tools/json_schema_compiler/dart_generator.py |
| @@ -0,0 +1,569 @@ |
| +# Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| +# Use of this source code is governed by a BSD-style license that can be |
| +# found in the LICENSE file. |
| +""" |
| +Generator language component for compiler.py that adds Dart language support. |
| + |
| +Pass 'dart' with the -l flag to compiler.py to activate the use of this library. |
| +""" |
| + |
| +from code import Code |
| +from model import * |
| +from schema_util import * |
| + |
| +import os |
| +from datetime import datetime |
| + |
| +LICENSE = (""" |
| +// Copyright (c) %s, the Dart project authors. Please see the AUTHORS file |
| +// for details. All rights reserved. Use of this source code is governed by a |
| +// BSD-style license that can be found in the LICENSE file.""" % |
| + datetime.now().year) |
| + |
| +class DartGenerator(object): |
| + """A .dart generator for a namespace. |
| + """ |
| + |
| + def __init__(self, namespace, dart_overrides_dir=None): |
| + self._namespace = namespace |
| + self._types = namespace.types |
| + |
| + # Build a dictionary of Type Name --> Custom Dart code. |
| + self._custom_dart = {} |
|
not at google - send to devlin
2013/01/31 02:08:48
_type_overrides?
sashab
2013/01/31 04:41:40
Done.
|
| + if dart_overrides_dir is not None: |
| + for filename in os.listdir(dart_overrides_dir): |
| + if filename.startswith(namespace.unix_name): |
| + with open(os.path.join(dart_overrides_dir, filename)) as f: |
| + # Split off the namespace and file extension, leaving just the type. |
| + type_path = '.'.join(filename.split('.')[1:-1]) |
| + self._custom_dart[type_path] = f.read() |
| + |
| + def Generate(self): |
| + """Generates a Code object with the .dart for the entire namespace. |
| + """ |
| + c = Code() |
| + (c.Append(LICENSE) |
| + .Append() |
| + .Append('part of chrome;')) |
| + |
| + if self._types: |
| + (c.Append() |
| + .Append('/**') |
| + .Append(' * Types') |
| + .Append(' */') |
| + ) |
| + for type_name in self._types: |
| + c.Concat(self._GenerateType(self._types[type_name])) |
| + |
| + if self._namespace.events: |
| + (c.Append() |
| + .Append('/**') |
| + .Append(' * Events') |
| + .Append(' */') |
| + ) |
| + for event_name in self._namespace.events: |
| + c.Concat(self._GenerateEvent(self._namespace.events[event_name])) |
| + |
| + (c.Append() |
| + .Append('/**') |
| + .Append(' * Functions') |
| + .Append(' */') |
| + ) |
| + c.Concat(self._GenerateMainClass()) |
| + |
| + return c |
| + |
| + def _GenerateType(self, type_): |
| + """Given a Type object, returns the Code with the .dart for this |
| + type's definition. |
| + |
| + Assumes this type is a Parameter Type (creatable by user), and creates an |
| + object that extends ChromeObject. All parameters are specifiable as named |
| + arguments in the constructor, and all methods are wrapped with getters and |
| + setters that hide the JS() implementation. |
| + """ |
| + c = Code() |
| + (c.Append() |
| + .Concat(self._GenerateDocumentation(type_)) |
| + .Sblock('class %(type_name)s extends ChromeObject {') |
| + ) |
| + |
| + # Check whether this type has function members. If it does, don't allow |
| + # public construction. |
| + for p in type_.properties.values(): |
| + print p.name, self._IsFunction(p) |
| + add_public_constructor = all(not self._IsFunction(p) |
| + for p in type_.properties.values()) |
| + constructor_fields = [self._GeneratePropertySignature(p) |
| + for p in type_.properties.values()] |
| + |
| + if add_public_constructor: |
| + (c.Append('/*') |
| + .Append(' * Public constructor') |
| + .Append(' */') |
| + .Sblock('%(type_name)s({%(constructor_fields)s}) {') |
| + ) |
| + |
| + for prop_name in type_.properties: |
| + c.Append('this.%s = %s;' % (prop_name, prop_name)) |
| + (c.Eblock('}') |
| + .Append() |
| + ) |
| + |
| + (c.Append('/*') |
| + .Append(' * Private constructor') |
| + .Append(' */') |
| + .Append('%(type_name)s._proxy(_jsObject) : super._proxy(_jsObject);') |
| + ) |
| + |
| + # Add an accessor (getter & setter) for each property. |
| + properties = [p for p in type_.properties.values() |
| + if not self._IsFunction(p)] |
| + if properties: |
| + (c.Append() |
| + .Append('/*') |
| + .Append(' * Public accessors') |
| + .Append(' */') |
| + ) |
| + for prop in properties: |
| + type_name = self._GetPropertyType(prop) |
| + prop_is_base_type = self._IsBaseType(prop) |
| + |
| + # Check for custom dart for this whole property. |
| + if not self._TryAppendOverride(c, type_, prop, add_doc=True): |
| + # Add the getter. |
| + if not self._TryAppendOverride(c, type_, prop, '.get', add_doc=True): |
|
not at google - send to devlin
2013/01/31 02:08:48
key_suffix='.get'
sashab
2013/01/31 04:41:40
Done.
|
| + # Add the documentation for this property. |
| + (c.Append() |
| + .Concat(self._GenerateDocumentation(prop)) |
| + ) |
| + |
| + # TODO(sashab): Serialize generic Dart objects differently. |
| + if prop_is_base_type or self._IsObjectType(prop): |
| + c.Append("%s get %s => JS('%s', '#.%s', this._jsObject);" % |
| + (type_name, prop.name, type_name, prop.name)) |
| + elif prop.type_.property_type == PropertyType.REF: |
| + c.Append("%s get %s => new %s._proxy(JS('', '#.%s', " |
| + "this._jsObject));" |
| + % (type_name, prop.name, type_name, prop.name)) |
| + else: |
| + raise Exception( |
| + "Could not generate wrapper for %s.%s: unserializable type %s" % |
| + (type_.name, prop.name, type_name) |
| + ) |
| + |
| + # Add the setter. |
| + if not self._TryAppendOverride(c, type_, prop, '.set'): |
|
not at google - send to devlin
2013/01/31 02:08:48
key_suffix='.set'
sashab
2013/01/31 04:41:40
Done.
|
| + wrapped_name = prop.name |
| + if not prop_is_base_type: |
| + wrapped_name = 'convertArgument(%s)' % prop.name |
| + |
| + (c.Append() |
| + .Sblock("void set %s(%s %s) {" % (prop.name, type_name, prop.name)) |
| + .Append("JS('void', '#.%s = #', this._jsObject, %s);" % |
| + (prop.name, wrapped_name)) |
| + .Eblock("}") |
| + ) |
| + |
| + # Now add all the methods. |
| + methods = [t for t in type_.properties.values() |
| + if self._IsFunction(t)] |
|
not at google - send to devlin
2013/01/31 02:08:48
might fit on 1 line?
sashab
2013/01/31 04:41:40
Done.
|
| + if methods: |
| + (c.Append() |
| + .Append('/*') |
| + .Append(' * Methods') |
| + .Append(' */') |
| + ) |
| + for prop in methods: |
| + c.Concat(self._GenerateFunction(prop.type_.function)) |
| + |
| + (c.Eblock('}') |
| + .Substitute({ |
| + 'type_name': type_.simple_name, |
| + 'constructor_fields': ', '.join(constructor_fields) |
| + }) |
| + ) |
| + |
| + return c |
| + |
| + def _GenerateDocumentation(self, prop): |
| + """Given an object, generates the documentation for this object (as a |
| + code string) and returns the Code object. |
| + |
| + Returns an empty code object if the object has no documentation. |
| + |
| + Uses triple-quotes for the string. |
| + """ |
| + c = Code() |
| + if prop.description is not None: |
| + for line in prop.description.split('\n'): |
| + c.Comment(line, comment_prefix='/// ') |
| + return c |
| + |
| + |
| + def _GenerateFunction(self, f): |
| + """Returns the Code object for the given function. |
| + """ |
| + c = Code() |
| + (c.Append() |
| + .Concat(self._GenerateDocumentation(f)) |
| + ) |
| + |
| + if not self._NeedsProxiedCallback(f): |
| + c.Append("%s => %s;" % (self._GenerateFunctionSignature(f), |
| + self._GenerateProxyCall(f))) |
| + return c |
| + |
| + c.Sblock("%s {" % self._GenerateFunctionSignature(f)) |
| + |
| + # Define the proxied callback. |
| + proxied_params = [] |
| + for p in f.callback.params: |
| + if self._IsBaseType(p): |
| + proxied_params.append(p.name) |
| + elif self._IsObjectType(p): |
| + proxied_params.append("new %s._proxy(%s)" % ( |
| + self._GetPropertyType(p), p.name)) |
| + else: |
| + raise Exception( |
| + "Cannot automatically create proxy; can't wrap %s.%s, type %s" % ( |
| + f.name, p.name, p.type_.name)) |
| + |
| + (c.Sblock("void __proxy_callback(%s) {" % ', '.join(p.name for p in |
| + f.callback.params)) |
| + .Sblock("if (?%s)" % f.callback.name) |
| + .Append("%s(%s);" % (f.callback.name, ', '.join(proxied_params))) |
| + .Eblock(None) |
|
not at google - send to devlin
2013/01/31 02:08:48
like I said earlier, just make None the default? T
sashab
2013/01/31 04:41:40
Done.
|
| + .Eblock("}") |
| + .Append("%s;" % self._GenerateProxyCall(f)) |
| + .Eblock("}") |
| + ) |
| + return c |
| + |
| + def _NeedsProxiedCallback(self, f): |
| + """Given a function, returns True if this function's callback needs to be |
| + proxied, False if not. |
| + |
| + Function callbacks need to be proxied if they have at least one |
| + non-base-type parameter. |
| + """ |
| + return f.callback and any( |
| + not self._IsBaseType(p) for p in f.callback.params) |
| + |
| + def _GenerateProxyCall(self, function, call_target='this._jsObject'): |
| + """Given a function, generates the code to call that function via JS(). |
| + Returns a string. |
| + |
| + |call_target| is the name of the object to call the function on. The default |
| + is this._jsObject. |
| + |
| + e.g. |
| + JS('void', '#.resizeTo(#, #)', this._jsObject, width, height) |
| + JS('void', '#.setBounds(#)', this._jsObject, convertArgument(bounds)) |
| + """ |
| + n_params = len(function.params) |
| + if function.callback: |
| + n_params += 1 |
| + |
| + params = ["'%s'" % self._GetPropertyType(function.returns), |
| + "'#.%s(%s)'" % (function.name, |
| + ', '.join(['#'] * n_params)), |
| + call_target] |
| + |
| + for param in function.params: |
| + if not self._IsBaseType(param): |
| + params.append('convertArgument(%s)' % param.name) |
| + else: |
| + params.append(param.name) |
| + if function.callback: |
| + # If this isn't a base type, we need a proxied callback. |
| + callback_name = function.callback.name |
| + if self._NeedsProxiedCallback(function): |
| + callback_name = "__proxy_callback" |
| + params.append('convertDartClosureToJS(%s, %s)' % (callback_name, |
| + len(function.callback.params))) |
| + |
| + return 'JS(%s)' % ', '.join(params) |
| + |
| + def _GenerateEvent(self, event): |
| + """Given a Function object, returns the Code with the .dart for this event, |
| + represented by the function. |
| + |
| + All events extend the Event base type. |
| + """ |
| + c = Code() |
| + |
| + # Add documentation for this event. |
| + (c.Append() |
| + .Concat(self._GenerateDocumentation(event)) |
| + .Sblock('class Event_%(event_name)s extends Event {') |
| + ) |
| + |
| + # Override Event callback type definitions. |
| + for ret_type, event_func in (('void', 'addListener'), |
| + ('void', 'removeListener'), |
| + ('bool', 'hasListener')): |
| + |
| + param_list = self._GenerateParameterList(event.params, event.callback, |
| + allow_optional = False) |
| + |
| + c.Append('%s %s(void callback(%s)) => super.%s(callback);' % |
| + (ret_type, event_func, param_list, event_func)) |
| + |
| + # Generate the constructor. |
| + (c.Append() |
| + .Append('Event_%(event_name)s(jsObject) : ' |
| + 'super(jsObject, %(param_num)d);') |
| + ) |
| + |
| + (c.Eblock('}') |
| + .Substitute({ |
| + 'event_name': self._namespace.unix_name + '_' + event.name, |
| + 'param_num': len(event.params) |
| + }) |
| + ) |
| + |
| + return c |
| + |
| + def _GenerateMainClass(self): |
| + """Generates the main class for this file, which links to all functions |
| + and events. |
| + |
| + Returns a code object. |
| + """ |
| + c = Code() |
| + (c.Append() |
| + .Sblock('class API_%s {' % self._namespace.unix_name) |
| + .Append('/*') |
| + .Append(' * API connection') |
| + .Append(' */') |
| + .Append('Object _jsObject;') |
| + ) |
| + |
| + # Add events. |
| + if self._namespace.events: |
| + (c.Append() |
| + .Append('/*') |
| + .Append(' * Events') |
| + .Append(' */') |
| + ) |
| + for event_name in self._namespace.events: |
| + c.Append('Event_%s_%s %s;' % (self._namespace.unix_name, event_name, |
| + event_name)) |
| + |
| + # Add functions. |
| + if self._namespace.functions: |
| + (c.Append() |
| + .Append('/*') |
| + .Append(' * Functions') |
| + .Append(' */') |
| + ) |
| + for function in self._namespace.functions.values(): |
| + c.Concat(self._GenerateFunction(function)) |
| + |
| + # Add the constructor. |
| + (c.Append() |
| + .Sblock('API_%s(this._jsObject) {' % self._namespace.unix_name) |
| + ) |
| + |
| + # Add events to constructor. |
| + for event_name in self._namespace.events: |
| + c.Append("%s = new Event_%s_%s(JS('', '#.%s', this._jsObject));" % |
| + (event_name, self._namespace.unix_name, event_name, event_name)) |
| + |
| + (c.Eblock('}') |
| + .Eblock('}') |
| + ) |
| + return c |
| + |
| + def _GeneratePropertySignature(self, prop): |
| + """Given a property, returns a signature for that property. |
| + Recursively generates the signature for callbacks. |
| + Returns a String for the given property. |
| + |
| + e.g. |
| + bool x |
| + void onClosed() |
| + void doSomething(bool x, void callback([String x])) |
| + """ |
| + if self._IsFunction(prop): |
| + return self._GenerateFunctionSignature(prop.type_.function) |
| + return '%(type)s %(name)s' % { |
| + 'type': self._GetPropertyType(prop), |
| + 'name': prop.simple_name |
| + } |
| + |
| + def _GenerateFunctionSignature(self, function): |
| + """Given a function object, returns the signature for that function. |
| + Recursively generates the signature for callbacks. |
| + Returns a String for the given function. |
| + |
| + If prepend_this is True, adds "this." to the function's name. |
| + |
| + e.g. |
| + void onClosed() |
| + bool isOpen([String type]) |
| + void doSomething(bool x, void callback([String x])) |
| + |
| + e.g. If prepend_this is True: |
| + void this.onClosed() |
| + bool this.isOpen([String type]) |
| + void this.doSomething(bool x, void callback([String x])) |
| + """ |
| + sig = '%(return_type)s %(name)s(%(params)s)' |
| + |
| + if function.returns: |
| + return_type = self._GetPropertyType(function.returns) |
| + else: |
| + return_type = 'void' |
| + |
| + return sig % { |
| + 'return_type': return_type, |
| + 'name': function.simple_name, |
| + 'params': self._GenerateParameterList(function.params, |
| + function.callback) |
| + } |
| + |
| + def _GenerateParameterList(self, params, callback=None, |
| + allow_optional=True): |
|
not at google - send to devlin
2013/01/31 02:08:48
if it can't fit on 1 line then align parameters ve
sashab
2013/01/31 04:41:40
Done.
|
| + """Given a list of function parameters, generates their signature (as a |
| + string). |
| + |
| + e.g. |
| + [String type] |
| + bool x, void callback([String x]) |
| + |
| + If allow_optional is False, ignores optional parameters. Useful for |
| + callbacks, where optional parameters are not used. |
| + """ |
| + # Params lists (required & optional), to be joined with ,'s. |
|
not at google - send to devlin
2013/01/31 02:08:48
commas?
sashab
2013/01/31 04:41:40
Done.
|
| + # FIXME(sashab): assuming all optional params come after required ones |
|
not at google - send to devlin
2013/01/31 02:08:48
I can't parse this sentence. Should it be like "Do
sashab
2013/01/31 04:41:40
Haha, yes, you're right. Fixed
|
| + params_req = [] |
| + params_opt = [] |
| + for param in params: |
| + p_sig = self._GeneratePropertySignature(param) |
| + if allow_optional and param.optional: |
| + params_opt.append(p_sig) |
| + else: |
| + params_req.append(p_sig) |
|
not at google - send to devlin
2013/01/31 02:08:48
this isn't ignoring optional parameters, it's maki
sashab
2013/01/31 04:41:40
Sorry, comment changed. And I also changed the nam
|
| + |
| + # Add the callback, if it exists. |
| + if callback: |
| + c_sig = self._GenerateFunctionSignature(callback) |
| + if callback.optional: |
| + params_opt.append(c_sig) |
| + else: |
| + params_req.append(c_sig) |
| + |
| + # Join the parameters, wrapping the optional params in square brackets. |
| + if params_opt: |
| + params_opt[0] = '[%s' % params_opt[0] |
| + params_opt[-1] = '%s]' % params_opt[-1] |
|
not at google - send to devlin
2013/01/31 02:08:48
a bit bizarre but much better, and I can't think o
sashab
2013/01/31 04:41:40
Done. Or should this rather be in the function sig
not at google - send to devlin
2013/02/02 00:45:47
What you have is good.
|
| + param_sets = [', '.join(params_req), |
| + ', '.join(params_opt)] |
|
not at google - send to devlin
2013/01/31 02:08:48
single line
sashab
2013/01/31 04:41:40
Done.
|
| + |
| + return ', '.join(p for p in param_sets if p) |
|
not at google - send to devlin
2013/01/31 02:08:48
why the "if p"? I.e. why can't it be ', '.join(par
sashab
2013/01/31 04:41:40
This is part of the bizarre logic. If there are no
|
| + |
| + def _TryAppendOverride(self, c, type_, prop, key_suffix='', add_doc=False): |
|
not at google - send to devlin
2013/01/31 02:08:48
TryAppendOverride is a bit of an awkward name. Per
sashab
2013/01/31 04:41:40
It returns True if it appended the override, False
|
| + """Given a particular type and property to find in the custom dart |
| + overrides, checks whether there is an override for that key. |
| + If there is, adds a newline, appends the override code, and returns True. |
|
not at google - send to devlin
2013/01/31 02:08:48
"adds a newline" -> doesn't seem like this should
sashab
2013/01/31 04:41:40
Ok, the reason this is needed is in case the overr
|
| + If not, returns False. |
| + |
| + |key_suffix| will be added to the end of the key before searching, e.g. |
| + '.set' or '.get' can be used for setters and getters respectively. |
| + |
| + If add_doc is given, adds the documentation for this property before the |
| + override code. |
| + If the override code is empty, does nothing (and does not add |
| + documentation), but returns True (treats it as a valid override). |
| + """ |
| + contents = self._custom_dart.get('%s.%s%s' % (type_.name, prop.name, |
| + key_suffix)) |
| + if contents is None: |
| + return False |
| + |
|
not at google - send to devlin
2013/01/31 02:08:48
maybe early-exit here too, if it's empty:
if cont
sashab
2013/01/31 04:41:40
Good idea! :D
|
| + if contents.strip(): |
| + if prop is not None: |
|
not at google - send to devlin
2013/01/31 02:08:48
although TBH I find this idiom a bit strange. Peop
sashab
2013/01/31 04:41:40
It's a little tough, because its unclear: does put
|
| + (c.Append() |
| + .Concat(self._GenerateDocumentation(prop)) |
| + ) |
| + else: |
| + c.Append() |
|
not at google - send to devlin
2013/01/31 02:08:48
seem to be always appending here, so pull out abov
sashab
2013/01/31 04:41:40
Thanks, fixed this one; see comment above about ca
|
| + for line in contents.split('\n'): |
| + c.Append(line) |
| + return True |
| + |
| + def _IsFunction(self, prop): |
| + """Given a model.Property, returns whether this type is a function. |
| + """ |
| + return prop.type_.property_type == PropertyType.FUNCTION |
| + |
| + def _IsObjectType(self, prop): |
| + """Given a model.Property, returns whether this type is an object. |
| + """ |
| + return prop.type_.property_type in [PropertyType.OBJECT, PropertyType.ANY] |
| + |
| + def _IsBaseType(self, prop): |
| + """Given a model.Property, returns whether this type is a base type |
| + (string, number or boolean). |
| + """ |
| + return (self._GetPropertyType(prop) in |
| + ['bool', 'num', 'int', 'double', 'String']) |
| + |
| + def _GetPropertyType(self, prop, container_type=False): |
|
not at google - send to devlin
2013/01/31 02:08:48
call this _GetDartType and pass in the type rather
sashab
2013/01/31 04:41:40
Yup, was going to do this before. Made a wrapper _
|
| + """Given a model.Property object, returns its type as a Dart string. |
| + |
| + If container_type is True, returns the type of the objects prop is |
| + containing, rather than the type of prop itself. |
| + """ |
| + if prop is None: |
| + return 'void' |
| + |
| + if container_type: |
| + type_ = prop.type_.item_type |
| + else: |
| + type_ = prop.type_ |
| + prop_type = type_.property_type |
| + |
| + if prop_type is None: |
| + return 'void' |
| + elif prop_type is PropertyType.REF: |
| + if GetNamespace(type_.ref_type) == self._namespace.name: |
| + # This type is in this namespace; just use its base name. |
| + return GetBaseNamespace(type_.ref_type) |
| + else: |
| + # TODO(sashab): Work out how to import this foreign type. |
| + return type_.ref_type |
| + elif prop_type is PropertyType.BOOLEAN: |
| + return 'bool' |
| + elif prop_type is PropertyType.INTEGER: |
| + return 'int' |
| + elif prop_type is PropertyType.INT64: |
| + return 'num' |
| + elif prop_type is PropertyType.DOUBLE: |
| + return 'double' |
| + elif prop_type is PropertyType.STRING: |
| + return 'String' |
| + elif prop_type is PropertyType.ENUM: |
| + return 'String' |
| + elif prop_type is PropertyType.CHOICES: |
| + # TODO: What is a Choices type? Is it closer to a Map Dart object? |
| + return 'Object' |
|
not at google - send to devlin
2013/01/31 02:08:48
I wrote a long thing in response to what you said,
sashab
2013/01/31 04:41:40
'Object' is the equivalent of untyped. See other c
|
| + elif prop_type is PropertyType.ANY: |
| + return 'Object' |
| + elif prop_type is PropertyType.OBJECT: |
| + # TODO(sashab): Work out a mapped type name. |
| + return type_.instance_of or 'Object' |
| + elif prop_type is PropertyType.FUNCTION: |
| + return 'Function' |
| + elif prop_type is PropertyType.ARRAY: |
| + if type_.item_type: |
|
not at google - send to devlin
2013/01/31 02:08:48
there should always be a list type, this check sho
sashab
2013/01/31 04:41:40
After investigating, you're right - its invalid ID
|
| + return 'List<%s>' % self._GetPropertyType(prop, container_type=True) |
| + else: |
| + return 'List' |
| + elif prop_type is PropertyType.BINARY: |
| + return 'String' |
| + else: |
| + raise NotImplementedError(prop_type) |
| + |