Dart code generation framework
Warning: This package is still considered experimental.
Mint
provides a framework to generate code through templates. It comes out of the box with support for copyWith
, copyJar
, equality
, toJson
, and fromJson
. However, it allows you to generate whatever you wish, and to interact with the generated code however you wish. All generated code comes from templates which you can easily modify to suit your needs. This power extends to interacting with generated third party code. This allows your classes to not need any “hook up” into generated code. The aim of mint is clean, simple, and intuitive data classes.
Usage
Usage is fairly straight forward:
Add the dependencies to your pubspec.yaml
dependencies:
au: ^0.2.1
json_annotation: ^4.7.0
dev_dependencies:
build_runner: ^2.0.0
json_serializable: ^6.3.1
mint: ^0.6.0
Add it to a model
// Import the au annotation
import 'package:au/au.dart';
// Import whatever other libraries you may need
import 'package:json_annotation/json_annotation.dart';
// Include the generated code. Notice the extension is .au.dart and not .g.dart.
part 'person.au.dart';
// Annotate the class with Au
@Au()
// Add whatever other annotations you may need.
@JsonSerializable(explicitToJson: true)
// Include the _$Person mixin (Pattern is _$CLASS)
class Person with _$Person {
final String name;
final int age;
const Person(this.name, this.age);
// Copy this constructor from the generated code
// The positional arguments represent the field names in alphabetical order
const Person._fromAu(
this.age,
this.name,
);
}
Update your build.yaml
Ensure you have the following configuration in your build.yaml
(in your project’s root). More details about these in the configuration section below.
targets:
$default:
builders:
mint:mint_builder:
enabled: True
options:
templates:
abstract: "package:mint/src/templates/abstract.mustache"
child: "package:mint/src/templates/child.mustache"
from_au_hint: "package:mint/src/templates/from_au_hint.mustache"
jar: "package:mint/src/templates/jar.mustache"
mixin: "package:mint/src/templates/mixin.mustache"
from_json: "package:mint/src/templates/from_json.mustache"
to_json: "package:mint/src/templates/to_json.mustache"
mixin_annotations:
- annotation: 'JsonSerializable'
template: 'to_json'
child_annotations:
- annotation: 'JsonSerializable'
template: 'from_json'
source_gen:combining_builder:
enabled: False
mint:mint_combining_builder:
enabled: True
options:
mint_rewire_parts:
- 'json_serializable.g.part'
Run the build runner
Run the build_runner to generate the code (or use the watch option to do it automatically on save):
dart run build_runner build --delete-conflicting-outputs
OR:
dart run build_runner watch --delete-conflicting-outputs
OR for Flutter:
flutter packages pub run build_runner build --delete-conflicting-outputs
Copy the _fromAu
constructor (if you didn’t write it manually)
At the top of the generated code, you should find the au constructor code:
// Copy the _fromAu constructor into your base class.
// const Person._fromAu(this.age,this.name,);
Good to go
final p1 = const Person('John', 35);
final p2 = p1.copyWith(
age: const AuValue<int>(25),
);
assert(p1 != p2);
// or just copy the jar
final p3 = p1.copyJar(const AuPersonJar(
age: 21,
name: 'Joe',
));
final p4 = AuPerson.fromJson(p3.toJson());
assert(p3 == p4);
final p5 = const AuPerson('John', 35);
assert(p5 == p1);
Notice you no longer have to write code for these common functionalities. You also no longer need to add the fromJson
factory constructor, you can use the AuPerson
(AuCLASS) generated child class’s fromJson
factory. This works because instances of Person
and AuPerson
are equivalent.
Regeneration
The only time you would ever need to rerun the build runner is if you ever modify the field or constructor definitions in a model. As previously mentioned this can be accomplished automatically with a watch command: dart run build_runner watch --delete-conflicting-outputs
.
If you want to take this a step farther, you can automate the execution of this command in VSCode so that it runs whenever you open your project. This can be accomplished through the VSCode tasks configuration. To set this up simply add this tasks.json
inside your .vscode
directory:
{
"version": "2.0.0",
"tasks": [
{
"type": "flutter",
"command": "flutter",
"args": [
"pub",
"run",
"build_runner",
"watch",
"lib/",
"--delete-conflicting-outputs",
],
"problemMatcher": [
"$dart-build_runner"
],
"options": {
"cwd": "example",
},
"runOptions": {
"runOn": "folderOpen"
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": false,
"clear": false
},
"group": "build",
"label": "Flutter Build Runner",
"detail": "example"
}
]
}
Configuration
Configuration for code generation is done through the build.yaml
file:
targets:
$default:
builders:
mint:mint_builder:
enabled: True
options:
templates:
# Required templates
abstract: "package:mint/src/templates/abstract.mustache"
child: "package:mint/src/templates/child.mustache"
from_au_hint: "package:mint/src/templates/from_au_hint.mustache"
jar: "package:mint/src/templates/jar.mustache"
mixin: "package:mint/src/templates/mixin.mustache"
# Annotation templates
from_json: "package:mint/src/templates/from_json.mustache"
to_json: "package:mint/src/templates/to_json.mustache"
mixin_annotations:
- annotation: 'JsonSerializable'
template: 'to_json'
child_annotations:
- annotation: 'JsonSerializable'
template: 'from_json'
# Disable source gen combining_builder for improved performance
source_gen:combining_builder:
enabled: False
# Utilize the mint:combining_builder instead
mint:mint_combining_builder:
enabled: True
options:
# Which parts will have generated model references replaced with Au child references (Person > AuPerson). This is only needed when there are factories being created in child_annotations. It essentially rewires the part generated code to function as if it were generated for the child class instead of the model. This allows you to interact with it from other generated code without ever needing to add anything to the model.
mint_rewire_parts:
- 'json_serializable.g.part'
The first step is to define the templates map. The required keys are: abstract
, child
, from_au_hint
, jar
, and mixin
. The keys represent the identifier of each template, the value is the URI to the mustache template file. If you want to use your own template simply replace the corresponding URI with one in your project.
There is also support for annotation templates (which is how Mint provides support for JsonSerializable
). It can also be used to create your own annotation driven generations. Once the templates are defined, we simply provide a list of maps for mixin_annotations
and child_annotations
with the corresponding template identifier.
eg: If the class is annotated with JsonSerializable
, generate the to_json
template into the mixin. The same applies if we need to add some functionality to the child class (eg: for a factory constructor), we simply utilize the child_annotations
list instead.
Note: As you may have realized, the configured annotations are strings and not actual TypeInterfaces. If you have multiple annotation classes with the same name this may present some challenges.
Note: You may not wish to follow along with referencing generated Au* classes in your codebase. If thats the case, you could opt to only use the mixin, which will still give you the majority of the functionality: copyWith, copyJar, equality, toJson, etc. You would of course have to define the necessary factories in your models (fromJson). The only other change would be to disable the mint_combining_builder, and enable the source_gen one.
Template Parameters
When creating your own templates for annotations, you will will need to access some model metadata. This can be done with the variables below:
[
'model_class_name',
'model_abstract_class_name',
'model_child_class_name',
'model_jar_class_name',
'fields': [
'field_name',
'field_name_capitalized',
'field_type',
'field_type_with_nullability',
'field_is_nullable',
'field_is_private',
'field_is_last',
]
]
Here’s an example of the abstract.mustache
file, which creates an abstract class with the same fields as the model:
abstract class {{model_abstract_class_name}} extends AuMinted {
{{#fields}}
{{field_type_with_nullability}} get {{field_name}};
{{/fields}}
}
Custom templates targeting an annotation
In the example project you can see an example of how to set this up. It’s fairly straight forward.
Create your annotation
In dart any class can be an annotation, it just has to have a const constructor.
class Foo {
const Foo();
}
Create your custom template
// Custom generated function from Foo annotation
String foo() {
return 'foo on {{model_class_name}} - {{#fields}}{{field_name}}{{^field_is_last}},{{/field_is_last}}{{/fields}}';
}
Update build.yaml
templates:
abstract: "package:mint/src/templates/abstract.mustache"
child: "package:mint/src/templates/child.mustache"
from_au_hint: "package:mint/src/templates/from_au_hint.mustache"
jar: "package:mint/src/templates/jar.mustache"
mixin: "package:mint/src/templates/mixin.mustache"
from_json: "package:mint/src/templates/from_json.mustache"
to_json: "package:mint/src/templates/to_json.mustache"
# Add your custom template
with_foo: "package:example/templates/with_foo.mustache"
mixin_annotations:
- annotation: 'JsonSerializable'
template: 'to_json'
# Add your custom annotation, and configure it to be added to the mixin_annotations
- annotation: 'Foo'
template: 'with_foo'
Use it
@Au()
@Foo()
@JsonSerializable(explicitToJson: true)
class WithFoo with _$WithFoo {
final String name;
final int age;
const WithFoo(this.name, this.age);
const WithFoo._fromAu(
this.age,
this.name,
);
}
Since the model is annotated with Foo
, and we configured Foo to generate the tempalte with_foo
, the system will generate this additional function as part of the method:
// Custom generated function from Foo annotation
String foo() {
return 'foo on WithFoo - age,name';
}
You can now use it as any other method:
final w = WithFoo('foo', 99);
print(w.foo());
Factory constructors
Factory constructors can’t be defined as part of a mixin. Therefore Mint
also generates a child class which extends the original model. This child class is named Au(CLASS)
. This child class is responsible for interacting with generated code for us. It allows us to not have to define fromJson
or other similar factory constructors which may be required by third party code generators. You can utilize a child class in the same way you utilize it’s parent model.
If you wish for code to be part of the child class instead you simply move the Foo annotation configuration to the child_annotations section:
child_annotations:
- annotation: 'JsonSerializable'
template: 'from_json'
# Add the foo annotation configuration
- annotation: 'Foo'
template: 'with_foo'
Optional rewiring
Depending on the third party code generation library you’re trying to integrate with, you may also need to “rewire” the third party generated code to now point to the child class instead of the original model it was generated for (Person > AuPerson). To achieve this, you simply add the extension of the part file to the mint_combining_builder
configuration:
mint:mint_combining_builder:
enabled: True
options:
mint_rewire_parts:
- 'json_serializable.g.part'
# Add parts to rewire
Closing
Fun Fact: This package was originally named Augment
, and then I discovered the work in progress Dart feature by the same name. Therefore I renamed it to: Au + Mint. Hopefully augmentation gives us better options for accomplishing this type of behavior in the future, but until then.. keep minting gold!