🧱

MasonCLIでEnumやListを使いこなす

2024/01/04に公開

※ この記事の肝は「EnumやListの使い方がわからん!」からですので、Masonをすでに触ったことがある方はそこまで読み飛ばしください。

Masonとは。

Masonとは、Brickと呼ばれるテンプレートを使ってインタラクティブにコードを自動生成するためのツールです。MasonはDartで実装されており、使う際にはMasonCLIをdart pub global activateすることでコマンドラインから利用できます。
Brickは自分で書く他、BrickHubにて公開されているものをお借りして使うこともできます。

ちなみにMasonは石工という意味で、Brickはレンガという意味のよう。
テンプレートと自動生成をレンガと石工で比喩しているのはとてもオシャレでそんな美しい命名をしてみたいものだと思いました。

また、Masonについての詳しい説明は下記の動画にあるので、気になる方はぜひ見て欲しいです。
個人的にはかなり聞き取りやすい英語でした。

https://youtu.be/SnrHoN632NU?si=_km32AiuVLCncNny

Documentがまだまだ薄い。

Masonでは公式ドキュメントが用意されているものの、若干痒いところに手が届かない部分があるように感じました。実際に最低限のBrickを作るのに必要な知識は用意されていますが、細かい処理をしようとするとどうやっていいかわからないといった感じです。(自分の理解力が浅いだけかもですが、、)

Brickでできること。

テンプレートから生成できるといっても実際何ができるの?と思われると思うので、自分がここまで触った中でBrickにおいてできることを簡単に紹介します。

Brickでは主にmustache templateと呼ばれる記法を用いて、テンプレートを記述します。慣れてしまえば簡単で自動生成の際に変数として使いたい値を{{hoge}}のように波括弧を二つ重ねて囲む感じです。

また、変数はstring,number,boolean,enum,array,listが用意されています。
それぞれPrimitiveな型としてわかりやすいので特に説明は不要かと思いますが、arrayとlistについてはarrayが「定義されたvaluesの中からいくつかを選択する」、listが「自由記述でいくつかを選択する」というものになっています。

Type Description Example
string A primitive string "Dash"
number A primitive number 42
boolean A primitive boolean true/false
enum An enumeration (single choice) ["red", "green", "blue"] -> "red"
array An array of strings (multiple choices) ["red", "green", "blue"] -> ["red", "blue"]
list A dynamic list of strings (open ended) ["c++", "dart", "python"]

Table: 公式ドキュメントから引用

また、これらの変数はbrick.yamlに定義することで、生成時に入力できます。
入力方法は2つあり、mason_cliのコマンドにoptionとして追記する方法と、対話形式で入力していく方法があります。optionを追記しなかった場合、brick.yamlを元に必要な変数を判断して、対話形式で尋ねてくれます。brick.yamlには必要な変数について、質問文や詳細、default値などを記載しておき、それをもとに対話が始まります。

