From be574c2e3e4f2ef9f6461b10d1cd304abb958ef1 Mon Sep 17 00:00:00 2001 From: David Morgan Date: Sun, 12 Apr 2015 15:02:11 +0200 Subject: [PATCH] Add generator, tests and example with tests. --- .gitignore | 10 + AUTHORS | 6 + CHANGELOG.md | 5 + CONTRIBUTING.md | 34 ++ LICENSE | 28 ++ README.md | 18 + built_value/LICENSE | 28 ++ built_value/lib/built_value.dart | 61 ++++ built_value/pubspec.yaml | 13 + built_value_generator/LICENSE | 28 ++ .../lib/built_value_generator.dart | 317 ++++++++++++++++++ built_value_generator/pubspec.yaml | 20 ++ .../test/built_value_generator_test.dart | 237 +++++++++++++ example/lib/compound_value.dart | 41 +++ example/lib/compound_value.g.dart | 55 +++ example/lib/value.dart | 44 +++ example/lib/value.g.dart | 107 ++++++ example/pubspec.yaml | 18 + example/test/compound_value_test.dart | 20 ++ example/test/value_test.dart | 129 +++++++ example/tools/build.dart | 13 + lib/built_value.dart | 6 - lib/src/built_value.dart | 1 - pubspec.yaml | 11 - test/all_tests.dart | 11 - test/built_value_test.dart | 17 - 26 files changed, 1232 insertions(+), 46 deletions(-) create mode 100644 .gitignore create mode 100644 AUTHORS create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 built_value/LICENSE create mode 100644 built_value/lib/built_value.dart create mode 100644 built_value/pubspec.yaml create mode 100644 built_value_generator/LICENSE create mode 100644 built_value_generator/lib/built_value_generator.dart create mode 100644 built_value_generator/pubspec.yaml create mode 100644 built_value_generator/test/built_value_generator_test.dart create mode 100644 example/lib/compound_value.dart create mode 100644 example/lib/compound_value.g.dart create mode 100644 example/lib/value.dart create mode 100644 example/lib/value.g.dart create mode 100644 example/pubspec.yaml create mode 100644 example/test/compound_value_test.dart create mode 100644 example/test/value_test.dart create mode 100644 example/tools/build.dart delete mode 100644 lib/built_value.dart delete mode 100644 lib/src/built_value.dart delete mode 100644 pubspec.yaml delete mode 100644 test/all_tests.dart delete mode 100644 test/built_value_test.dart diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3296fe5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.buildlog +.DS_Store +.idea +.pub/ +.settings/ +*.iml +**/build +**/packages +**/pubspec.lock +**/.packages diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..ecf702c6 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,6 @@ +# Below is a list of people and organizations that have contributed +# to the project. Names should be added to the list like so: +# +# Name/Organization + +David Morgan/Google Inc. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..ca3ae24d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 0.0.1 + +- Generator, tests and example. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..f1322f8f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,34 @@ +Want to contribute? Great! First, read this page (including the small print at +the end). + +### Before you contribute +Before we can use your code, you must sign the +[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) +(CLA), which you can do online. The CLA is necessary mainly because you own the +copyright to your changes, even after your contribution becomes part of our +codebase, so we need your permission to use and distribute your code. We also +need to be sure of various other things—for instance that you'll tell us if you +know that your code infringes on other people's patents. You don't have to sign +the CLA until after you've submitted your code for review and a member has +approved it, but you must do it before we can put your code into our codebase. + +Before you start working on a larger contribution, you should get in touch with +us first through the issue tracker with your idea so that we can help out and +possibly guide you. Coordinating up front makes it much easier to avoid +frustration later on. + +### Code reviews +All submissions, including submissions by project members, require review. We +use Github pull requests for this purpose. + +### File headers +All files in the project must start with the following header. + + // Copyright (c) 2015, Google Inc. 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. + +### The small print +Contributions made by corporations are covered by a different agreement than the +one above, the +[Software Grant and Corporate Contributor License Agreement](https://developers.google.com/open-source/cla/corporate). diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..ec8a981f --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +Copyright 2015, Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..bef84241 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Built Values for Dart + +Built values are value types for Dart. + +They have generated `equals`, `hashCode`, `toString` and builder class. + +This provides an easy way to write deeply immutable classes for Dart that +behave in a consistent, predicatable way. + +See +[this example](https://github.com/google/built_value.dart/tree/master/example) +for a full project with a `build.dart` and some example value types. + +## Features and bugs + +Please file feature requests and bugs at the [issue tracker][tracker]. + +[tracker]: https://github.com/google/built_value.dart/issues diff --git a/built_value/LICENSE b/built_value/LICENSE new file mode 100644 index 00000000..ec8a981f --- /dev/null +++ b/built_value/LICENSE @@ -0,0 +1,28 @@ +Copyright 2015, Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/built_value/lib/built_value.dart b/built_value/lib/built_value.dart new file mode 100644 index 00000000..c79c7cf1 --- /dev/null +++ b/built_value/lib/built_value.dart @@ -0,0 +1,61 @@ +// Copyright (c) 2015, Google Inc. 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. + +library built_value; + +export 'package:quiver/core.dart' show hashObjects; + +/// Implement this for a Built Value. +/// +/// Then use built_value_generator.dart code generation functionality to +/// provide the rest of the implementation. +/// +/// See +abstract class Built, B extends Builder> { + /// Rebuilds the instance. + /// + /// The result is the same as this instance but with [updates] applied. + /// [updates] is a function that takes a builder [B]. + /// + /// The implementation of this method will be generated for you by the + /// built_value generator. + V rebuild(updates(B builder)); + + /// Converts the instance to a builder [B]. + /// + /// The implementation of this method will be generated for you by the + /// built_value generator. + B toBuilder(); +} + +/// Every Built Value must have a [Builder] class. +/// +/// Use it to set defaults, if needed, and to do validation. +/// +/// See +abstract class Builder, B extends Builder> { + /// Replaces the value in the builder with a new one. + /// + /// The implementation of this method will be generated for you by the + /// built_value generator. + void replace(V value); + + /// Applies updates. + /// + /// [updates] is a function that takes a builder [B]. + void update(updates(B builder)); + + /// Builds. + /// + /// The implementation of this method will be generated for you by the + /// built_value generator. + /// + /// Override this method to add validation at build time. + V build(); +} + +// Nullable annotation for Built Value fields. +// +// Fields marked with this annotation are allowed to be null. +const String nullable = 'nullable'; diff --git a/built_value/pubspec.yaml b/built_value/pubspec.yaml new file mode 100644 index 00000000..495e1979 --- /dev/null +++ b/built_value/pubspec.yaml @@ -0,0 +1,13 @@ +name: built_value +version: 0.0.1 +description: > + Value types with builders. This library is the runtime dependency. +authors: +- David Morgan +homepage: https://github.com/google/built_value.dart + +environment: + sdk: '>=1.8.0 <2.0.0' + +dependencies: + quiver: '>=0.21.0 <0.22.0' diff --git a/built_value_generator/LICENSE b/built_value_generator/LICENSE new file mode 100644 index 00000000..ec8a981f --- /dev/null +++ b/built_value_generator/LICENSE @@ -0,0 +1,28 @@ +Copyright 2015, Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/built_value_generator/lib/built_value_generator.dart b/built_value_generator/lib/built_value_generator.dart new file mode 100644 index 00000000..9f131208 --- /dev/null +++ b/built_value_generator/lib/built_value_generator.dart @@ -0,0 +1,317 @@ +// Copyright (c) 2015, Google Inc. 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. + +library built_value_generator; + +import 'dart:async'; +import 'package:built_collection/built_collection.dart'; +import 'package:quiver/iterables.dart' show concat; + +import 'package:analyzer/src/generated/element.dart'; +import 'package:source_gen/source_gen.dart'; + +/// Generator for Built Values. +/// +/// See +class BuiltValueGenerator extends Generator { + Future generate(Element element) async { + if (element is! ClassElement) { + return null; + } + final classElement = element as ClassElement; + + // TODO(davidmorgan): more exact type check. + if (!classElement.allSupertypes + .any((interfaceType) => interfaceType.name == 'Built')) { + return null; + } + + if (classElement.displayName.startsWith('_\$')) { + return null; + } + + final fields = getFields(classElement); + + final builderClassElement = + element.library.getType(classElement.displayName + 'Builder'); + final builderFields = getBuilderFields(builderClassElement); + final errors = concat([ + checkPart(classElement), + checkValueClass(classElement), + checkBuilderClass(builderClassElement), + checkFields(fields, builderFields), + ]); + + if (errors.isNotEmpty) { + throw new InvalidGenerationSourceError( + 'Please make changes to use built_value.', + todo: errors.join(' ')); + } + + return generateCode(classElement.displayName, fields, builderFields); + } + + Iterable checkPart(ClassElement classElement) { + final fileName = + classElement.library.source.shortName.replaceAll('.dart', ''); + final expectedCode = "part '$fileName.g.dart';"; + return classElement.library.source.contents.data.contains(expectedCode) + ? [] + : ['Import generated part: $expectedCode']; + } + + Iterable checkValueClass(ClassElement classElement) { + final result = []; + final name = classElement.displayName; + + if (!classElement.isAbstract) { + result.add('Make class abstract'); + } + + final expectedConstructor = '$name._();'; + final constructors = classElement.constructors + .where((constructor) => !constructor.isFactory); + if (constructors.length != 1 || + constructors.single.isSynthetic || + constructors.single.computeNode().toSource() != expectedConstructor) { + result + .add('Make class have exactly one constructor: $expectedConstructor'); + } + + final expectedFactory = + 'factory $name([updates(${name}Builder b)]) = _\$$name;'; + final factories = + classElement.constructors.where((constructor) => constructor.isFactory); + if (factories.length != 1 || + factories.single.computeNode().toSource() != expectedFactory) { + result.add('Make class have exactly one factory: $expectedFactory'); + } + + return result; + } + + Iterable checkBuilderClass(ClassElement classElement) { + if (classElement == null) { + return ['Add abstract class ValueBuilder']; + } + + final result = []; + final name = classElement.displayName; + + if (!classElement.isAbstract) { + result.add('Make builder class abstract'); + } + + final expectedConstructor = '$name._();'; + final constructors = classElement.constructors + .where((constructor) => !constructor.isFactory); + if (constructors.length != 1 || + constructors.single.isSynthetic || + constructors.single.computeNode().toSource() != expectedConstructor) { + result.add( + 'Make builder class have exactly one constructor: $expectedConstructor'); + } + + final expectedFactory = 'factory $name() = _\$$name;'; + final factories = + classElement.constructors.where((constructor) => constructor.isFactory); + if (factories.length != 1 || + factories.single.computeNode().toSource() != expectedFactory) { + result + .add('Make builder class have exactly one factory: $expectedFactory'); + } + + return result; + } + + List getFields(ClassElement classElement) { + final result = []; + for (final field in classElement.fields) { + if (!field.isStatic) result.add(field); + } + return result; + } + + List getBuilderFields(ClassElement classElement) { + final result = []; + if (classElement == null) { + return result; + } + for (final field in classElement.fields) { + if (!field.isStatic) result.add(field); + } + return result; + } + + Iterable checkFields( + Iterable fields, Iterable builderFields) { + final result = []; + var checkFieldTypes = true; + for (final field in fields) { + final fieldName = field.displayName; + if (field.getter == null || !field.getter.isAbstract) { + checkFieldTypes = false; + result.add('Make field $fieldName an abstract getter'); + } + } + for (final field in builderFields) { + final fieldName = field.displayName; + if (field.getter == null || + field.getter.isAbstract || + !field.getter.isSynthetic) { + checkFieldTypes = false; + result.add('Make builder field $fieldName a normal field'); + } + } + + final fieldNames = + new BuiltList(fields.map((field) => field.displayName)); + final builderFieldNames = + new BuiltList(builderFields.map((field) => field.displayName)); + if (fieldNames != builderFieldNames) { + result.add( + 'Make builder have exactly these fields: ' + fieldNames.join(', ')); + checkFieldTypes = false; + } + + if (checkFieldTypes) { + for (int i = 0; i != fields.length; ++i) { + final field = fields.elementAt(i); + final fieldType = field.getter.returnType.displayName; + final builderField = builderFields.elementAt(i); + final builderFieldType = builderField.getter.returnType.displayName; + if (fieldType != builderFieldType && + // TODO(davidmorgan): smarter check for builder types. + fieldType.replaceAll('Built', '') != + builderFieldType.replaceAll('Builder', '')) { + result.add( + 'Make builder field ${field.displayName} have type $fieldType'); + } + } + } + + return result; + } + + String generateCode(String className, List fields, + List builderFields) { + final result = new StringBuffer(); + + final fieldNames = fields.map((field) => field.displayName); + + final nullableFields = fields.where((field) => field.getter.metadata.any( + (metadata) => metadata.evaluationResult.value.value == 'nullable')); + final nullableFieldNames = nullableFields.map((field) => field.displayName); + + final buildableFields = builderFields.where( + (field) => field.getter.returnType.displayName.contains('Builder')); + final buildableFieldNames = + buildableFields.map((field) => field.displayName); + + result.writeln('class _\$$className extends $className {'); + for (final field in fields) { + final fieldName = field.displayName; + final fieldType = field.getter.returnType.displayName; + result.writeln('final $fieldType $fieldName;'); + } + + result.write('_\$$className._({'); + result.write(fieldNames.map((name) => 'this.$name').join(', ')); + result.write('}) : super._() {'); + for (final fieldName in fieldNames) { + if (!nullableFieldNames.contains(fieldName)) { + result.writeln( + "if ($fieldName == null) throw new ArgumentError('null $fieldName');"); + } + } + result.write('}'); + + result.writeln('factory _\$$className([updates(${className}Builder b)]) ' + '=> (new ${className}Builder()..update(updates)).build();'); + result.writeln('$className rebuild(updates(${className}Builder b)) ' + '=> (toBuilder()..update(updates)).build();'); + result.writeln('_\$${className}Builder toBuilder() ' + '=> new _\$${className}Builder()..replace(this);'); + + result.writeln('bool operator==(other) {'); + result.writeln(' if (other is! $className) return false;'); + if (fields.length == 0) { + result.writeln('return true;'); + } else { + result.writeln('return'); + result.writeln(fieldNames + .map((fieldName) => '$fieldName == other.$fieldName') + .join('&&')); + result.writeln(';'); + } + result.writeln('}'); + + result.writeln('int get hashCode {'); + if (fields.length == 0) { + result.writeln('return ${className.hashCode};'); + } else { + result.writeln('return hashObjects(['); + result.writeln(fieldNames.join(', ')); + result.writeln(']);'); + } + result.writeln('}'); + + result.writeln('String toString() {'); + if (fields.length == 0) { + result.writeln("return '$className {}';"); + } else { + result.writeln("return '$className {'"); + result.writeln(fieldNames + .map((fieldName) => "'$fieldName=\${$fieldName.toString()}\\n'") + .join('')); + result.writeln("'}'"); + result.writeln(';'); + } + result.writeln('}'); + + result.writeln('}'); + + result + .writeln('class _\$${className}Builder extends ${className}Builder {'); + result.writeln(); + result.writeln('_\$${className}Builder() : super._();'); + + for (final field in builderFields) { + final fieldName = field.displayName; + final fieldType = field.getter.returnType.displayName; + result.writeln('$fieldType get $fieldName => super.$fieldName;'); + result.writeln('void set $fieldName($fieldType $fieldName) {'); + if (!nullableFieldNames.contains(fieldName)) { + result.writeln( + "if ($fieldName == null) throw new ArgumentError('null $fieldName');"); + } + result.writeln('super.$fieldName = $fieldName;'); + result.writeln('}'); + result.writeln(); + } + + result.writeln('void replace(${className} other) {'); + result.writeln((fieldNames.map((name) { + return buildableFieldNames.contains(name) + ? 'super.$name = other.$name.toBuilder();' + : 'super.$name = other.$name;'; + })).join('\n')); + result.writeln('}'); + + result.writeln('void update(updates(${className}Builder b)) {' + ' if (updates != null) updates(this); }'); + result.writeln('$className build() => new _\$$className._('); + result.write(builderFields.map((field) { + final fieldName = field.displayName; + + return buildableFieldNames.contains(fieldName) + ? '$fieldName: $fieldName.build()' + : '$fieldName: $fieldName'; + }).join(', ')); + result.write(');'); + result.writeln('}'); + + return result.toString(); + } +} diff --git a/built_value_generator/pubspec.yaml b/built_value_generator/pubspec.yaml new file mode 100644 index 00000000..305dfe4d --- /dev/null +++ b/built_value_generator/pubspec.yaml @@ -0,0 +1,20 @@ +name: built_value_generator +version: 0.0.1 +description: > + Value types with builders. This library is the dev dependency. +authors: +- David Morgan +homepage: https://github.com/google/built_value.dart + +environment: + sdk: '>=1.12.0-dev.5.10 <2.0.0' + +dependencies: + analyzer: '>=0.26.1 <1.0.0' + built_collection: '^1.0.0' + built_value: '^0.0.1' + source_gen: '>=0.4.3 <1.0.0' + quiver: '>=0.21.0 <0.22.0' + +dev_dependencies: + test: any diff --git a/built_value_generator/test/built_value_generator_test.dart b/built_value_generator/test/built_value_generator_test.dart new file mode 100644 index 00000000..7ef07d13 --- /dev/null +++ b/built_value_generator/test/built_value_generator_test.dart @@ -0,0 +1,237 @@ +// Copyright (c) 2015, Google Inc. 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. + +import 'dart:async'; +import 'dart:io'; + +import 'package:built_value_generator/built_value_generator.dart'; +import 'package:source_gen/source_gen.dart'; +import 'package:test/test.dart'; + +void main() { + group('generator', () { + test('suggests to import part file', () async { + expect(await generate('''library value; +import 'package:built_value/built_value.dart'; +abstract class Value extends Built { + Value._(); + factory Value([updates(ValueBuilder b)]) = _\$Value; +} +abstract class ValueBuilder extends Builder { + ValueBuilder._(); + factory ValueBuilder() = _\$ValueBuilder; +}'''), contains("TODO: Import generated part: part 'value.g.dart';")); + }); + + test('suggests to make value class abstract', () async { + expect(await generate('''library value; +import 'package:built_value/built_value.dart'; +part 'value.g.dart'; +class Value extends Built { + Value._(); + factory Value([updates(ValueBuilder b)]) = _\$Value; +} +abstract class ValueBuilder extends Builder { + ValueBuilder._(); + factory ValueBuilder() = _\$ValueBuilder; +}'''), contains("TODO: Make class abstract")); + }); + + test('suggests to add constructor to value class', () async { + expect( + await generate('''library value; +import 'package:built_value/built_value.dart'; +part 'value.g.dart'; +abstract class Value extends Built { + factory Value([updates(ValueBuilder b)]) = _\$Value; +} +abstract class ValueBuilder extends Builder { + ValueBuilder._(); + factory ValueBuilder() = _\$ValueBuilder; +}'''), + contains( + "TODO: Make class have exactly one constructor: Value._();")); + }); + + test('suggests to add factory to value class', () async { + expect( + await generate('''library value; +import 'package:built_value/built_value.dart'; +part 'value.g.dart'; +abstract class Value extends Built { + Value._(); +} +abstract class ValueBuilder extends Builder { + ValueBuilder._(); + factory ValueBuilder() = _\$ValueBuilder; +}'''), + contains("TODO: Make class have exactly one factory: " + "factory Value([updates(ValueBuilder b)]) = _\$Value;")); + }); + + test('suggests to add builder class', () async { + expect(await generate('''library value; +import 'package:built_value/built_value.dart'; +part 'value.g.dart'; +abstract class Value extends Built { + Value._(); + factory Value([updates(ValueBuilder b)]) = _\$Value; +}'''), contains("TODO: Add abstract class ValueBuilder")); + }); + + test('suggests to make builder class abstract', () async { + expect(await generate('''library value; +import 'package:built_value/built_value.dart'; +part 'value.g.dart'; +abstract class Value extends Built { + Value._(); + factory Value([updates(ValueBuilder b)]) = _\$Value; +} +class ValueBuilder extends Builder { + ValueBuilder._(); + factory ValueBuilder() = _\$ValueBuilder; +}'''), contains("TODO: Make builder class abstract")); + }); + + test('suggests to add constructor to builder class', () async { + expect( + await generate('''library value; +import 'package:built_value/built_value.dart'; +part 'value.g.dart'; +abstract class Value extends Built { + Value._(); + factory Value([updates(ValueBuilder b)]) = _\$Value; +} +abstract class ValueBuilder extends Builder { + factory ValueBuilder() = _\$ValueBuilder; +}'''), + contains("TODO: Make builder class " + "have exactly one constructor: ValueBuilder._();")); + }); + + test('suggests to add factory to builder class', () async { + expect( + await generate('''library value; +import 'package:built_value/built_value.dart'; +part 'value.g.dart'; +abstract class Value extends Built { + Value._(); + factory Value([updates(ValueBuilder b)]) = _\$Value; +} +abstract class ValueBuilder extends Builder { + ValueBuilder._(); +}'''), + contains("TODO: Make builder class have exactly one factory: " + "factory ValueBuilder() = _\$ValueBuilder;")); + }); + + test('suggests value fields must be getters', () async { + expect( + await generate('''library value; +import 'package:built_value/built_value.dart'; +part 'value.g.dart'; +abstract class Value extends Built { + Value._(); + factory Value([updates(ValueBuilder b)]) = _\$Value; + int foo; +} +abstract class ValueBuilder extends Builder { + ValueBuilder._(); + factory ValueBuilder() = _\$ValueBuilder; + int foo; +}'''), + contains("TODO: Make field foo an abstract getter")); + }); + + test('suggests builder fields must be getters', () async { + expect( + await generate('''library value; +import 'package:built_value/built_value.dart'; +part 'value.g.dart'; +abstract class Value extends Built { + Value._(); + factory Value([updates(ValueBuilder b)]) = _\$Value; + int get foo; +} +abstract class ValueBuilder extends Builder { + ValueBuilder._(); + factory ValueBuilder() = _\$ValueBuilder; + int get foo; +}'''), + contains("TODO: Make builder field foo a normal field")); + }); + + test('suggests builder fields must be in sync', () async { + expect( + await generate('''library value; +import 'package:built_value/built_value.dart'; +part 'value.g.dart'; +abstract class Value extends Built { + Value._(); + factory Value([updates(ValueBuilder b)]) = _\$Value; + int get foo; +} +abstract class ValueBuilder extends Builder { + ValueBuilder._(); + factory ValueBuilder() = _\$ValueBuilder; +}'''), + contains("TODO: Make builder have exactly these fields: foo")); + }); + + test('suggests builder fields must be same type', () async { + expect( + await generate('''library value; +import 'package:built_value/built_value.dart'; +part 'value.g.dart'; +abstract class Value extends Built { + Value._(); + factory Value([updates(ValueBuilder b)]) = _\$Value; + int get foo; +} +abstract class ValueBuilder extends Builder { + ValueBuilder._(); + factory ValueBuilder() = _\$ValueBuilder; + String foo; +}'''), + contains("TODO: Make builder field foo have type int")); + }); + }); +} + +// Test setup. + +Future generate(String source) async { + final tempDir = + Directory.systemTemp.createTempSync('built_value_generator.dart.'); + final packageDir = new Directory(tempDir.path + '/packages')..createSync(); + final builtValueDir = new Directory(packageDir.path + '/built_value') + ..createSync(); + final builtValueFile = new File(builtValueDir.path + '/built_value.dart') + ..createSync(); + builtValueFile.writeAsStringSync(builtValueSource); + + final libDir = new Directory(tempDir.path + '/lib')..createSync(); + final sourceFile = new File(libDir.path + '/value.dart'); + sourceFile.writeAsStringSync(source); + + await build([], [new BuiltValueGenerator()], + projectPath: tempDir.path, librarySearchPaths: ['lib']); + final outputFile = new File(libDir.path + '/value.g.dart'); + return outputFile.existsSync() ? outputFile.readAsStringSync() : ''; +} + +const String builtValueSource = r''' +library built_value; + +abstract class Built, B extends Builder> { + V rebuild(updates(B builder)); + B toBuilder(); +} + +abstract class Builder, B extends Builder> { + void replace(V value); + void update(updates(B builder)); + V build(); +} +'''; diff --git a/example/lib/compound_value.dart b/example/lib/compound_value.dart new file mode 100644 index 00000000..1b566edc --- /dev/null +++ b/example/lib/compound_value.dart @@ -0,0 +1,41 @@ +// Copyright (c) 2015, Google Inc. 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. + +library compound_value; + +import 'package:built_value/built_value.dart'; + +import 'value.dart'; + +part 'compound_value.g.dart'; + +/// Example of how to use built_value. +/// +/// The value class must implement [Built]. It must be abstract, and have +/// fields declared as abstract getters. Finally, it must have a particular +/// constructor and factory, as shown here. +abstract class CompoundValue + implements Built { + Value get value; + + CompoundValue._(); + factory CompoundValue([updates(CompoundValueBuilder b)]) = _$CompoundValue; +} + +/// The builder class must implement [Builder]. It must be abstract, and have +/// fields declared as normal public fields. Finally, it must have a particular +/// constructor and factory, as shown here. +/// +/// Defaults can be specified by assigning them to fields here. +/// +/// Validation can be done by overriding the [build] method. +abstract class CompoundValueBuilder + implements Builder { + ValueBuilder value = new ValueBuilder() + ..anInt = 0 + ..aString = '1'; + + CompoundValueBuilder._(); + factory CompoundValueBuilder() = _$CompoundValueBuilder; +} diff --git a/example/lib/compound_value.g.dart b/example/lib/compound_value.g.dart new file mode 100644 index 00000000..1fb1d626 --- /dev/null +++ b/example/lib/compound_value.g.dart @@ -0,0 +1,55 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// 2015-10-10T18:36:55.864Z + +part of second_value; + +// ************************************************************************** +// Generator: BuiltValueGenerator +// Target: abstract class CompoundValue +// ************************************************************************** + +class _$CompoundValue extends CompoundValue { + final Value value; + _$CompoundValue._({this.value}) : super._() { + if (value == null) throw new ArgumentError('null value'); + } + factory _$CompoundValue([updates(CompoundValueBuilder b)]) => + (new CompoundValueBuilder()..update(updates)).build(); + CompoundValue rebuild(updates(CompoundValueBuilder b)) => + (toBuilder()..update(updates)).build(); + _$CompoundValueBuilder toBuilder() => + new _$CompoundValueBuilder()..replace(this); + bool operator ==(other) { + if (other is! CompoundValue) return false; + return value == other.value; + } + + int get hashCode { + return hashObjects([value]); + } + + String toString() { + return 'CompoundValue {' + 'value=${value.toString()}\n' + '}'; + } +} + +class _$CompoundValueBuilder extends CompoundValueBuilder { + _$CompoundValueBuilder() : super._(); + ValueBuilder get value => super.value; + void set value(ValueBuilder value) { + if (value == null) throw new ArgumentError('null value'); + super.value = value; + } + + void replace(CompoundValue other) { + super.value = other.value.toBuilder(); + } + + void update(updates(CompoundValueBuilder b)) { + if (updates != null) updates(this); + } + + CompoundValue build() => new _$CompoundValue._(value: value.build()); +} diff --git a/example/lib/value.dart b/example/lib/value.dart new file mode 100644 index 00000000..e007bdb4 --- /dev/null +++ b/example/lib/value.dart @@ -0,0 +1,44 @@ +// Copyright (c) 2015, Google Inc. 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. + +library value; + +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/built_value.dart'; + +part 'value.g.dart'; + +/// Example of how to use built_value. +/// +/// The value class must implement [Built]. It must be abstract, and have +/// fields declared as abstract getters. Finally, it must have a particular +/// constructor and factory, as shown here. +abstract class Value implements Built { + int get anInt; + String get aString; + @nullable Object get anObject; + int get aDefaultInt; + BuiltList get listOfInt; + + Value._(); + factory Value([updates(ValueBuilder b)]) = _$Value; +} + +/// The builder class must implement [Builder]. It must be abstract, and have +/// fields declared as normal public fields. Finally, it must have a particular +/// constructor and factory, as shown here. +/// +/// Defaults can be specified by assigning them to fields here. +/// +/// Validation can be done by overriding the [build] method. +abstract class ValueBuilder implements Builder { + int anInt; + String aString; + @nullable Object anObject; + int aDefaultInt = 7; + ListBuilder listOfInt = new ListBuilder(); + + ValueBuilder._(); + factory ValueBuilder() = _$ValueBuilder; +} diff --git a/example/lib/value.g.dart b/example/lib/value.g.dart new file mode 100644 index 00000000..8c56aef1 --- /dev/null +++ b/example/lib/value.g.dart @@ -0,0 +1,107 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// 2015-10-10T18:36:55.892Z + +part of value; + +// ************************************************************************** +// Generator: BuiltValueGenerator +// Target: abstract class Value +// ************************************************************************** + +class _$Value extends Value { + final int anInt; + final String aString; + final Object anObject; + final int aDefaultInt; + final BuiltList listOfInt; + _$Value._( + {this.anInt, + this.aString, + this.anObject, + this.aDefaultInt, + this.listOfInt}) + : super._() { + if (anInt == null) throw new ArgumentError('null anInt'); + if (aString == null) throw new ArgumentError('null aString'); + if (aDefaultInt == null) throw new ArgumentError('null aDefaultInt'); + if (listOfInt == null) throw new ArgumentError('null listOfInt'); + } + factory _$Value([updates(ValueBuilder b)]) => + (new ValueBuilder()..update(updates)).build(); + Value rebuild(updates(ValueBuilder b)) => + (toBuilder()..update(updates)).build(); + _$ValueBuilder toBuilder() => new _$ValueBuilder()..replace(this); + bool operator ==(other) { + if (other is! Value) return false; + return anInt == other.anInt && + aString == other.aString && + anObject == other.anObject && + aDefaultInt == other.aDefaultInt && + listOfInt == other.listOfInt; + } + + int get hashCode { + return hashObjects([anInt, aString, anObject, aDefaultInt, listOfInt]); + } + + String toString() { + return 'Value {' + 'anInt=${anInt.toString()}\n' + 'aString=${aString.toString()}\n' + 'anObject=${anObject.toString()}\n' + 'aDefaultInt=${aDefaultInt.toString()}\n' + 'listOfInt=${listOfInt.toString()}\n' + '}'; + } +} + +class _$ValueBuilder extends ValueBuilder { + _$ValueBuilder() : super._(); + int get anInt => super.anInt; + void set anInt(int anInt) { + if (anInt == null) throw new ArgumentError('null anInt'); + super.anInt = anInt; + } + + String get aString => super.aString; + void set aString(String aString) { + if (aString == null) throw new ArgumentError('null aString'); + super.aString = aString; + } + + Object get anObject => super.anObject; + void set anObject(Object anObject) { + super.anObject = anObject; + } + + int get aDefaultInt => super.aDefaultInt; + void set aDefaultInt(int aDefaultInt) { + if (aDefaultInt == null) throw new ArgumentError('null aDefaultInt'); + super.aDefaultInt = aDefaultInt; + } + + ListBuilder get listOfInt => super.listOfInt; + void set listOfInt(ListBuilder listOfInt) { + if (listOfInt == null) throw new ArgumentError('null listOfInt'); + super.listOfInt = listOfInt; + } + + void replace(Value other) { + super.anInt = other.anInt; + super.aString = other.aString; + super.anObject = other.anObject; + super.aDefaultInt = other.aDefaultInt; + super.listOfInt = other.listOfInt.toBuilder(); + } + + void update(updates(ValueBuilder b)) { + if (updates != null) updates(this); + } + + Value build() => new _$Value._( + anInt: anInt, + aString: aString, + anObject: anObject, + aDefaultInt: aDefaultInt, + listOfInt: listOfInt.build()); +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 00000000..1e1d969d --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,18 @@ +name: example +version: 0.0.1 +description: > + Just an example, not for publishing. +authors: +- David Morgan +homepage: https://github.com/google/built_value.dart + +environment: + sdk: '>=1.8.0 <2.0.0' + +dependencies: + built_collection: '^1.0.0' + built_value: '^0.0.1' + +dev_dependencies: + built_value_generator: '^0.0.1' + test: any diff --git a/example/test/compound_value_test.dart b/example/test/compound_value_test.dart new file mode 100644 index 00000000..166f72a0 --- /dev/null +++ b/example/test/compound_value_test.dart @@ -0,0 +1,20 @@ +// Copyright (c) 2015, Google Inc. 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. + +import 'package:example/compound_value.dart'; +import 'package:test/test.dart'; + +void main() { + group('compound value', () { + test('can be instantiated', () { + new CompoundValue(); + }); + + test('allows nested updates', () { + expect(new CompoundValue((b) => b.value + ..anInt = 1 + ..anObject = 2).value.anInt, 1); + }); + }); +} diff --git a/example/test/value_test.dart b/example/test/value_test.dart new file mode 100644 index 00000000..c023982e --- /dev/null +++ b/example/test/value_test.dart @@ -0,0 +1,129 @@ +// Copyright (c) 2015, Google Inc. 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. + +import 'package:example/value.dart'; +import 'package:test/test.dart'; + +void main() { + group('value', () { + test('can be instantiated', () { + new Value((b) => b + ..anInt = 0 + ..aString = ''); + }); + + test('has defaults', () { + expect(new Value((b) => b + ..anInt = 0 + ..aString = '').aDefaultInt, 7); + }); + + test('throws on null for non-nullable fields', () { + expect(() => new Value(), throws); + }); + + test('throws on wrong type assign', () { + expect(() => new Value((b) => b + ..anInt = '0' + ..aString = ''), throws); + }); + + test('nullable fields default to null', () { + final value = new Value((b) => b + ..anInt = 0 + ..aString = ''); + expect(value.anObject, isNull); + }); + + test('fields can be set via build constructor', () { + final value = new Value((b) => b + ..anInt = 1 + ..aString = 'two' + ..anObject = 3); + expect(value.anInt, 1); + expect(value.aString, 'two'); + expect(value.anObject, 3); + }); + + test('fields can be updated via rebuild method', () { + final value = new Value((b) => b + ..anInt = 0 + ..aString = '').rebuild((b) => b + ..anInt = 1 + ..aString = 'two' + ..anObject = 3); + expect(value.anInt, 1); + expect(value.aString, 'two'); + expect(value.anObject, 3); + }); + + test('builder can be instantiated', () { + new ValueBuilder(); + }); + + test('collections can be updated using builder', () { + final value = new Value((b) => b + ..anInt = 1 + ..aString = 'two' + ..listOfInt.update((b) => b + ..addAll([3, 2, 1]) + ..sort())); + expect(value.listOfInt, [1, 2, 3]); + }); + + test('compares equal when equal', () { + final value1 = new Value((b) => b + ..anInt = 0 + ..aString = ''); + final value2 = new Value((b) => b + ..anInt = 0 + ..aString = ''); + expect(value1, value2); + }); + + test('compares not equal when not equal', () { + final value1 = new Value((b) => b + ..anInt = 0 + ..aString = ''); + final value2 = new Value((b) => b + ..anInt = 1 + ..aString = ''); + expect(value1, isNot(equals(value2))); + }); + + test('hashes equal when equal', () { + final value1 = new Value((b) => b + ..anInt = 0 + ..aString = ''); + final value2 = new Value((b) => b + ..anInt = 0 + ..aString = ''); + expect(value1.hashCode, value2.hashCode); + }); + + test('hashes not equal when not equal', () { + final value1 = new Value((b) => b + ..anInt = 0 + ..aString = ''); + final value2 = new Value((b) => b + ..anInt = 1 + ..aString = ''); + expect(value1.hashCode, isNot(equals(value2.hashCode))); + }); + + test('has toString', () { + final value1 = new Value((b) => b + ..anInt = 0 + ..aString = ''); + expect( + value1.toString(), + '''Value {anInt=0 +aString= +anObject=null +aDefaultInt=7 +listOfInt=[] +}'''); + }); + }); +} diff --git a/example/tools/build.dart b/example/tools/build.dart new file mode 100644 index 00000000..61ed94a3 --- /dev/null +++ b/example/tools/build.dart @@ -0,0 +1,13 @@ +// Copyright (c) 2015, Google Inc. 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. + +import 'package:built_value_generator/built_value_generator.dart'; +import 'package:source_gen/source_gen.dart'; + +/// Example of how to use source_gen with [BuiltValueGenerator]. +/// +/// All you need is to import the generators you want and call [build]. +void main(List args) { + build(args, [new BuiltValueGenerator()]).then((result) => print(result)); +} diff --git a/lib/built_value.dart b/lib/built_value.dart deleted file mode 100644 index c1c96912..00000000 --- a/lib/built_value.dart +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) 2015, Google Inc. 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. -library built_value; - -part 'src/built_value.dart'; diff --git a/lib/src/built_value.dart b/lib/src/built_value.dart deleted file mode 100644 index f5dc9105..00000000 --- a/lib/src/built_value.dart +++ /dev/null @@ -1 +0,0 @@ -part of built_value; diff --git a/pubspec.yaml b/pubspec.yaml deleted file mode 100644 index 7dcd5878..00000000 --- a/pubspec.yaml +++ /dev/null @@ -1,11 +0,0 @@ -name: built_value -version: 0.0.1 -description: > - Code generation supporting value types with builders. -authors: -- David Morgan -homepage: https://github.com/google/built-value-dart -dependencies: - source_gen: '>=0.3.0' -dev_dependencies: - unittest: '>=0.10.0' diff --git a/test/all_tests.dart b/test/all_tests.dart deleted file mode 100644 index 585647e9..00000000 --- a/test/all_tests.dart +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2015, Google Inc. 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. - -library built_value.test.all_tests; - -import 'built_value_test.dart' as built_value_test; - -void main() { - built_value_test.main(); -} diff --git a/test/built_value_test.dart b/test/built_value_test.dart deleted file mode 100644 index 2794d2e9..00000000 --- a/test/built_value_test.dart +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2015, Google Inc. 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. - -library built_value.built_value_test; - -import 'package:built_value/built_value.dart'; -import 'package:unittest/unittest.dart'; - -void main() { - group('BuiltValue', () { - test('has a method like Iterable.skipWhile that updates in place', () { - expect( - (new SetBuilder([1, 2])..skipWhile((x) => x == 1)).build(), [2]); - }); - }); -}