├── .gitignore ├── .vscode └── launch.json ├── README.md ├── data_classes ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example │ └── main.dart ├── lib │ └── data_classes.dart ├── pubspec.lock └── pubspec.yaml ├── data_classes_generator ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.yaml ├── lib │ └── data_classes_generator.dart └── pubspec.yaml └── example ├── lib ├── colors.dart └── main.dart └── pubspec.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.lock 2 | **/*.g.dart 3 | 4 | data_classes/.dart_tool 5 | data_classes/.packages 6 | data_classes_generator/.dart_tool 7 | data_classes_generator/.packages 8 | example/.dart_tool 9 | example/.packages 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Dart", 9 | "program": "bin/main.dart", 10 | "request": "launch", 11 | "type": "dart" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Welcome to the Git repo of the 2 | [`data_classes` pub package](pub.dev/packages/data_classes). 3 | 4 | You're welcome to contribute, just file an issue or open a pull request! 5 | -------------------------------------------------------------------------------- /data_classes/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [3.0.0] - 2019-10-21 2 | 3 | - Support value getters by annotating the corresponding field with 4 | `@GenerateValueGetters()`. You can optionally set `generateNegations` and 5 | `usePrefix` to `true`. 6 | - Update readme. 7 | 8 | ## [2.1.0] - 2019-10-20 9 | 10 | - Make example not show any errors by ignoring `undefined_class` and 11 | `uri_has_nont_been_generated`. 12 | - Replace unused `generateCopy` field of `GenerateDataClassFor` class with 13 | `generateCopyWith`. 14 | - `copyWith` method now gets generated if you opt-in. Only works if all the 15 | fields are non-nullable. 16 | - Make code more helpful by adding helpful comments at some places and using a 17 | `StringBuffer` instead of returning the output right away. 18 | 19 | ## [2.0.2] - 2019-10-09 20 | 21 | - Support classes with fields that have types which were imported qualified 22 | (using `import '...' as ...;`). 23 | - Type-promote fields that take generic type arguments. 24 | - Make `freshApple` in example `const`. 25 | 26 | ## [2.0.1] - 2019-09-20 27 | 28 | - Revise readme: Little typo fixes and document `build_runner` dependency. 29 | - Code generation now throws error if problems occur. 30 | 31 | ## [2.0.0] - 2019-09-20 32 | 33 | - Change `@DataClass()` annotation to `@GenerateDataClassFor()`. 34 | - `GeneratedClass.fromMutable()` is now a normal constructor instead of a 35 | factory constructor. 36 | - Provide new example. 37 | - Revise readme. 38 | - New license. 39 | 40 | ## [1.1.1] - 2019-09-05 41 | 42 | - Fix newline issue in `toString()`. 43 | 44 | ## [1.1.0] - 2019-09-05 45 | 46 | - Change `@Nullable()` annotation to `@nullable`. 47 | - Add `toString()` method to generated class. 48 | - Make sure there are no `final` fields in the mutable class. 49 | 50 | ## [1.0.3] - 2019-09-04 51 | 52 | - Relax those version constraints even further. 53 | 54 | ## [1.0.2] - 2019-09-04 55 | 56 | - Rename example's mutable class to `MutableUser`. 57 | - Relax version constraints on `analyzer` and `build_runner` in the 58 | `pubspec.yaml`. 59 | 60 | ## [1.0.1] - 2019-09-04 61 | 62 | - Add example. 63 | - Change blueprint prefix from `$` to the more intuitive `Mutable`. 64 | 65 | ## [1.0.0] - 2019-09-04 66 | 67 | - Initial release: Support `DataClass` and `Nullable` annotations. Using the 68 | `data_classes_generator` package, classes with fields, a constructor, 69 | converter to and from the original mutable class, custom `==`, `hashCode` and 70 | `copyWith` can get generated. 71 | -------------------------------------------------------------------------------- /data_classes/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Marcel Garus 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /data_classes/README.md: -------------------------------------------------------------------------------- 1 | Looking for data classes? Check out the [freezed](https://pub.dev/packages/freezed) package, it is more ergonomic and has a lot of fancy features. 2 | Looking for has/is getters? Check out the [has_is_getters](https://pub.dev/packages/has_is_getters) package! 3 | 4 | --- 5 | 6 | Hey there! 7 | If you're reading this and want data classes to become a language-level feature 8 | of Dart, consider giving 9 | [this issue](https://github.com/dart-lang/language/issues/314) a thumbs up. 👍 10 | 11 | In the meantime, this library generates immutable data classes for you based on 12 | simple mutable blueprint classes. Here's how to get started: 13 | 14 | **1.** 📦 Add these packages to your dependencies: 15 | 16 | ```yaml 17 | dependencies: 18 | data_classes: ^3.0.0 19 | 20 | dev_dependencies: 21 | build_runner: ^1.7.1 22 | data_classes_generator: ^3.0.0 23 | ``` 24 | 25 | **2.** 🧬 Write a blueprint class. Let the name start with `Mutable` and 26 | annotate it with `@GenerateDataClass()`: 27 | 28 | ```dart 29 | import 'package:data_classes/data_classes.dart'; 30 | 31 | part 'my_file.g.dart'; 32 | 33 | @GenerateDataClass() 34 | class MutableFruit { 35 | String name; 36 | @nullable String color; 37 | } 38 | ``` 39 | 40 | By default, attributes are considered non-nullable. If you want an attribute to 41 | be nullable, annotate it with `@nullable`. 42 | 43 | **3.** 🏭 Run `pub run build_runner build` in the command line (or 44 | `flutter pub run build_runner build`, if you're using Flutter). The 45 | implementation based on your mutable class will automatically be generated. 46 | 47 | ## copy & copyWith 48 | 49 | By default, a `copy` method will be generated that takes a mutating function: 50 | 51 | ```dart 52 | var freshApple = const Fruit( 53 | name: 'apple', 54 | color: 'green', 55 | ); 56 | var banana = freshApple.copy((fruit) => 57 | fruit 58 | ..name = 'banana' 59 | ..color = 'yellow' 60 | ); 61 | ``` 62 | 63 | Sometimes, you have classes that are guaranteed to only have non-nullable variables. For those, you can opt in to generate a `copyWith` method! 64 | 65 | ```dart 66 | @GenerateDataClass(generateCopyWith = true) 67 | class MutableFruit { 68 | String name; 69 | String color; 70 | } 71 | 72 | var oldApple = freshApple.copyWith( 73 | color: 'brown', 74 | ); 75 | ``` 76 | 77 | If you're wondering why all fields have to be non-nullable, [here](https://github.com/marcelgarus/data_classes/issues/3)'s a discussion about that. 78 | 79 | ## value getters 80 | 81 | Sometimes, you have enum values as fields. In that case, you have the option to generate getters directly on the immutable class: 82 | 83 | ```dart 84 | enum Color { red, yellow, green, brown } 85 | enum Shape { round, curved } 86 | 87 | @GenerateDataClass() 88 | class MutableFruit { 89 | @GenerateValueGetters(generateNegations = true) 90 | Color color; 91 | 92 | @GenerateValueGetters(usePrefix = true) 93 | Shape theShape; 94 | } 95 | 96 | var banana = Fruit(color: Color.yellow, theShape: Shape.curved); 97 | 98 | banana.isYellow; // true 99 | banana.isNotGreen; // true 100 | banana.isTheShapeRound; // false 101 | ``` 102 | 103 | ## full example 104 | 105 | Here's an example with all the features (except nullability, so `copyWith` works). To showcase prefixed type imports, the `Color` enum from above has been moved to a file named `colors.dart`. 106 | 107 | ```dart 108 | import 'package:data_classes/data_classes.dart'; 109 | 110 | import 'colors.dart' as colors; 111 | 112 | part 'main.g.dart'; 113 | 114 | enum Shape { round, curved } 115 | 116 | /// A fruit with a doc comment. 117 | @GenerateDataClass(generateCopyWith: true) 118 | class MutableFruit { 119 | String name; 120 | 121 | /// The color of this fruit. 122 | @GenerateValueGetters(generateNegations: true) 123 | colors.Color color; 124 | 125 | @GenerateValueGetters(usePrefix: true) 126 | Shape shape; 127 | 128 | List likedBy; 129 | } 130 | ``` 131 | 132 | And here's the generated code: 133 | 134 | ```dart 135 | 136 | /// A fruit with a doc comment. 137 | @immutable 138 | class Fruit { 139 | final String name; 140 | 141 | /// The color of this fruit. 142 | final colors.Color color; 143 | 144 | final Shape shape; 145 | 146 | final List likedBy; 147 | 148 | // Value getters. 149 | bool get isRed => this.color == colors.Color.red; 150 | bool get isNotRed => this.color != colors.Color.red; 151 | bool get isYellow => this.color == colors.Color.yellow; 152 | bool get isNotYellow => this.color != colors.Color.yellow; 153 | bool get isGreen => this.color == colors.Color.green; 154 | bool get isNotGreen => this.color != colors.Color.green; 155 | bool get isBrown => this.color == colors.Color.brown; 156 | bool get isNotBrown => this.color != colors.Color.brown; 157 | bool get isShapeRound => this.shape == Shape.round; 158 | bool get isShapeCurved => this.shape == Shape.curved; 159 | 160 | /// Default constructor that creates a new [Fruit] with the given 161 | /// attributes. 162 | const Fruit({ 163 | @required this.name, 164 | @required this.color, 165 | @required this.shape, 166 | @required this.likedBy, 167 | }) : assert(name != null), 168 | assert(color != null), 169 | assert(shape != null), 170 | assert(likedBy != null); 171 | 172 | /// Creates a [Fruit] from a [MutableFruit]. 173 | Fruit.fromMutable(MutableFruit mutable) 174 | : name = mutable.name, 175 | color = mutable.color, 176 | shape = mutable.shape, 177 | likedBy = mutable.likedBy; 178 | 179 | /// Turns this [Fruit] into a [MutableFruit]. 180 | MutableFruit toMutable() { 181 | return MutableFruit() 182 | ..name = name 183 | ..color = color 184 | ..shape = shape 185 | ..likedBy = likedBy; 186 | } 187 | 188 | /// Checks if this [Fruit] is equal to the other one. 189 | bool operator ==(Object other) { 190 | return other is Fruit && 191 | name == other.name && 192 | color == other.color && 193 | shape == other.shape && 194 | likedBy == other.likedBy; 195 | } 196 | 197 | int get hashCode { 198 | return hashList([name, color, shape, likedBy]); 199 | } 200 | 201 | /// Copies this [Fruit] with some changed attributes. 202 | Fruit copy(void Function(MutableFruit mutable) changeAttributes) { 203 | assert( 204 | changeAttributes != null, 205 | "You called Fruit.copy, but didn't provide a function for changing " 206 | "the attributes.\n" 207 | "If you just want an unchanged copy: You don't need one, just use " 208 | "the original. The whole point of data classes is that they can't " 209 | "change anymore, so there's no harm in using the original class."); 210 | final mutable = this.toMutable(); 211 | changeAttributes(mutable); 212 | return Fruit.fromMutable(mutable); 213 | } 214 | 215 | /// Copies this [Fruit] with some changed attributes. 216 | Fruit copyWith({ 217 | String name, 218 | colors.Color color, 219 | Shape shape, 220 | List likedBy, 221 | }) { 222 | return Fruit( 223 | name: name ?? this.name, 224 | color: color ?? this.color, 225 | shape: shape ?? this.shape, 226 | likedBy: likedBy ?? this.likedBy, 227 | ); 228 | } 229 | 230 | /// Converts this [Fruit] into a [String]. 231 | String toString() { 232 | return 'Fruit(\n' 233 | ' name: $name\n' 234 | ' color: $color\n' 235 | ' shape: $shape\n' 236 | ' likedBy: $likedBy\n' 237 | ')'; 238 | } 239 | } 240 | ``` 241 | -------------------------------------------------------------------------------- /data_classes/example/main.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: undefined_class, uri_has_not_been_generated 2 | 3 | import 'package:data_classes/data_classes.dart'; 4 | 5 | part 'main.g.dart'; 6 | 7 | void main() { 8 | const freshApple = const Fruit(type: 'apple', color: 'green'); 9 | var someApple = freshApple.copy((fruit) => fruit..color = null); 10 | var kiwi = someApple.copy((fruit) => fruit 11 | ..type = 'Kiwi' 12 | ..color = 'brown'); 13 | print(kiwi); 14 | } 15 | 16 | @GenerateDataClass() 17 | class MutableFruit { 18 | String type; 19 | 20 | @nullable 21 | String color; 22 | } 23 | -------------------------------------------------------------------------------- /data_classes/lib/data_classes.dart: -------------------------------------------------------------------------------- 1 | export 'package:meta/meta.dart' show immutable, required; 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | @immutable 6 | class GenerateDataClass { 7 | const GenerateDataClass({ 8 | this.generateCopyWith = false, 9 | }) : assert(generateCopyWith != null); 10 | 11 | final bool generateCopyWith; 12 | } 13 | 14 | @immutable 15 | class GenerateValueGetters { 16 | const GenerateValueGetters({ 17 | this.usePrefix = false, 18 | this.generateNegations = false, 19 | }) : assert(usePrefix != null), 20 | assert(generateNegations != null); 21 | 22 | final bool usePrefix; 23 | final bool generateNegations; 24 | } 25 | 26 | const String nullable = 'nullable'; 27 | 28 | /// Combines the [Object.hashCode] values of an arbitrary number of objects 29 | /// from an [Iterable] into one value. This function will return the same 30 | /// value if given [null] as if given an empty list. 31 | // Borrowed from dart:ui. 32 | int hashList(Iterable arguments) { 33 | var result = 0; 34 | if (arguments != null) { 35 | for (Object argument in arguments) { 36 | var hash = result; 37 | hash = 0x1fffffff & (hash + argument.hashCode); 38 | hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); 39 | result = hash ^ (hash >> 6); 40 | } 41 | } 42 | result = 0x1fffffff & (result + ((0x03ffffff & result) << 3)); 43 | result = result ^ (result >> 11); 44 | return 0x1fffffff & (result + ((0x00003fff & result) << 15)); 45 | } 46 | -------------------------------------------------------------------------------- /data_classes/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | meta: 5 | dependency: "direct main" 6 | description: 7 | name: meta 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "1.1.7" 11 | sdks: 12 | dart: ">=2.2.2 <3.0.0" 13 | -------------------------------------------------------------------------------- /data_classes/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: data_classes 2 | description: Automatically generates immutable data classes for you based on a mutable "blueprint" class. 3 | version: 3.0.0+1 4 | author: Marcel Garus 5 | homepage: https://github.com/marcelgarus/data_classes 6 | 7 | environment: 8 | sdk: ">=2.2.2 <3.0.0" 9 | 10 | dependencies: 11 | meta: ^1.1.7 12 | -------------------------------------------------------------------------------- /data_classes_generator/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | The releases of this package are linked to the releases of the [`data_classes`](https://pub.dev/packages/data_classes) package. See there for a changelog. 2 | -------------------------------------------------------------------------------- /data_classes_generator/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Marcel Garus 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /data_classes_generator/README.md: -------------------------------------------------------------------------------- 1 | See [`data_classes`' readme](pub.dev/packages/data_classes) for how to use this 2 | package. 3 | -------------------------------------------------------------------------------- /data_classes_generator/build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | data_classes_generator|data_classes: 5 | enabled: true 6 | 7 | builders: 8 | data_classes: 9 | target: ":data_classes_generator" 10 | import: "package:data_classes_generator/data_classes_generator.dart" 11 | builder_factories: ["generateDataClass"] 12 | build_extensions: { ".dart": [".data_classes.g.part"] } 13 | auto_apply: dependents 14 | build_to: cache 15 | applies_builders: ["source_gen|combining_builder"] 16 | -------------------------------------------------------------------------------- /data_classes_generator/lib/data_classes_generator.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/element/element.dart'; 2 | import 'package:analyzer/dart/element/type.dart'; 3 | import 'package:build/build.dart'; 4 | import 'package:build/src/builder/build_step.dart'; 5 | import 'package:source_gen/source_gen.dart'; 6 | import 'package:data_classes/data_classes.dart'; 7 | 8 | Builder generateDataClass(BuilderOptions options) => 9 | SharedPartBuilder([DataClassGenerator()], 'data_classes'); 10 | 11 | class CodeGenError extends Error { 12 | CodeGenError(this.message); 13 | final String message; 14 | String toString() => message; 15 | } 16 | 17 | class DataClassGenerator extends GeneratorForAnnotation { 18 | @override 19 | generateForAnnotatedElement( 20 | Element element, 21 | ConstantReader annotation, 22 | BuildStep _, 23 | ) { 24 | if (element is! ClassElement) { 25 | throw CodeGenError( 26 | 'You can only annotate classes with @GenerateDataClass(), but ' 27 | '"${element.name}" isn\'t a class.'); 28 | } 29 | if (!element.name.startsWith('Mutable')) { 30 | throw CodeGenError( 31 | 'The names of classes annotated with @GenerateDataClass() should ' 32 | 'start with "Mutable", for example Mutable${element.name}. The ' 33 | 'immutable class (in that case, ${element.name}) will then get ' 34 | 'automatically generated for you by running "pub run build_runner ' 35 | 'build" (or "flutter pub run build_runner build" if you\'re using ' 36 | 'Flutter).'); 37 | } 38 | 39 | final originalClass = element as ClassElement; 40 | final name = originalClass.name.substring('Mutable'.length); 41 | 42 | // When import prefixes (`import '...' as '...';`) are used in the mutable 43 | // class's file, then in the generated file, we need to use the right 44 | // prefix in front of the type in the immutable class too. So here, we map 45 | // the module identifiers to their import prefixes. 46 | Map qualifiedImports = { 47 | for (final import in originalClass.library.imports) 48 | if (import.prefix != null) 49 | import.importedLibrary.identifier: import.prefix.name, 50 | }; 51 | 52 | // Collect all the fields and getters from the original class. 53 | final fields = {}; 54 | final getters = {}; 55 | 56 | for (final field in originalClass.fields) { 57 | if (field.isFinal) { 58 | throw CodeGenError( 59 | 'Mutable classes shouldn\'t have final fields, but the class ' 60 | 'Mutable$name has the final field ${field.name}.'); 61 | } else if (field.setter == null) { 62 | assert(field.getter != null); 63 | getters.add(field); 64 | } else if (field.getter == null) { 65 | assert(field.setter != null); 66 | throw CodeGenError( 67 | 'Mutable classes shouldn\'t have setter-only fields, but the ' 68 | 'class Mutable$name has the field ${field.name}, which only has a ' 69 | 'setter.'); 70 | } else { 71 | fields.add(field); 72 | } 73 | } 74 | 75 | // Check whether we should generate a `copyWith` method. Also ensure that 76 | // there are no nullable fields. 77 | final generateCopyWith = originalClass.metadata 78 | .firstWhere((annotation) => 79 | annotation.element?.enclosingElement?.name == 'GenerateDataClass') 80 | .constantValue 81 | .getField('generateCopyWith') 82 | .toBoolValue(); 83 | if (generateCopyWith && fields.any(_isNullable)) { 84 | final exampleField = fields.firstWhere(_isNullable).name; 85 | throw CodeGenError( 86 | 'You tried to generate a copyWith method for the $name class (which ' 87 | 'gets generated based on the Mutable$name class). Unfortunately, ' 88 | 'you can only generate this method if all the fields are ' 89 | 'non-nullable, but for example, the $exampleField field is marked ' 90 | 'with @nullable. If you really want a copyWith method, you should ' 91 | 'consider removing that annotation.\n' 92 | 'Why does this rule exist? Let\'s say, we would allow the copyWith ' 93 | 'method to get generated. If you would call it, it would have no ' 94 | 'way of knowing whether you just didn\'t pass in a $exampleField as ' 95 | 'a parameter or you intentionally tried to set it to null, because ' 96 | 'in both cases, the function parameter would be null. That makes ' 97 | 'the code vulnerable to subtle bugs when passing variables to the ' 98 | 'copyWith method. ' 99 | 'For more information about this, see the following GitHub issue: ' 100 | 'https://github.com/marcelgarus/data_classes/issues/3'); 101 | } 102 | 103 | // Users can annotate fields that hold an enum value with 104 | // `@GenerateValueGetters()` to generate value getters on the immutable 105 | // class. Here, we prepare a map from the getter name to its code content. 106 | final valueGetters = {}; 107 | for (final field in fields) { 108 | final annotation = field.metadata 109 | .firstWhere( 110 | (annotation) => 111 | annotation.element?.enclosingElement?.name == 112 | 'GenerateValueGetters', 113 | orElse: () => null) 114 | ?.computeConstantValue(); 115 | if (annotation == null) continue; 116 | 117 | final usePrefix = annotation.getField('usePrefix').toBoolValue(); 118 | final generateNegations = 119 | annotation.getField('generateNegations').toBoolValue(); 120 | 121 | final enumClass = field.type.element as ClassElement; 122 | if (enumClass?.isEnum == false) { 123 | throw CodeGenError( 124 | 'You annotated the Mutable$name\'s ${field.name} with ' 125 | '@GenerateValueGetters(), but that\'s of ' 126 | '${enumClass == null ? 'an unknown type' : 'the type ${enumClass.name}'}, ' 127 | 'which is not an enum. @GenerateValueGetters() should only be ' 128 | 'used on fields of an enum type.'); 129 | } 130 | 131 | final prefix = 'is${usePrefix ? _capitalize(field.name) : ''}'; 132 | final enumValues = enumClass.fields 133 | .where((field) => !['values', 'index'].contains(field.name)); 134 | 135 | for (final value in enumValues) { 136 | for (final negate in generateNegations ? [false, true] : [false]) { 137 | final getter = 138 | '$prefix${negate ? 'Not' : ''}${_capitalize(value.name)}'; 139 | final content = 'this.${field.name} ${negate ? '!=' : '=='} ' 140 | '${_qualifiedType(value.type, qualifiedImports)}.${value.name}'; 141 | 142 | if (valueGetters.containsKey(getter)) { 143 | throw CodeGenError( 144 | 'A conflict occurred while generating value getters. The two ' 145 | 'conflicting value getters of the Mutable$name class are:\n' 146 | '- $getter, which tests if ${valueGetters[getter]}\n' 147 | '- $getter, which tests if $content'); 148 | } 149 | 150 | valueGetters[getter] = content; 151 | } 152 | } 153 | } 154 | 155 | // Actually generate the class. 156 | final buffer = StringBuffer(); 157 | buffer.writeAll([ 158 | // Start of the class. 159 | originalClass.documentationComment ?? 160 | '/// This class is the immutable pendant of the [Mutable$name] class.', 161 | '@immutable', 162 | 'class $name {', 163 | 164 | // The field members. 165 | for (final field in fields) ...[ 166 | if (field.documentationComment != null) field.documentationComment, 167 | 'final ${_fieldToTypeAndName(field, qualifiedImports)};\n', 168 | ], 169 | 170 | // The value getters. 171 | '\n // Value getters.', 172 | for (final getter in valueGetters.entries) 173 | 'bool get ${getter.key} => ${getter.value};', 174 | 175 | // The default constructor. 176 | '/// Default constructor that creates a new [$name] with the given', 177 | '/// attributes.', 178 | 'const $name({', 179 | for (final field in fields) ...[ 180 | if (!_isNullable(field)) '@required ', 181 | 'this.${field.name},' 182 | ], 183 | '}) : ', 184 | fields 185 | .where((field) => !_isNullable(field)) 186 | .map((field) => 'assert(${field.name} != null)') 187 | .join(','), 188 | ';\n', 189 | 190 | // Converters (fromMutable and toMutable). 191 | '/// Creates a [$name] from a [Mutable$name].', 192 | '$name.fromMutable(Mutable$name mutable) : ', 193 | fields.map((field) => '${field.name} = mutable.${field.name}').join(','), 194 | ';\n', 195 | '/// Turns this [$name] into a [Mutable$name].', 196 | 'Mutable$name toMutable() {', 197 | 'return Mutable$name()', 198 | fields.map((field) => '..${field.name} = ${field.name}').join(), 199 | ';', 200 | '}\n', 201 | 202 | // Equality stuff (== and hashCode). 203 | '/// Checks if this [$name] is equal to the other one.', 204 | 'bool operator ==(Object other) {', 205 | 'return other is $name &&', 206 | fields 207 | .map((field) => '${field.name} == other.${field.name}') 208 | .join(' &&\n'), 209 | ';\n}\n', 210 | 'int get hashCode {', 211 | 'return hashList([', 212 | fields.map((field) => field.name).join(', '), 213 | ']);\n', 214 | '}\n', 215 | 216 | // copy 217 | '/// Copies this [$name] with some changed attributes.', 218 | '$name copy(void Function(Mutable$name mutable) changeAttributes) {', 219 | 'assert(changeAttributes != null,', 220 | '"You called $name.copy, but didn\'t provide a function for changing "', 221 | '"the attributes.\\n"', 222 | '"If you just want an unchanged copy: You don\'t need one, just use "', 223 | '"the original. The whole point of data classes is that they can\'t "', 224 | '"change anymore, so there\'s no harm in using the original class."', 225 | ');', 226 | 'final mutable = this.toMutable();', 227 | 'changeAttributes(mutable);', 228 | 'return $name.fromMutable(mutable);', 229 | '}\n', 230 | 231 | // copyWith 232 | if (generateCopyWith) ...[ 233 | '/// Copies this [$name] with some changed attributes.', 234 | '$name copyWith({', 235 | for (final field in fields) 236 | '${_fieldToTypeAndName(field, qualifiedImports)},', 237 | '}) {', 238 | 'return $name(', 239 | for (final field in fields) 240 | '${field.name}: ${field.name} ?? this.${field.name},', 241 | ');', 242 | '}', 243 | ], 244 | 245 | // toString converter. 246 | '/// Converts this [$name] into a [String].', 247 | 'String toString() {', 248 | "return '$name(\\n'", 249 | for (final field in fields) "' ${field.name}: \$${field.name}\\n'", 250 | "')';", 251 | '}', 252 | 253 | // End of the class. 254 | '}', 255 | ].expand((line) => [line, '\n'])); 256 | 257 | return buffer.toString(); 258 | } 259 | 260 | /// Whether the [field] is nullable. 261 | bool _isNullable(FieldElement field) { 262 | assert(field != null); 263 | 264 | return field.metadata 265 | .any((annotation) => annotation.element.name == nullable); 266 | } 267 | 268 | /// Capitalizes the first letter of a string. 269 | String _capitalize(String string) { 270 | assert(string.isNotEmpty); 271 | return string[0].toUpperCase() + string.substring(1); 272 | } 273 | 274 | /// Turns the [field] into type and the field name, separated by a space. 275 | String _fieldToTypeAndName( 276 | FieldElement field, 277 | Map qualifiedImports, 278 | ) { 279 | assert(field != null); 280 | assert(qualifiedImports != null); 281 | 282 | return '${_qualifiedType(field.type, qualifiedImports)} ${field.name}'; 283 | } 284 | 285 | /// Turns the [type] into a type with prefix. 286 | String _qualifiedType(DartType type, Map qualifiedImports) { 287 | final typeLibrary = type.element.library; 288 | final prefixOrNull = qualifiedImports[typeLibrary.identifier]; 289 | final prefix = (prefixOrNull != null) ? '$prefixOrNull.' : ''; 290 | return '$prefix$type'; 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /data_classes_generator/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: data_classes_generator 2 | description: Automatically generates immutable data classes for you based on a mutable "blueprint" class. 3 | version: 3.0.0 4 | author: Marcel Garus 5 | homepage: https://github.com/marcelgarus/data_classes 6 | 7 | environment: 8 | sdk: ">=2.2.2 <3.0.0" 9 | 10 | dependencies: 11 | analyzer: ">=0.27.1 <1.0.0" 12 | build: ">=0.12.0 <2.0.0" 13 | data_classes: ^3.0.0 14 | source_gen: ^0.9.0 15 | 16 | dev_dependencies: 17 | build_runner: ">=0.9.0 <1.6.7" 18 | -------------------------------------------------------------------------------- /example/lib/colors.dart: -------------------------------------------------------------------------------- 1 | enum Color { red, yellow, green, brown } 2 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:data_classes/data_classes.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | import 'colors.dart'; 5 | 6 | part 'main.g.dart'; 7 | 8 | enum Shape { round, curved } 9 | 10 | @GenerateDataClass() 11 | class MutableFruit { 12 | Color color; 13 | Shape shape; 14 | 15 | MutableFruit({ 16 | @required this.color, 17 | @required this.shape, 18 | }) : assert(color != null), 19 | assert(shape != null); 20 | } 21 | 22 | // --- generated code --- 23 | 24 | void main() { 25 | final a = Fruit(); 26 | a.doubleShape; 27 | a.doubleLine; 28 | } 29 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: Automatically generates immutable data classes for you based on a mutable "blueprint" class. 3 | version: 1.0.0 4 | author: Marcel Garus 5 | homepage: https://github.com/marcelgarus/flutter_data_classes 6 | 7 | environment: 8 | sdk: ">=2.6.0-dev.8.2 <3.0.0" 9 | 10 | dependencies: 11 | data_classes: ^3.0.0 12 | json_serializable: ^3.2.3 13 | 14 | dev_dependencies: 15 | build_runner: ^1.7.1 16 | data_classes_generator: ^3.0.0 17 | --------------------------------------------------------------------------------