特に便利なのがbooleanで、{{^isHoge}}FugaFuga{{/isHoge}}と書くとFugaFugaについてはisHogeがfalseの場合は無視されて生成されます。逆に{{#isHoge}}FugaFuga{{/isHoge}}のように^の代わりに#を使った場合はisHogeがtrueで無視されるようです。

EnumやListの使い方がわからん!

やっと本題です。
EnumやListが変数に用意されていることは先ほど触れた通りですし、普段プログラムを書いているあなたであれば、BrickでEnumやListを使ってやりたいことの1つや2つ思いつくかと思います。
しかし、公式ドキュメントを読んでもこれらの書き方はイマイチピンと来ず、GitHubで他の方のコードを漁りに漁って(15分くらいだけど)、やっとこさいい感じの書き方が見つけられたので、紹介します。

Enumを使ってSwitchのように扱いたい。

例えば今回試しに作成したWidgetBrickを紹介します。WidgetBrickは自分がよく使うStatelessWidget,HookWidget,ConsumerWidget,HookConsumerWidgetの4つのどれかを継承したWidgetクラスを作成する際にクラス名とWidgetの種類を入力することでファイルを自動生成してくれるものです。
この場合Widgetの種類は4つで確定しているためenumで表して、dartのswitchのように分岐させたいと考えます。現状Masonでswitchで分岐させる方法は提供されていないようなので、事前に各種booleanを計算してbooleanを利用して擬似的にswitchのように扱います。
下記にbrick.yamlで引数を宣言する箇所を記載します。

## ここから上は省略
vars:
  widget_name:
    type: string
    description: The name of Widget.
    prompt: What is the Widget name?
  widget_type:
    type: enum
    description: The type of the Widget.
    default: Stateless
    prompt: What kind of the Widget?
    values:
      - Stateless
      - Hook
      - Consumer
      - HookConsumer

引数は2つで生成するクラスの名前(widget_name)と生成するWidgetの種類(widget_type)です。

次に引数として渡したwidget_typeを使ってTemplateを分岐させたいのですが、Masonではswitchのような文法がなさそうなので、4つのbooleanを生成して擬似的にswitchのようなことをすることを考えます。ここではuse_stateless, use_hook, use_consumer, use_hook_consumer というbooleanを用意します。選択したenumの値に該当するものだけがtrueとなりそれ以外はfalseとなるようにしたいです。
ここでMasonに用意されているHooksという機能を利用します。
Hooksは生成前に実行されるpre_gen.dartと生成後に実行されるpost_gen.dartというプログラムをdartで記述することができます。
今回は自動生成で利用するためのboolean値を計算しておきたいので、pre_gen.dartで生成前に実行されるコードを記述します。

まずhooks/pre_gen.darthooks/pubspec.yamlを作成します。
ここで記述するpubspec.yamlはプロジェクト名とdartのversionとdependenciesの最低限が書いてあれば良いです。

name: bloc_hooks

environment:
  sdk: "^3.0.0"

dependencies:
  mason: ^0.1.0-dev

次にpre_gen.dartで選択されたEnumの値からboolean値を生成します。

import 'package:mason/mason.dart';

Future<void> run(HookContext context) async {
  final type = context.vars['widget_type'];
  context.vars = {
    ...context.vars,
    'use_stateless': type == 'Stateless',
    'use_hook': type == 'Hook',
    'use_consumer': type == 'Consumer',
    'use_hook_consumer': type == 'HookConsumer',
  };
}

このように記述します。
context.varsにオプションまたは対話形式で入力した変数がMap<String,dynamic>で格納されています。入力したwidget_typeを比較することで選択した値に該当するuse_hogeだけがtrueでそれ以外はfalseが入ります。...context.vars,はスプレッド演算子というもので、元のIterable(ここではMap)を展開して、要素として扱えるものです。

入力されたwidget_typeがpre_gen.dartによってuse_hogehogeというbooleanの値に変更されてcontext.barsに格納されるようになったので、次にBrickを書いていきます。

{{#use_stateless}}{{> stateless_widget }}{{/use_stateless}}{{#use_consumer}}{{> consumer_widget }}{{/use_consumer}}{{#use_hook_consumer}}{{> hook_consumer_widget }}{{/use_hook_consumer}}{{#use_hook}}{{> hook_widget }}{{/use_hook}}

1行で書かれるとちょっとわかりにくいですが、4つのuse_hogeのうち一つだけがtrueであることを考えると{{> fugafuga_widget}}が一つだけ採用されることがわかると思います。

次に{{> fugafuga_widget}}についてですが、これは簡単で/brick/ に配置されている{{~ hugahuga_widget}}をそのまま読み込んでコピペされる感じになります。
そのため、/brick/ に今回であれば{{~ stateless_widget}}, {{~ hook_widget}}, {{~ consumer_widget}}, {{~ hook_consumer_widget}}を配置し、それぞれ任意のコードを書いておくことで入力したEnum値に該当するクラスファイルを自動生成できるようになります。

気になる方はGitHubも併せてご確認ください。

https://github.com/miyaji555/bricks/tree/main/widget

Listでユーザから複数の入力を受け付けたい。

次に紹介するのはsealedクラスを定義するBrickです。
こちらはあるsealedクラスを定義した後に、そのsealedクラスを継承したクラスを任意の個数生成してくれるものです。
このような場合はlistを使うことで繰り返しの入力を受け付けることができます。
まずはbrick.yamlに下記のように宣言します。

## ここから上は省略
vars:
  class_name:
    type: string
    description: The name of sealed class.
    prompt: What is the sealed class name.
  class_list:
    type: list
    prompt: What is the new subclass name you want to create extending from the sealed class?

引数は2つで生成するsealedクラスの名前と、そのsealedクラスを継承したサブクラスの名前を持ったリストです。
次にBrickを書いていきます。

sealed class {{class_name.pascalCase()}} {}
{{#class_list}}
class {{..pascalCase()}} extends {{class_name.pascalCase()}} {}
{{/class_list}}

まず、class_nameに指定した名前をPascalCase(UpperCamelCase)で読み込んで、sealedクラスとして定義しています。
次に{{#hoge}}{{.}}{{/hoge}}とすることで引数で渡したリストの要素を一つずつ取り出して利用できます。今回はclass_listを繰り返して取得した上でPascalCaseで読み込んでいます。

このようにすることで生成したいサブクラスがいくつであっても一気に生成することができます。

試しに実行してみると

mason make sealed
? What is the sealed class name. Vehicle
? What is the new subclass name you want to create extending from the sealed class? [Car, Truck, Bicycle]
✓ Generated 1 file. (60ms)
  created vehicle.dart

こんな感じでbrick.yamlで指定した引数が一つずつ聞かれます。ちなみにlistはカンマ区切りで入力できました。

sealed class Vehicle {}

class Car extends Vehicle {}

class Truck extends Vehicle {}

class Bicycle extends Vehicle {}

このように簡単にselaedクラスとそのサブクラスを生成できました。

https://github.com/miyaji555/bricks/tree/main/sealed

おわりに

今回紹介したBrick自体は簡単なサンプル程度なのですが、高機能なBrickを作成しようとするとEnumやListは必須になってくるんじゃないかなと思いました!

2024年は技術発信も頑張ろうと思っているので、記事が参考になった方は記事とGitHubのいいね(スター)とフォローをしていただけると励みになります!
よろしくお願いします!

Discussion