ボイラープレートを一括生成するコマンドを作る

2022 7/12

APIサーバーの開発をGoで行っていて、アーキテクチャとしてクリーンアーキテクチャに沿った形で設計した。

責務が分かれたり、レイヤー毎のユニットテストがやりやすくなったのはいいのだが、新しいエンドポイントを追加する際に追加しなければならない記述が多いのが非常に不満だった。
controllers, database, interactors, repository, entities…
エンドポイント追加のたびにこれらにコンストラクタだったりDIだったりを書いていくのは非常にだるい

退屈なことはプログラムにやらせよう、ということでこれらのボイラープレートを自動で一括で生成できる君を作ったのでまとめておく。

目次

目指したゴール

クリーンアーキテクチャでエンドポイントを追加する際には各レイヤーに記述を書かなければならず、最低限以下のようなファイルと記述が必要になる。

・controllers: コンストラクタ、interactorsの呼び出し
・database: コンストラクタ、実際のDB処理
・interactors: コンストラクタ、アプリケーションロジック
・repository: databaseのinterface

もちろん処理内容によってはドメインロジックが必要だったり、リクエストパラメータのbind処理が必要になったりするが、それを抜きにしてもこれだけある。

やってられない。毎回0からこれらを記述するのはだるいのでこれらを一括生成したい。
もちろんアプリケーションロジックなどは処理内容によって大きく変わるが、コンストラクタなどの雛形は一括生成することの恩恵を受けられるはず。

イメージとしては以下のようなコマンドを打つと、適切なファイル名で最低限必要なコードが書かれたものを一括生成する。

[生成コマンド] file_name=admin_user

// 生成ファイル
interfaces/controllers/admin_user.go
interfaces/database/admin_user.go
usecase/interactors/admin_user.go
usecase/repository/admin_user.go

このコマンドを作るにあたってのポイントは、以下のように命名規則を揃えること。

・ファイル名はsnake_case
・ファイル名に応じて書かれる変数名はUpperCamelCaseもしくはlowerCamelCase
 (Goのスコープを適切に設定するため)

今回はコマンドラインの引数としてファイル名のsnake_caseを受け取って、ファイル内の記述で使うUpperCamelCaselowerCamelCasesnake_caseから変換することとした。

ライブラリの調査

目的のことができそうなライブラリを色々探してて最初に見つかったのがwireというライブラリ。

GitHub
GitHub - google/wire: Compile-time Dependency Injection for Go
GitHub - google/wire: Compile-time Dependency Injection for GoCompile-time Dependency Injection for Go. Contribute to google/wire development by creating an account on GitHub.

DIライブラリでわりと人気があるものだが、いわゆるDIコンテナとはちょっと違うらしい。
そもそもDIコンテナのある言語をちゃんと触ったことがないのでこの辺の違いはわかってない。

このライブラリで目的が達成できるか考えてみたが、結論厳しそう。
ちゃんと理解できてるか怪しいが、こういったツールはあくまでDIにおける組み立て部分をよしなにやってくれるものであって、組み立てに必要なパーツは自分で定義する必要があるという認識でいる。
今回やりたいのはそのパーツを自動生成したいという話なので目的にマッチしないかなと思った。

もう少し調べてみると、標準ライブラリにtext/templateなるパッケージがあるらしい。
引数を渡すとその引数を埋め込んだ文字列を出力してくれるとのこと。

CLOVER🍀
Go標準のテンプレートエンジンtext/templateを使ってみる - CLOVER🍀
Go標準のテンプレートエンジンtext/templateを使ってみる - CLOVER🍀これは、なにをしたくて書いたもの? Goのテンプレートエンジンを調べてみようかなと思ったのですが、標準ライブラリにあるようなので、こちらを試して みることにしました...

これだ…!!!

ファイル名を引数として渡して、それを変数名に変換した上で文字列として埋め込んだファイルを出力すればいけそう。
というわけでやってみた。

text/templateで必要なファイルを生成する

text/templateの基本的な使い方を見た後に、実際に目的のファイルを生成するという順で見ていく。

基本的な使い方

package main

import (
    "log"
    "os"
    "text/template"
)

func main() {
    tmpl := "Hello {{.}}!\n"

    t, err := template.New("sample").Parse(tmpl)
    if err != nil {
        log.Fatal(err)
    }
    if err = t.Execute(os.Stdout, "World"); err != nil {
        log.Fatal(err)
    }
}

// コンソール上にHello Worldが出力される

10行目のようなtemplateの文字列を用意しておいて、12行目で*template.Templateを作成する。
16行目でt.Executeとし、templateにWorldを埋め込んだ文字列を標準出力に出すようにしている。

template中の{{.}}の部分にt.Executeで渡した引数の文字列が埋め込まれるようになっている。

t.Executeの第一引数はio.Writerになっているので、ここを標準出力じゃなくてファイルにすればファイルに出力できそう。
*template.Template作成時にも別で用意したファイルをパースすることができるっぽい。

これらを組み合わせてファイル名の文字列を引数として渡すと目的の記述が書かれたファイルを生成する処理を書いてみる。

ファイル名に応じたファイルを生成する

具体的な記述に入る前に全体的なディレクトリ構成を抑えておく。クリーンアーキテクチャに沿ったディレクトリ構成に加えて、今回templateを作成するために必要なファイル群はtemplatesディレクトリとして分けた。
ここにコマンド実行のためのmain.goを配置する形にしている。

