Open12

goでrequestのbind用のライブラリを考えてみる

podhmopodhmo

https://github.com/gorilla/schema

で良いという説はあるがreflect用の小さなコードを吐いてそれを利用するみたいなことをやるのも良いかも知れない。

podhmopodhmo

実行せずとも正しさを確認したい。
その場合には素直に静的なコードになってると嬉しいが

podhmopodhmo

一度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}
podhmopodhmo

これを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)
}
podhmopodhmo

query stringのパースにはフラットなものを対応するだけでも良いけれど、値のバリデーションなどのときにはネストした構造に対応したくなるかもしれない。その場合は型ごとにコードを生成しておき、それを渡り歩いて評価していけば良いような気がする。

podhmopodhmo

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
}
podhmopodhmo

素直に手書きすると以下のようになる。

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
}
podhmopodhmo

先程の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
}

それっぽく動くコードはこちら

https://gist.github.com/podhmo/05e499bb1d903514ac8e80170821158e

あと、エラー時には即時エラーにしたい場合とまとめてエラーとして返したい場合と無視したい場合があるかもしれない。query stringのparseに関しては無視したい。

podhmopodhmo

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
	}
}
podhmopodhmo

enums的なものの定義の場合に、switchで書けないのは残念かもしれない。

podhmopodhmo

横道だけど手でそのまま書くとしたら、以下みたいな方がわかりやすいかもしれない。

Bind(rt, "Sort").ToStringEnumQuery("sort", "id", "-id")