Go: 構造体の定義からフィールド値でフィルタできるイテレータを自動生成するCLIを作ってみた(miyamo2/filtgen)
30秒でなんとなくわかる概要
タイトルの通り今回も作ってみた系です
早速どんなものを作ったのかサクッと書いていくので実際にTryしていただける方は以降のセクションは飛ばしてもらってもよいかも
インプットとなる構造体です
package main
import (
"time"
)
type User struct {
ID string `filtgen:"eq,ne"`
Name string `filtgen:"eq"`
Age int `filtgen:"eq,ge,le"`
Disabled bool `filtgen:"eq"`
LastActive time.Time `filtgen:"matches"`
}
filtgen generate
コマンドで先ほどの構造体を食べさせます
filtgen generate -s user.go
以下のようなdefined-typeと
type UserSlice []User
type UserMap[T comparble] map[T]User
type UserSeq iter.Seq[User]
type UserSeq2[T any] iter.Seq2[T]
以下のようなメソッドが生成されます
同じシグニチャのメソッドがUserMap
, UserSeq
, UserSeq2
でもそれぞれ生成されますがレシーバ以外は基本的に一緒です
ただしUserSeq
のみ戻り値の型が若干異なります
func(s UserSlice) IDEq(str string) UserSeq2 { ... }
func(s UserSlice) IDNe(str string) UserSeq2 { ... }
func(s UserSlice) NameEq(str string) UserSeq2 { ... }
func(s UserSlice) AgeEq(i int) UserSeq2 { ... }
func(s UserSlice) AgeGe(i int) UserSeq2 { ... }
func(s UserSlice) AgeLe(i int) UserSeq2 { ... }
func(s UserSlice) DisableEq(b bool) UserSeq2 { ... }
func(s UserSlice) LastActiveMatches(matcher func(time.Time) bool) UserSeq2 { ... }
使う側のコードはこんな感じです
var s []User = getUsers()
for i, v := range UserSlice(s).IDEq("ABCD12345") {
fmt.Printf("%d: %v\n", i, v)
}
for i, v := range UserSlice(s).NameEq("Bob") {
fmt.Printf("%d: %v\n", i, v)
}
for i, v := range UserSlice(s).AgeGe(20) {
fmt.Printf("%d: %v\n", i, v)
}
for i, v := range UserSlice(s).LastActiveMatches(func(t time.Time) bool { return t.Before(time.Now().Add(-time.Year)) }) {
fmt.Printf("%d: %v\n", i, v)
}
// メソッドチェインでも書けます
for i, v := range UserSlice(s).Disabled(false).LastActiveMatches(func(t time.Time) bool { return t.Before(time.Now().Add(-time.Year)) }) {
fmt.Printf("%d: %v\n", i, v)
}
生成手順
インストール
go install github.com/miyamo2/filtgen@latest
使い方
filtgen
タグを設定する
ステップ1: 構造体にフィルタ機能を適用したいフィールドにfiltgen
タグを設定してください
filtgen
タグについては後述します
package main
import (
"time"
)
type Foo struct {
StringField string `filtgen:"*"`
IntField int `filtgen:"*"`
BoolField bool `filtgen:"*"`
TimeField time.Time `filtgen:"*"`
ErrorField error `filtgen:"*"`
}
任意でgo:generate
ディレクティブを設定することも可能です
//go:generate filtgen generate -s $GOFILE
package main
filtgen generate
の実行
ステップ2: filtgen
のサブコマンド、generate
でコード生成を行います
--source
(-s
)は対象ファイルを指定するためのフラグで必須です
filtgen generate -s your_struct.go
ステップ1でgo:generate
ディレクティブを設定した方はいつものgo generate
でいけます
個人的にはこちらをおすすめしたい
go generate ./...
ステップ3: 生成されたコードを使ってみる
errSomething := errors.New("something")
s := []Foo{
{StringField: "a", IntField: 1, BoolField: true, TimeField: time.Now()},
{StringField: "b", IntField: 2, BoolField: false, TimeField: time.Now(), ErrorField: errSomething},
{StringField: "c", IntField: 3, BoolField: true, TimeField: time.Now().Add(-(time.Hour * 2))},
}
for i, v := range FooSlice(s).StringFieldGe("a") {
fmt.Printf("%d: %s\n", i, v.StringField)
}
// Output: 0: a
// 1: b
// 2: c
for i, v := range FooSlice(s).IntFieldGt(1) {
fmt.Printf("%d: %s\n", i, v.StringField)
}
// Output: 1: b
// 2: c
for i, v := range FooSlice(s).BoolFieldEq(true) {
fmt.Printf("%d: %s\n", i, v.StringField)
}
// Output: 0: a
// 2: c
for i, v := range FooSlice(s).TimeFieldMatches(func(t time.Time) bool { return t.Before(time.Now().Add(-time.Hour)) }) {
fmt.Printf("%d: %s\n", i, v.StringField)
}
// Output: 2: c
for i, v := range FooSlice(s).ErrorFieldIs(errSomething) {
fmt.Printf("%d: %s\n", i, v.StringField)
}
// Output: 1: b
実際にfiltgen
で生成したコードは exampleをご覧ください
filtgenが生成するコード
型
filtgen
では以下のdefined-typeが生成されます
-
XxxSlice
([]T
) -
XxxMap[U]
(map[U compareble]T
) -
XxxSeq[T]
(iter.Seq[T]
) -
XxxSeq2[U]
(iter.Seq2[U, T]
)
型の名前は対象の構造体に応じてUpperCamelで解決されます
e.g. User
-> UserSlice
.
フィルタを利用する場合、slice
, map
, iter.Seq
もしくはiter.Seq2
をそれぞれ対応するdefined-typeにキャストをしてください
s := []User{
{Name: "Alice"},
{Name: "Bob"},
}
for i, v := range UserSlice(s).NameEq("Alice") {
fmt.Printf("%d: %s\n", i, v.Name)
}
メソッド
filtgen
では以下のメソッドを生成することが可能です
ただし、フィールドの型によっては生成できないメソッドもあるので要注意です
型/メソッドの対応一覧についてはこちらを参照してください
メソッド名はフィールド名に応じてUpperCamelで解決されます
e.g. Name
-> NameEq
.
XxxEq
イテレータを引数と等価な項目のみにフィルタします
stringの場合はstrings.Compare
によって判断
type User struct {
Name string `filtgen:"eq"`
}
for i, v := range UserSlice(s).NameEq("Alice") {
fmt.Printf("%d: %s\n", i, v.Name)
}
XxxNe
イテレータを引数と等価でない項目のみにフィルタします
stringの場合はstrings.Compare
によって判断
type User struct {
Name string `filtgen:"ne"`
}
for i, v := range UserSlice(s).NameNe("Alice") {
fmt.Printf("%d: %s\n", i, v.Name)
}
XxxGt
イテレータを引数より大きい項目のみにフィルタします
stringの場合はstrings.Compare
によって判断
type User struct {
Name string `filtgen:"gt"`
}
for i, v := range UserSlice(s).NameGt("Alice") {
fmt.Printf("%d: %s\n", i, v.Name)
}
XxxLt
イテレータを対象のフィールドが引数より小さい項目のみにフィルタします
stringの場合はstrings.Compare
によって判断
type User struct {
Name string `filtgen:"lt"`
}
for i, v := range UserSlice(s).NameLt("Alice") {
fmt.Printf("%d: %s\n", i, v.Name)
}
XxxGe
イテレータを対象のフィールドが引数と等価か引数より大きい項目のみにフィルタします
stringの場合はstrings.Compare
によって判断
type User struct {
Name string `filtgen:"ge"`
}
for i, v := range UserSlice(s).NameGe("Alice") {
fmt.Printf("%d: %s\n", i, v.Name)
}
XxxLe
イテレータを対象のフィールドが引数と等価か引数より小さい項目のみにフィルタします
stringの場合はstrings.Compare
によって判断
type User struct {
Name string `filtgen:"le"`
}
for i, v := range UserSlice(s).NameLe("Alice") {
fmt.Printf("%d: %s\n", i, v.Name)
}
XxxMatches
イテレータを対象のフィールドが引数の関数でtrueを返す項目のみにフィルタします
type User struct {
Name string `filtgen:"matches"`
}
for i, v := range UserSlice(s).NameMatches(func(s string) bool { return strings.HasPrefix(s, "A") }) {
fmt.Printf("%d: %s\n", i, v.Name)
}
XxxIs
イテレータを対象のフィールドが引数のエラーと一致する項目のみにフィルタします
errors.Is
によって判断
type Transaction struct {
ID string `filtgen:"eq"`
Err error `filtgen:"is"`
}
for i, v := range TransactionSlice(s).ErrIs(fmt.Errorf("something")) {
fmt.Printf("%d: %s\n", i, v.ID)
}
XxxIsnt
イテレータを対象のフィールドが引数のエラーと一致しない項目のみにフィルタします
errors.Is
によって判断
type Transaction struct {
ID string `filtgen:"eq"`
Err error `filtgen:"isnt"`
}
for i, v := range TransactionSlice(s).ErrIsnt(fmt.Errorf("something")) {
fmt.Printf("%d: %s\n", i, v.ID)
}
型ごとの対応メソッド一覧
型╲メソッド | XxxEq |
XxxNe |
XxxGt |
XxxLt |
XxxGe |
XxxLe |
XxxMatches |
XxxIs |
XxxIsnt |
---|---|---|---|---|---|---|---|---|---|
string |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
int |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
int8 |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
int16 |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
int32 |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
int64 |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
uint |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
uint8 |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
uint16 |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
uint32 |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
uint64 |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
float32 |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
float64 |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
complex64 |
❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
complex128 |
❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
byte |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
rune |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
error |
❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
bool |
✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
その他 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
filtgen
タグ
filtgen
タグには以下の値が設定できます
複数指定したい場合は,
区切りで記述できます
type A struct {
StringField string `filtgen:"eq,ne"`
}
値 | 説明 |
---|---|
* |
フィールドの型が対応するすべてのメソッドを生成する。 |
eq |
XxxEq メソッドを生成する。 |
ne |
XxxNe メソッドを生成する。 |
gt |
XxxGt メソッドを生成する。 |
lt |
XxxLt メソッドを生成する。 |
ge |
XxxGe メソッドを生成する。 |
le |
XxxLe メソッドを生成する。 |
matches |
XxxMatches メソッドを生成する。 |
is |
XxxIs メソッドを生成する。 |
isnt |
XxxIsnt メソッドを生成する。 |
おわりに
元ネタを読み込む処理にastを使っているのですが他言語含めastを触るのは初めてだったので刺激的でした
出力部分にはtext/template
を使っているので今後テンプレートをいい感じに分割していきたいです
filtgen
が気になった方や気に入ってくれた方はstarを、バグや改善案を見つけてくれた方はイシューを、共鳴してくれた方はPRをいただけると励みになります
Discussion