.
├── entities
├── infra
├── interfaces
│   ├── controllers
│   └── database
├── templates
│   ├── controllers.tmpl
│   ├── database.tmpl
│   ├── domain.tmpl
│   ├── interactors.tmpl
│   ├── main.go
│   ├── main_test.go
│   └── repository.tmpl
└── usecase
    ├── interactors
    └── repository

まずテンプレートとなるファイルを用意する。
今回は比較的記述の少ないrepository層の記述を自動生成してみる。

//go:generate mockgen -source=$GOFILE -destination=./mock/$GOFILE
package repository

type {{.UpperCamelCase}}Repository interface {
	Find() (*{{.UpperCamelCase}}, error)
}

先ほどとは異なり、埋め込み部分は{{.UpperCamelCalse}}となっていて、これは変数として埋め込みができるようにこうしている。
ファイルによって埋め込む文字列がUpperCamelCaseとlowerCamelCaseの両方だったりすることと、可読性向上のためにこうしている。

次にこのtemplateファイルを使用してファイルを自動生成する部分の記述。

package main

import (
    "flag"
    "log"
    "os"
    "text/template"
)

func main() {
    flag.Parse()
  	snake := flag.Arg(0)
  	lowerCamel := toLowerCamelCase(snake)
  	upperCamel := cases.Title(language.English, cases.NoLower).String(lowerCamel)

    t, err := template.ParseFiles("templates/repository.tmpl")
    if err != nil {
        log.Fatal(err)
    }
  fp, err := os.Create("usecase/repository/" + snake + ".go")
  if err != nil {
        log.Println("error creating"+f.fp+"file", err)
  }
  defer fp.Close()
    
  if err = t.Execute(fp,  map[string]string{"UpperCamelCase": upperCamel}); err != nil {
        log.Fatal(err)
    }
}

11-14行目で受け取った引数のsnake_caseをlowerCamelCaseとUpperCamelCaseに変換している。
16行目で先ほどのテンプレートファイルを使用して、*template.Templateを生成。
20行目でusecase/repository配下に必要なファイルを作成して、26行目でupperCamelのmapでテンプレートに埋め込む文字を渡している。

これで以下のようにコマンドを叩いてみる。

go run templates/main.go sample_file

すると該当のファイルが生成される。

//go:generate mockgen -source=$GOFILE -destination=./mock/$GOFILE
package repository

type SampleFileRepository interface {
	Find() (*SampleFile, error)
}

あとは他のファイルも自動的に生成されるようにそれぞれのtemplateファイルを用意した上で、自動生成するコード部分を以下のようにループ処理させるように変更する。

package main

import (
    "flag"
    "log"
    "os"
    "text/template"
)

func main() {
	flag.Parse()
	snake := flag.Arg(0)
	lowerCamel := toLowerCamelCase(snake)
	upperCamel := cases.Title(language.English, cases.NoLower).String(lowerCamel)

	files := []struct {
		tmpl string
		fp   string
		data map[string]string
	}{
		{
			tmpl: "entities.tmpl",
			fp:   "entities/",
			data: map[string]string{"UpperCamelCase": upperCamel},
		},
		{
			tmpl: "controllers.tmpl",
			fp:   "interfaces/controllers/",
			data: map[string]string{
				"LowerCamelCase": lowerCamel,
				"UpperCamelCase": upperCamel,
			},
		},
		{
			tmpl: "database.tmpl",
			fp:   "interfaces/database/",
			data: map[string]string{"UpperCamelCase": upperCamel},
		},
		{
			tmpl: "interactors.tmpl",
			fp:   "usecase/interactors/",
			data: map[string]string{"UpperCamelCase": upperCamel},
		},
		{
			tmpl: "repository.tmpl",
			fp:   "usecase/repository/",
			data: map[string]string{"UpperCamelCase": upperCamel},
		},
	}

	for _, f := range files {
		t, err := template.ParseFiles("templates/" + f.tmpl)
		if err != nil {
			log.Println(err)
		}

		fp, err := os.Create(f.fp + snake + ".go")
		if err != nil {
			log.Println("error creating"+f.fp+"file", err)
		}
		defer fp.Close()

		if err = t.Execute(fp, f.data); err != nil {
			log.Println(err)
		}
	}
}

これでコマンドを叩くだけでボイラープレートが一括生成できるようになった。
あとは目的の処理に応じて記述内容を修正していけばよい。

ついでにmakefileを使ってコマンドを管理した。

scaffold:
	go run templates/main.go ${FILE_NAME}

// 呼び出し方
make scaffold FILE_NAME=sample_file

このコマンド1つで5つのファイルをそれっぽい記述が書かれた状態で自動生成できるようになったので開発効率がかなり上がってめでたし。

まとめ

Goって本当こういう標準ライブラリ充実してるなと思う
引数がインターフェースになってて汎用性ある形になってるし、色々応用効きそうなのもよい

こういう自動化作業はやってて非常に楽しいし達成感が得られやすいから好きだ

この記事が参考になったからコーヒーくらいおごってもいいぜという方は、以下からサポートいただけると次の記事書くモチベになりますのでよろしくお願いします

Buy Me A Coffee

参考

あわせて読みたい
template package - text/template - Go Packages
Qiita
Go text/template で文字列作成 - Qiita
Go text/template で文字列作成 - Qiita Go text/template で文字列作成 みなさん、こんにちは! Go書いてますか? 今回は、text/template を使って ひな形 から 文字列 を生成する方法をご紹介します。 tex...

この記事を書いた人

コメント

コメントする

目次
閉じる