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..3e5a0b92d0f6eeb5f4568c6f0ed1e737c9c6a982 |
--- /dev/null |
+++ b/tools/json_schema_compiler/dart_generator.py |
@@ -0,0 +1,584 @@ |
+# 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. |
not at google - send to devlin
2013/02/02 00:45:48
maybe that'll change.
sashab
2013/02/04 05:09:27
Doesn't belong in here anyway. Removed.
|
+""" |
+ |
+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): |
+ def __init__(self, dart_overrides_dir=None): |
+ self._dart_overrides_dir = dart_overrides_dir |
+ |
+ def Generate(self, namespace): |
+ return _Generator(namespace, self._dart_overrides_dir).Generate() |
+ |
+class _Generator(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._type_overrides = {} |
+ 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._type_overrides[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. |
+ 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._ConcatOverride(c, type_, prop, add_doc=True): |
+ # Add the getter. |
+ if not self._ConcatOverride(c, type_, prop, key_suffix='.get', |
+ add_doc=True): |
+ # 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._ConcatOverride(c, type_, prop, key_suffix='.set'): |
+ 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)] |
+ 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 |
+ |
not at google - send to devlin
2013/02/02 00:45:48
extra \n
sashab
2013/02/04 05:09:27
Nice spotting :) Removed.
|
+ |
+ 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() |
+ .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), |
not at google - send to devlin
2013/02/02 00:45:48
You have inspired me to write https://codereview.c
sashab
2013/02/04 05:09:27
Hehe, good change :) Done.
Not sure how to pull y
not at google - send to devlin
2013/02/04 17:21:06
Oh, it should have been submitted already..
sashab
2013/02/04 23:12:45
It doesn't look like its landed?
|
+ "'#.%s(%s)'" % (function.name, |
+ ', '.join(['#'] * n_params)), |
not at google - send to devlin
2013/02/02 00:45:48
can fit on 1 line
sashab
2013/02/04 05:09:27
Done.
|
+ 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, |
+ convert_optional=True) |
+ |
+ 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, |
+ convert_optional=False): |
+ """Given a list of function parameters, generates their signature (as a |
+ string). |
+ |
+ e.g. |
+ [String type] |
+ bool x, void callback([String x]) |
+ |
+ If convert_optional is True, changes optional parameters to be required. |
+ Useful for callbacks, where optional parameters are treated as required. |
+ """ |
+ # Params lists (required & optional), to be joined with commas. |
+ # TODO(sashab): Don't assume optional params always come after required |
+ # ones. |
+ params_req = [] |
+ params_opt = [] |
+ for param in params: |
+ p_sig = self._GeneratePropertySignature(param) |
+ if param.optional and not convert_optional: |
+ params_opt.append(p_sig) |
+ else: |
+ params_req.append(p_sig) |
+ |
+ # 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 with commas. |
+ # Optional parameters have to be in square brackets, e.g.: |
+ # |
+ # required params | optional params | output |
+ # [] | [] | '' |
+ # [x, y] | [] | 'x, y' |
+ # [] | [a, b] | '[a, b]' |
+ # [x, y] | [a, b] | 'x, y, [a, b]' |
+ if params_opt: |
+ params_opt[0] = '[%s' % params_opt[0] |
+ params_opt[-1] = '%s]' % params_opt[-1] |
+ param_sets = [', '.join(params_req), ', '.join(params_opt)] |
+ |
+ # The 'if p' part here is needed to prevent commas where there are no |
+ # parameters of a certain type. |
+ # If there are no optional parameters, this prevents a _trailing_ comma, |
+ # e.g. '(x, y,)'. Similarly, if there are no required parameters, this |
+ # prevents a leading comma, e.g. '(, [a, b])'. |
+ return ', '.join(p for p in param_sets if p) |
+ |
+ def _ConcatOverride(self, c, type_, prop, key_suffix='', add_doc=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. |
+ 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._type_overrides.get('%s.%s%s' % (type_.name, prop.name, |
+ key_suffix)) |
+ if contents is None: |
+ return False |
+ |
+ if contents.strip() == '': |
+ # Passing in an empty file here is the idiom for disabling compilation of |
+ # a function for dart. |
+ return Code() |
not at google - send to devlin
2013/02/02 00:45:48
This is going to change, right?
sashab
2013/02/04 05:09:27
Yes, I changed it now. And I removed the "adding a
|
+ |
+ c.Append() |
+ if prop is not None: |
+ c.Concat(self._GenerateDocumentation(prop)) |
+ 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): |
not at google - send to devlin
2013/02/02 00:45:48
Since it's so few places that call this (and about
sashab
2013/02/04 05:09:27
This is here in case prop is None, in which case p
|
+ """Given a model.Property object, returns its type as a Dart string. |
+ """ |
+ |
+ if prop is None: |
+ return 'void' |
not at google - send to devlin
2013/02/02 00:45:48
Once function.returns is a type I don't think this
sashab
2013/02/04 05:09:27
Done.
|
+ return self._GetDartType(prop.type_) |
+ |
+ def _GetDartType(self, type_): |
+ """Given a model.Type object, returns its type as a Dart string. |
+ """ |
+ 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 StripNamespace(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' |
+ 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: |
+ return 'List<%s>' % self._GetDartType(type_.item_type) |
+ elif prop_type is PropertyType.BINARY: |
+ return 'String' |
+ else: |
+ raise NotImplementedError(prop_type) |
+ |