👋

プログラムでDartのコードを生成してみる

2021/04/06に公開

freezedとかアノテーション付けてbuild_runnerでコードを生成する系のパッケージがあるが、そもそもプログラムでDartのコードを生成するのってどうやるのだろう?と気になったので試してみる。

code_builder

https://pub.dev/packages/code_builder

code_builderというパッケージを使うことでプログラムでDartのコードを生成することが出来るらしい。

import 'package:code_builder/code_builder.dart';
import 'package:dart_style/dart_style.dart';

void main() {
 final animal = Class((b) => b
   ..name = 'Animal'
   ..extend = refer('Organism')
   ..methods.add(Method.returnsVoid((b) => b
     ..name = 'eat'
     ..body = const Code("print('Yum');"))));
 final emitter = DartEmitter();
 print(DartFormatter().format('${animal.accept(emitter)}'));
}

READMEにあるサンプルコードはこんな感じ。クラスを定義して、メソッドを定義して、出力すると出来上がり。結構簡単に使えそう。

class Animal extends Organism {
 void eat() => print('Yum!');
}

dart_styleというパッケージが使われているが、これはコードをいい感じに整形してくれるものである。とりあえず入れておけばOKそう。

https://pub.dev/packages/dart_style

・code_builder:プログラムでコードを生成するやつ
・dart_style:生成するコードを整形するやつ

JSONを元にDartのコードを生成してみる

実際の使い所としては、アノテーションを元にコードを生成するとか、何かしらの構造化された情報が記載されているファイル(JSONとかYAMLとか)を元にコードを生成する感じになると思う。面倒くさい作業を短縮する感じ。

ここでは、JSONを定義してそれを元にコード生成するのを試してみようと思う。やってみることの全体像はこんな感じ。

{
  "User": {
    "id": "String",
    "age": "int",
  }
}

というJSONを定義した時に

class User {
  User({this.id, this.age})
  final String id;
  final int age;
}

といった感じのコードを生成出来るようにしたい。

コード全体

全体としてはこんな感じで、JSONからDartのコードを生成することが出来た。

// @dart=2.9
import 'dart:convert';

import 'package:code_builder/code_builder.dart';
import 'package:dart_style/dart_style.dart';

void main() {
 final json = '''
 {
   "User": {
     "id": "String",
     "name": "String",
     "age": "int",
     "createdAt": "DateTime",
     "updatedAt": "DateTime"
   },
   "Post": {
     "id": "String",
     "title": "String",
     "body": "String",
     "createdAt": "DateTime",
     "updatedAt": "DateTime"
   }
 }
 ''';
 final emitter = DartEmitter();
 final formatter = DartFormatter();

 final data = jsonDecode(json) as Map<String, dynamic>;
 for (final entry in data.entries) {
   final fields = entry.value as Map<String, dynamic>;
   final klass = Class((b) {
     // クラス名
     b.name = entry.key;

     // コンストラクタ
     b.constructors.add(Constructor(
       // Namedパラメータ
       (b) => b.optionalParameters.addAll([
         for (final entry in fields.entries)
           Parameter((b) => b
             ..name = entry.key
             ..named = true
             ..toThis = true)
       ]),
     ));

     // インスタンス変数
     b.fields.addAll([
       for (final entry in fields.entries)
         Field((b) => b
           ..name = entry.key
           ..modifier = FieldModifier.final$
           ..type = Reference(entry.value)),
     ]);
   });

   // クラスをコードとして出力
   print(formatter.format('${klass.accept(emitter)}'));
 }
}
class User {
 User({this.id, this.name, this.age, this.createdAt, this.updatedAt});

 final String id;

 final String name;

 final int age;

 final DateTime createdAt;

 final DateTime updatedAt;
}

class Post {
 Post({this.id, this.title, this.body, this.createdAt, this.updatedAt});

 final String id;

 final String title;

 final String body;

 final DateTime createdAt;

 final DateTime updatedAt;
}

基本的にはクラスに対してコンストラクタなり変数なり定義したい要素をどんどん追加していけば良い。どの要素でもXxxBuilder経由で詳細な要素を追加すればOKなので簡単である。ただし、イメージしている処理がどの要素であるのかは使ってみないとわからない部分もあるのでそこはトライ&エラーを繰り返してみるしかなさそう。

最後に

比較的シンプルなコードであれば簡単に生成することが出来た。複雑なコードでも頑張れば生成できそうだが単純に面倒くさいかも。

次回はbuild_runner経由でコード生成を試してみたいと思う。

作って学ぶ、FlutterとFirebaseを使ったアプリ開発

FlutterとFirebaseを使ったアプリ開発に関して書籍にまとめました。

Flutterで始めるアプリ開発

https://www.flutter-study.dev/

Discussion