見出し画像

自作アノテーションとBuildRunnerを使ってDartのコードを自動生成

以前BuildRunnerを使ってDartのコードを自動生成してみた。今回はその発展として、アノテーションを定義して、その定義を元にコード生成を行ってみたいと思う。

Metadata

@override とか @deprecated とか @ で始まるやつを Dart的には Metadata と呼ぶ。いわゆるアノテーション。

Metadataを定義するのは簡単で、const な クラス or 変数 を定義するだけでOK。

class Field {
  const Field(this.name);
  
  final String name;
}


class User {
  @Field('id');
  String id;
  
  @Field('created_at');
  DateTime createdAt;
}

任意のクラスからJSON用Mapを作成する

概要

ここでは、任意のクラスからJSON用Mapを作成するようなコードを自動生成することとしてみる。

・任意のクラスの各フィールドにアノテーションを適用する
・名前と型に従ってMapを返すコードを生成する

// --------------------------------------------------
// lib/metadata.dart << アノテーション
// --------------------------------------------------
class MyField {
 const MyField(this.name, this.type);

 final String name;
 final String type;
}


// --------------------------------------------------
// lib/user.dart << 自動生成元のクラス
// --------------------------------------------------
import 'package:my_builder/metadata.dart';

part 'user.field.dart';

class User {
 @MyField('id', 'string')
 String id;

 @MyField('age', 'number')
 int age;

 @MyField('created_at', 'string')
 DateTime createdAt;

 Map<String, dynamic> toMap() => $UserData.toMap(this);
}


// --------------------------------------------------
// lib/user.data.dart << 自動生成されたクラス
// --------------------------------------------------
class $UserData {
 static Map<String, dynamic> toMap(User data) {
   final map = <String, dynamic>{
     'id': data.id.toString(),
     'age': int.parse(data.age.toString()),
     'created_at': data.createdAt.toString(),
   };
   return map;
 }
}

アノテーションを作成する

↑に示したとおりで、アノテーションを作成して、任意のクラスに適用しておく。

Builderを作成する

自動生成元のDartファイル内に part を記述して、その定義を元に自動生成するBuilderを作る場合は source_gen パッケージの PartBuilder を使うらしい。

また、Dartのコード本体は Generator を使って定義し、PartBuilderへと差し込んでいく。

※ user.g.dart のように、.g.dart とする場合は SharedPartBuilder を使うらしいので注意。

Builder dataBuilderFactory(BuilderOptions options) {
 return PartBuilder([DataGenerator()], '.data.dart');
}

class DataGenerator extends Generator {}

Generatorを作成する

Generatorを使ってDartのコードを生成するには、generate() をオーバーライドして、返り値としてコードの文字列を返せばOKである。

class DataGenerator extends Generator {
 final myFieldChecker = TypeChecker.fromRuntime(MyField);

 @override
 FutureOr<String> generate(LibraryReader library, BuildStep buildStep) async {
   return 'class Hoge {}';
 }
}

アノテーションを読み取る

typeChecker.annotationsOf() を使うことで各Elementに定義されているアノテーションを読み取ることが出来る。

また、TypeChecker のインスタンスは TypeChecker.fromRuntime() にアノテーション本体のクラスを指定して作成すればOK。

class DataGenerator extends Generator {
 final myFieldChecker = TypeChecker.fromRuntime(MyField);

 @override
 FutureOr<String> generate(LibraryReader library, BuildStep buildStep) async {
   final classElement = library.classes.first;
   classElement.fields.forEach((fieldElement) {
     final myField = myFieldChecker.annotationsOf(fieldElement).first;
     final name = myField.getField('name').toStringValue();
     final type = myField.getField('type').toStringValue();
     print('name:$name, type:$type');
   });

   return '';
 }
}

後はアノテーションの内容に従って、Dartのコードを作成すればOK。プログラムからDartのコードを作成する仕組みに関しては以前の記事で紹介した知識で対応できた。

