Open12
goでrequestのbind用のライブラリを考えてみる
で良いという説はあるがreflect用の小さなコードを吐いてそれを利用するみたいなことをやるのも良いかも知れない。
実行せずとも正しさを確認したい。
その場合には素直に静的なコードになってると嬉しいが
一度bindを考えずにvmの雰囲気を味わってみる
例えば以下のような値を変更するreflectのコードがあるとする
package main
import (
"fmt"
"reflect"
)
func Touch[T any](ob *T) {
rv := reflect.ValueOf(ob).Elem()
{
rf := rv.FieldByName("Name") // Field()のほうが高速
rf.SetString("*bar*")
}
}
type Person struct {
Name string
Age int
}
func main() {
ob := &Person{Name: "foo", Age: 20}
fmt.Printf("before: %#+v\n", ob)
Touch(ob)
fmt.Printf("after : %#+v\n", ob)
}
結果はこう
before: &main.Person{Name:"foo", Age:20}
after : &main.Person{Name:"*bar*", Age:20}
これをScan()という関数でコードを生成して、Apply()という関数で適用する事を考えてみる。
以下のようなイメージ。
func main() {
type CreatePersonInput struct {
Name string
Age int
}
rt := reflect.TypeOf(CreatePersonInput{})
code := Scan(rt) // 一度生成したらcache
input := &CreatePersonInput{Name: "foo", Age: 20}
fmt.Printf("before: %#+v\n", input)
Apply(input, code)
fmt.Printf("after : %#+v\n", input)
}
// 型を見て動作を生成 (e.g. 例えば入力に対するvalidationなど)
func Scan(rt reflect.Type) []Op {
return Code(
Elem(), OpPush,
FieldByName(rt, "Name"), OpPush, SetString("*bar*"), OpPop,
FieldByName(rt, "Age"), OpPush, SetInt(100), OpPop,
OpPop)
}
requestの値をbindしてみる
いよいよrequestのbindを考えてみる。先程のコードはプリミティブ過ぎたかも知れない。
以下のようなrequestを受け取りこの値をbindしたstructを受け取る事を考えてみる
GET /items?sort=-id&size=100
// {Size:100 Sort:-id Debug:false} のような結果がほしい
structの定義は以下のようなもの
type Input struct {
Size int64
Sort string // "id","-id"
Debug bool
}
素直に手書きすると以下のようになる。
func run() error {
req, err := http.NewRequest("GET", "/items?sort=-id&size=100", nil)
if err != nil {
return err
}
q := req.URL.Query()
input := Input{}
if v := q.Get("size"); v != "" {
n, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return fmt.Errorf("parse size: %w (value=%v)", err, v)
}
input.Size = n
}
if v := q.Get("sort"); v != "" {
switch v {
case "id", "-id":
input.Sort = v
default:
return fmt.Errorf("parse sort: %w (value=%v)", err, v)
}
}
fmt.Printf("%+v\n", input)
return nil
}
先程のvmのコードを利用して考えてみる。
合成命令のようなものを考えるなら実はわざわざスタックマシンみたいな感じにするのは大げさかもしれない?(ネストしたフィールドに対応したりするときには欲しくなる)
以下のような形で使われる
type Sort string
func run() error {
type Input struct {
Size int64
Sort string // "id","-id"
Sort2 Sort // "id","-id"
Debug bool
}
parse := Scan[Input]()
req, err := http.NewRequest("GET", "/items?sort=-id&size=100&sort2=-id", nil)
if err != nil {
return err
}
input, err := parse(req)
if err != nil {
return err
}
fmt.Printf("%+v\n", input)
return nil
}
それっぽく動くコードはこちら
あと、エラー時には即時エラーにしたい場合とまとめてエラーとして返したい場合と無視したい場合があるかもしれない。query stringのparseに関しては無視したい。
reflectでタグを読むこんで作成みたいなことはできなくもない。
無駄にgenericsを使ってparseする関数を返す関数を定義したが、この辺はstructを返した方が良いかもしれない。
func Scan[T any]() func(*http.Request) (T, error) {
var ob T
rt := reflect.TypeOf(ob)
code := Code( // 本当は生成
OpElem, OpPush,
BindIntQuery(rt, "size", "Size"),
BindStringEnumsQuery(rt, "sort", "Sort", []string{"id", "-id"}),
BindStringEnumsQuery(rt, "sort2", "Sort2", []string{"id", "-id"}),
OpPop,
)
return func(req *http.Request) (T, error) {
var input T
if err := Apply(req, &input, code); err != nil {
return input, fmt.Errorf("apply: %w", err)
}
return input, nil
}
}
enums的なものの定義の場合に、switchで書けないのは残念かもしれない。
横道だけど手でそのまま書くとしたら、以下みたいな方がわかりやすいかもしれない。
Bind(rt, "Sort").ToStringEnumQuery("sort", "id", "-id")