class DataGenerator extends Generator {
 final myFieldChecker = TypeChecker.fromRuntime(MyField);

 @override
 FutureOr<String> generate(LibraryReader library, BuildStep buildStep) async {
   final emitter = DartEmitter();
   final formatter = DartFormatter();

   final classElement = library.classes.first;
   final klass = Class((b) {
     b.name = '\$${classElement.name}Data';
     b.methods.add(Method((b) {
       b.name = 'toMap';
       b.static = true;
       b.requiredParameters.add(Parameter((b) {
         b.name = 'data';
         b.type = Reference(classElement.name);
       }));
       b.returns = Reference('Map<String, dynamic>');
       b.body = Code([
         'final map = <String, dynamic>{',
         ...classElement.fields.map((fieldElement) {
           final fieldName = fieldElement.name;
           final myField = myFieldChecker.annotationsOf(fieldElement).first;
           final name = myField.getField('name').toStringValue();
           final type = myField.getField('type').toStringValue();
           switch (type) {
             case 'string':
               return "'$name': data.$fieldName.toString(),";
             case 'number':
               return "'$name': int.parse(data.$fieldName.toString()),";
             default:
               throw ArgumentError();
           }
         }),
         '};',
         'return map;',
       ].join('\n'));
     }));
   });

   return formatter.format('${klass.accept(emitter)}');
 }
}

自動生成の処理を実行する

最後に、build.yaml を定義して、build_runner を走らせればOKである。

builders:
 my_builder:
   import: 'package:my_builder/data_builder.dart'
   builder_factories: ['dataBuilderFactory']
   build_extensions: {'.dart': ['.data.dart']}
   auto_apply: root_package
   build_to: source
targets:
 $default:
   builders:
     my_builder:
       generate_for:
         - lib/user.dart
$ dart run build_runner build
[INFO] Generating build script completed, took 333ms
[INFO] Reading cached asset graph completed, took 44ms
[INFO] Checking for updates since last build completed, took 423ms
[WARNING] my_builder|lib/field_builder.dart was not found in the asset graph, incremental builds will not work.
This probably means you don't have your dependencies specified fully in your pubspec.yaml.
[WARNING] Invalidating asset graph due to build script update!
[INFO] Cleaning up outputs from previous builds. completed, took 3ms
[INFO] Generating build script completed, took 51ms
[INFO] Creating build script snapshot... completed, took 9.5s
[INFO] There was output on stdout while compiling the build script snapshot, run with `--verbose` to see it (you will need to run a `clean` first to re-snapshot).
[INFO] Building new asset graph completed, took 514ms
[INFO] Checking for unexpected pre-existing outputs. completed, took 1ms
[INFO] Running build completed, took 809ms
[INFO] Caching finalized dependency graph completed, took 22ms
[INFO] Succeeded after 840ms with 1 outputs (1 actions)

で、生成された user.data.dart がこちら。

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'user.dart';

// **************************************************************************
// DataGenerator
// **************************************************************************

class $UserData {
 static Map<String, dynamic> toMap(User data) {
   final map = <String, dynamic>{
     'id': data.id.toString(),
     'age': int.parse(data.age.toString()),
     'created_at': data.createdAt.toString(),
   };
   return map;
 }
}

これで、任意のクラスからJSON用のMapを作成するためのコードが自動生成されるようになった。

void main() {
 final user = User()
   ..id = '123'
   ..age = 20
   ..createdAt = DateTime.now();
 print(user.toMap());
 // {id: 123, age: 20, created_at: 2999-01-01 00:00:00.000000}
}

最後に

アノテーションの作成も、アノテーションを使ったコード生成も、使い方を知ってしまえば簡単である。

アノテーションを多用しすぎると後々面倒くさくなる可能性もありそうなので用法用量は意識したほうが良いと思うが、アノテーションを使ったコード生成を選択肢の1つとして持っている状態にしておくのは良いと思う。

この記事が気に入ったらサポートをしてみませんか?