Open134

gos builderをいい感じに定義したい

podhmopodhmo
  • 補完の効くいい感じのコードを書きたい
  • 余計な補完候補は排除したい
  • kin-openapiやgo-aws-sdkの利用経験からstructで覆っても楽にならない

あと

  • yamlにはモジュールがなくimportができず他のファイルに定義されたSymbolを補完できない
  • cueとかはlanguage serverがない
  • protobufは悪くない候補。しかし既存のコードが多すぎてトップダウンに新規開発以外で使えない。
podhmopodhmo

無理なら諦めてdenoとかpythonでやる。

denoのオブジェクトって順序気にしたっけ?そういうyamlのライブラリとかあったっけ?(まぁnodeのなにかがあるのでは?)

podhmopodhmo

最終的にmountのようなことがしたくなりそこでインタプリタが必要になるかもしれない。yaegiが活きることがあるかも?

とくにインタプリタが嬉しいのは該当箇所のファイル行を表示したりするときかも?

https://github.com/traefik/yaegi

ところで、structのフィールドのコメントだったり関数ではなくメソッドの定義箇所を取ろうとしたときにはunsafeを使ってsymtabを見たり大変だった。そのときはcode to schemaの方針。

https://github.com/podhmo/reflect-openapi

それをベースにしたWAFを作ったりしてた。
https://github.com/podhmo/quickapi

しかし、今回はschema to codeの話。

podhmopodhmo

connect-goとかgqlgenとかも知ってるけれど。validationとかどうしてるんだろ?

podhmopodhmo

もう少し詳しく文脈を追加すると

  • 宣言的な記述はオプション指定だけに留まり、parse部分とruntime部分を常に書く必要がある
  • 既存の言語の定義の機能に相乗りすると、結局自由度が限定されずに辛い(goのタグ、goでコメントに記述してastとかをparse)フリースタイルになる
  • 自作言語はlanguage serverの対応が辛い
podhmopodhmo

とりあえずgoでbuilderを使ってやっていく。仮に手書きでopenAPIのサブセットのbuilderを作りその知見を利用してbuilderを生成したい。

https://gist.github.com/podhmo/28b1a49dbe615501c5730f0d2e21e444

genericsを使うことでreturn selfの問題は解決できる。structを渡したらsetter methodが生えるようにしたい。

このままのコードだと型毎にフィールドの型を定義する必要があり死ぬ。

埋め込みをgenericsで使いたい。

podhmopodhmo

builderを生成するgeneratorを作るときに設定可能なメタデータをstructで定義して渡すようにしたい。

podhmopodhmo

メタデータをマージする方法はどういう方法があるだろう?

jsonのunmarshalを経由するのは何か嫌。

podhmopodhmo

全部のbuilder毎にto schemaを書いてくのは不毛かも…。

podhmopodhmo

とはいえ出力方法を別ファイルでカスタマイズというのは最終的な緊急脱出ハッチとしては便利そう。

ただフィールドが増えるたびにそれ用のコードを付け足すのは不毛では?

podhmopodhmo

一旦は渡したstructにいい感じに値が入ることを目的としてやる。

ところで未設定みたいな状態もありboolに関しても3つの状態が欲しいかもしれない。全部のプリミティブなフィールドはポインタでomitempty的なタグを付けるべきかもしれない。

podhmopodhmo

orderedMapを利用してdeepにmergeする関数を書こうとしたらびっくりするくらい面倒だった。

podhmopodhmo

とりあえず、それっぽく作った (maplib.Merge())

メタデータとして取れる設定がそれぞれの階層毎に存在する

  • type共通の設定 (e.g. description, format)
  • fieldの持つ設定 (e.g. description, required)
  • 各種type毎に個別の設定 (e.g. stringならpattern, arrayならmaxitems)

あと個別にToSchema()というメソッドを実装することになった。
これはemitter的なものを考えれば自前で実装しなければいけないのは自然かもしれない。

https://github.com/podhmo/gos/pull/3

podhmopodhmo

この手書きのbuilderから生成したいものを整理する必要がある

  • お試しで対応するprotocolがopenAPIのcomponents schemaのサブセット
  • 存在するtypeはString,Intger,Object,Array,Map
  • レンダリング自体は個別に定義

この辺を楽にしたいというのはどういうことなんだろ?
ほしいのはbuilderの記述でレンダリング自体は自前で書く必要がある(ToSchema())

例えば、String部分のBuilderの入力としては以下のようなstructを渡す。

type String struct {
	MinLength int64  `json:"minlength,omitempty"`
	MaxLength int64  `json:"maxlength,omitempty"`
	Pattern   string `json:"pattern,omitempty"`
}
podhmopodhmo

以下のようなbuilderを使ったコードが書けるようになる。

package main

import (
	"encoding/json"
	"os"

	"github.com/podhmo/gos/builder"
)

func main() {
	b := builder.New()

	Name := b.String().MinLength(1).As("Name")

	b.Object(
		b.Field("name", b.String()).Doc("name of person"),
		b.Field("age", b.Integer().Format("int32")),
		b.Field("nickname", b.Reference(Name)).Required(false),
		b.Field("father", b.ReferenceByName("Person")).Required(false),
		b.Field("friends", b.Array(b.ReferenceByName("Person"))).Required(false),
	).As("Person").Doc("person object")

	b.Object(
		b.Field("title", b.String()),
		b.Field("tests", b.Map(b.Integer()).
			PatternProperties(`\-score$`, b.Integer().Doc("score (0~100)")).
			PatternProperties(`\-grade$`, b.String().Doc("grade (A,B,C,D,E,F)"))),
	).As("TestScore")

	doc, err := builder.ToSchema(b)
	if err != nil {
		panic(err)
	}
	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	if err := enc.Encode(doc); err != nil {
		panic(err)
	}
}
podhmopodhmo

future works

  • builderの生成
  • openapiに関連する話なら、pathsの定義ができる必要がある
  • openapiに関連しない話なら、もっと他の型の組み合わせが可能化を考える
  • (別の箇所で作ったenumの定義をこのbuilderで利用することはできるか?)
podhmopodhmo

:memo: 確かpattern propertiesの定義の部分で設定にinterfaceが必要になってしまっていた。

type Map struct {
	PatternProperties map[string]TypeBuilder `json:"-,omitempty"`
}

type TypeBuilder interface {
	typevalue() *Type
	WriteType(io.Writer) error
	ToSchema(b *Builder) *orderedmap.OrderedMap
}

あと、レンダリングなどで利用するメソッドがinterface中で必要になってしまうので、これが補完の候補に載ってしまうのがちょっと煩い。

podhmopodhmo

とりあえず、APIの定義が可能か?というところから始めていけば良いかも知れない?

podhmopodhmo

(複雑さを求めてpatternPropertiesを作ったけど複数の型が持たせられる定義だとobjectにある感じかも?)

podhmopodhmo

最終的には簡単なスタックマシン的なもので実行できるような形になっていれば便利ななのでは?と思ったりしてる。

podhmopodhmo

生成するとして入力の種類はどのようなものがあるか?

  • primitive types (e.g. string int)
  • composite types (e.g. array, map)
  • object with fields

objectはproduct typesだとしてまだsum typesを考えてない。

Q. arrayとmapをbuilder側で個別に直書きすることになるのはなぜか?1つの型で十分では?

A. それぞれの型毎に別々のメタデータ(オプション)を持ちたいため。このメタデータのstruct定義がbuilder generatorに渡される入力になる。

podhmopodhmo

arrayみたいな型変数を持つものの生成結果ってどんな感じなんだろ?

あとobjectの部分を作ってて思ったけれど、builderで生成するときの入力は型が付くものの出力は型変数が失われる。

podhmopodhmo

循環参照や再帰的な参照には気をつけなくてはいけない。reference的な型を用意して名前で指定できるようにする必要はありそう。

実はjsonschemaを相手にして考えた方が面白いのかも(トップレベルの定義をどうするか?)

podhmopodhmo

zodの記述とかが読みにくくように感じるのと同様にbuilderでの記述も最高に読みやすいとは言えない。

yamlが辛いのはファイルが複数に分かれてから。goto definitionが辛かったり。参照が二段になってくると構造が追えなかったりする。

これはbuilderで解決するんだろうか?

podhmopodhmo

enumの定義は実は一番小さな考え事の例として良いのでは?

  • goのenumは手書きでやる分にはけっこう重複した記述が必要(タイポが怖い)
  • openapi docにしろprotobufにしろdescriptionと一緒に管理しようとするとけっこう重複した記述が必要(タイポが怖い)

markupの手書きは常に辛いし生成のソースとしてどちらも不適切なのかもしれない。

podhmopodhmo

enum定義用のbuilder

ところでここで生成された値は後々の定義で使えるんだろうか?
(protobufやopenapi doc)

podhmopodhmo

そろそろ機能のプロトタイプは動くようになったので名前などをわかりやすく整理する

  • 各層毎のオプションなので、structの名前はString より StringMetadataのほうがわかりやすそう
    • ダサいけどわかりやすい
    • 同様にfield名をvalueからmetadataにする
    • コード生成器に渡すことを考えるとファイルを分けたほうが良いかもしれない
  • As(<name>) で名前をつけて登録するのはわかりにくそうDefine(<name>, <typ>)みたいな関数を定義する
    • パッケージ名的には builder.New() で rootのBuilderができて builder.Define(<name>, b.Object(...)) という感じでトップレベルの定義をするのはキモいかも知れない
      • gopenapi3.NewBuilder(), gopenapi3.Define()
      • 素直にstoreを作っちゃうのが正解かも?
      • Routerを作るときにまた考える
  • XXXBuilderではなくXXXTypeにToSchema()などを定義するべきかもしれない。
podhmopodhmo

だいたいそれっぽくやれた。あとはWalk()的な関数を経由してそれぞれのメタデータに触れるようにするべきかもしれない。

podhmopodhmo

Metadataをexportしてしまったら属性などが色々補完にでてしまって辛くなったので、
メソッド経由で取得できるようにした。<Name>()というメソッドはすべてsetterとして機能しているので、GetMetadata(), GetTypeMetadata(), GetFieldMetadata()を表示するようにした。

podhmopodhmo

そろそろbuilderという名前のパッケージで作業をするのが辛くなってきた。

とりあえずプロトタイプなので/prototypeというパッケージ名にする。後々monorepoにやるかもしれないが、go.workも一旦消すことにする。

podhmopodhmo

これにともなって関数名を一箇所変えた

  • builder.New() -> prototype.NewBuilder()
podhmopodhmo

example testも追加した

  • Example<Name>() という名前の関数を作る
  • // Output: というコメントを保持する

というような感じにするとうまく動く。毎回忘れてしまう。

podhmopodhmo

enum用のbuilderを作るか。 ここで言っていたやつ

genumとかパッケージ名のprefixにgを付けるか。 (goimports friendly)

:thought-balloon: ここで一度手書きしてから、builderの生成に入る。

podhmopodhmo

メソッドが型変数を持てないのがめんどくさい。こういうコードが書けない。

	// simple
	genum.Define(b.Enum[int]("OneTwo",
		b.Value(1).Name("One"),
		b.Value(2).Name("Two"),
	))

結局Int(),String()とか用意しないとだめなのか?

podhmopodhmo

こんな感じになる?まぁstringとintで別々に書く必要がでてくるのは結構普通なような気もした。

b := genum.NewBuilder()

// simple
genum.Define(b.IntEnum("OneTwo",
	b.Value(1).Name("One"),
	b.Value(2).Name("Two"),
))

// complex
genum.Define(b.StringEnum("RGBColor",
	b.Value("R").Name("Red").Doc("red color"),
	b.Value("G").Name("Green").Doc("red color"),
	b.Value("B").Name("Blue").Doc("red color"),
)).Default("R").Doc("rgb")
podhmopodhmo

valueの型がカオスにならない?それならBuilder自体が型変数を持つようにすれば良いのでは?

podhmopodhmo

builderを分けた場合にemitするのがちょっと面倒かも?

podhmopodhmo

例えばopenapiDocにここでの定義を追加しようとしたときにどう扱うのが良いんだろう?converterが必要になりそう。それは自前で定義できたほうが良い?

podhmopodhmo

Enumにオプションとか不要かもと思っていたけれど、Parse()関数を作るか作らないか?みたいなオプションがあったりするのか。

podhmopodhmo

Toplevelで管理したいもの以外はTypeにならないというふうに考えると自然か。。

podhmopodhmo

あー、valueを型変数にしてしまうとswitchで分岐できないのか。
とりあえず、reflectでごまかす。

podhmopodhmo

Example testsを書くときに、タブの部分とコメントの部分を別の文字に変えたくなった。
つまるところグローバルな設定が欲しくなった。BuilderがConfigを持てるような設計である必要があるのかも?

podhmopodhmo

差分を確認

  • EnumのBuilderはConfigを持つ
  • EnumのBuilderは型変数を持つ
  • OASのBuilderは参照や再帰を持つ (reference )

思ったこと

  • gormftをかけたい
  • type_部分のコードが煩雑?
  • TypeMetadataは常に必要になる
  • OASのObjectのFieldとenumのValueに対応するコードをどう扱えば良いのかわからない。
podhmopodhmo

genericsで書き切ったあとだけどメソッドを分けるべきだったかも?型変数を取る実装は間違いかもしれない。

podhmopodhmo

次はOASのendpointをActionみたいな形で定義できるか試す感じにするか。

podhmopodhmo

endpointを登録できるようにしてみる。どのような記述が良さそうなんだろう?

podhmopodhmo

Actionの定義とrouterへの登録は分けるべき?

getFoo := b.Action(
	b.Input(
		b.Path("id", b.Int()),
	),
	b.Output(
		Foo,
	),
)

router.Get("/foo/{id}") // application/jsonのcontent-typeは自動で補完?
podhmopodhmo

componentのbuilderと分かれたほうが便利なんだろうか?
とりあえずは一緒にしてみることにする。

podhmopodhmo

くっつけた場合はこう。必須なのだからくっつけるべきという話はあるかもしれない。

postFoo := prototype.Post("/foo",
	b.Action(
		b.Input(
			b.Body("body", Foo),
		),
		b.Output(NoContent).Status(200),
	),
)

schemaの登録をDefine()にまかせているのだから同様に分かれていても良い気がする。

podhmopodhmo

Define()という名前をDefineType()にすると、DefineAction()のような関数を作ることができるかもしれない。

Foo := DefineType("Foo", b.Object(...))


postFoo := prototype.DefineAction("/foo",
	b.Action(
		b.Input(
			b.Body("body", Foo),
		),
		b.Output(NoContent).Status(200),
	),
).Method("POST").OperationID("postFoo").Doc("create foo")

OperationIdとMethodは常に必須なのだからDefineAction()のpositional argumentsにするべきなのでは?reflect-openapiを実装するときに関数名を頑張って名前にしていたがこちらはそもそも無名のActionを登録できることになる。変数名を参照することは流石に無理なのでは?

podhmopodhmo

一度builderを作る目的を整理してみる

  • 書くときに、どのような範囲の値を渡せるかを補完候補の選択によって制限できるようにしたい
    • (free styleな記述の後のvalidationは自由度が高すぎる)
  • 書くときに、必須な記述は自然と埋められるようにしたい
    • (任意の記述は後でカスタマイズできるようになっていてほしい)
  • 書くときに、別の箇所で定義した設定をimportしたい
  • 書くときに、不要そうな記述はデフォルトで補完されてほしい
    • (暗黙の前提を置くことになる?)
    • (subsetの範囲に絞る?公式の仕様の1:1の翻訳ではない?)
  • 読むときに、どのような設定が行われているかを把握したい
    • (positional argumentsを多用しすぎると辛い?)
  • 読むときに、参照先の参照が読みやすくなっていると良い
    • (builderも読みにくい問題は抱えそう)
    • (せめてgo to definitionで飛べる様になっていてほしい)。
      • (循環参照などの絡みで名前で参照先を指定している部分に関してはgo to definitionも無理)
  • 本当に中核を成す定義であるなら、変わらず単に出力用のprintterが変わるだけでは? (e.g. OAS2.0 -> OAS3.0, protobuf)
    • endpointの定義はUI層の記述であって頻繁に変わりうるものなのでは?
podhmopodhmo

routerとactionの定義が別で嬉しい場合もありそう?

これはdjangoのurls.pyとviews.pyのような感じ。

urls.py

# 全体のroutingを俯瞰できる
r.get("/foo", foo.List, operation_id="list_foo")
r.post("/foo", foo.Create, operation_id="create_foo")

こうなるとoperationIdがactionと紐づかずわかりにくいみたいな話はある?自動で導出したくなる。。かも。。?
まぁgrepして見つけたところでgoto definitionをすれば良いのでは?

podhmopodhmo

必須かそうでないか?で決まるのでは?Input,Outputは必須ではない気がした。

podhmopodhmo

TypeのBuilderとActionのBuilderを一緒にすると値の制限の上ではObjectがFieldを持つというような構造を b.Object().Field(...).Field(...)で作れたほうが嬉しいのかもしれない。 b.Object(b.Field(...), b.Field(...), b.Field(...))のような形だと、他のbuilderでの制限が効かなくなる。

b.Action().Input(...).Output(...)のような形のほうが嬉しい?

追記: b.Object().Field(...).Doc()) が無理になるのか。objectのdoc, fieldのdoc, fieldのtypeのdoc.

podhmopodhmo

b.Action(b.Input(b.Body(), ...)) みたいな形より b.Action(b.Body(), ..., b.Path(...)) のほうが良いかと思ったがこれはopenapiに縛られているような気がする。

podhmopodhmo

parameters, headers, query, pathとかが持てない。

b.Action(
	b.Input(
		b.Field("id", b.String()),
	).Doc("input description"),
	b.Output(
		b.String(),
	).Doc("output description"),
).Doc("action description")
podhmopodhmo

メソッドにするとタグにしやすい。
そしてprotobufにしやすい。
Serviceというククリを作るかどうか?あるいは全部に明示的にタグを付ける。

podhmopodhmo

そもそもroutingの定義はrouterにやるからあまり意識しなくて良いのでは?
operationIdのようなものを自動で導出したい場合に楽かも?という感じ。

podhmopodhmo

routingの定義がだいぶだるい

r := prototype.NewRouter(b)
r.Group("person", func(r *prototype.Grouped) {
	createPerson := b.Action("createPerson",
		b.Input(b.Param("name", b.String())),
		b.Output(Person),
	).Doc("create person")

	r.Method("POST", "/person", createPerson)
})

これをprotobuf風にするとどうなるんだろ?

podhmopodhmo

importされる感じだから

r := prototype.NewRouter(b)
r.Group("person", func(r *prototype.Grouped){
  r.Method("POST", "/person", actions.CreatePerson)
})

こんな感じになるのでは? Group == Service (protobuf)

podhmopodhmo

path,headers,query,cookieとかはどうしよう。あとrequestBody

podhmopodhmo

メタデータを入力としている段階でどのような構造でstruct同士がつながるかわかるようになっていてほしい(これの作成方法はあまりわかり易くなくても良い)。

podhmopodhmo

builderの定義の方に価値があり、toschemaとかtostringは正直なところおまけなのだけど、そちらも含めて良い感じにやりたい。

podhmopodhmo

meta builder

そろそろbuilder builderについて考えるか。現状手書きで書いているbuilder (prototype package)を生成するための定義をするためのbuilderが欲しい。どういう情報が必要か後でまとめる。

Type = {primitive type, composite type, product type}

primitive type :: string, integer
composite type :: array[T], map[V] // map[string]V
product type :: object{fields}, func{params, return}
podhmopodhmo

現状はfuncではなくaction[Input,Output]として作っている。

podhmopodhmo
product type :: action{input, output}, input{params}, output{return}
podhmopodhmo

FieldとParameterをどうやって定義すれば良いんだろう?

podhmopodhmo

とりあえず、enumから順に置き換えていけば良さそう?

podhmopodhmo

手書きで書いてたenumのbuilderを生成した。 👣
次はprototypeで作っていたやつを復活させる。

podhmopodhmo

2段階で進める

  • typeの定義の対応
  • action (endpoint) の定義の対応
podhmopodhmo

typeの定義の対応

以下の機能が不足していた

  • referenceの対応
  • 型変数を取るbuilderの対応
  • build targetのmetadata (例えばType)

いろいろいじってmain.goを作り直した。

  • genericsに対応したtext/templateが地獄 (とは言え末尾カンマが許されているので直書きがまだ許されるという感じではある)
  • 引数を取れるように調整したのがだいぶいい感じ。
  • strings.Join()を受け取るような関数をどう扱えば良いかわからない
  • ポインター対応やslice対応はまだ
podhmopodhmo

descriptionとかも渡したくなってしまった。手書きのbuilderも生成し直すか。。

podhmopodhmo

辛かった。直した。

cmd := seed.NewCommand(os.Args[1:])
options := cmd.Config

// define
b := seed.NewBuilder(options.PkgName)
b.Metadata.GeneratedBy = "github.com/podhmo/gos/gopenapi/tools"
b.NeedReference()

Type := b.BuildTarget("Type",
	b.Field("Format", seed.Symbol("string")).Tag(`json:"format"`),
)
b.InterfaceMethods("writeTyper // see: ./to_string.go")

Bool := b.Type("Bool").
	NeedBuilder().Underlying("boolean")
Int := b.Type("Int",
	b.Field("Maximum", seed.Symbol("int64")).Tag(`json:"maximum,omitempty"`),
	b.Field("Minimum", seed.Symbol("int64")).Tag(`json:"minimum,omitempty"`),
).NeedBuilder().Underlying("intger")
String := b.Type("String",
	b.Field("Pattern", seed.Symbol("string")).Tag(`json:"pattern,omitempty"`),
	b.Field("MaxLength", seed.Symbol("int64")).Tag(`json:"maxlength,omitempty"`),
	b.Field("MinLength", seed.Symbol("int64")).Tag(`json:"minlength,omitempty"`),
).NeedBuilder().Underlying("string")

Array := b.Type("Array", b.TypeVar("Items", seed.Symbol("TypeBuilder")),
	b.Field("MaxItems", seed.Symbol("int64")).Tag(`json:"maxitems,omitempty"`),
	b.Field("MinItems", seed.Symbol("int64")).Tag(`json:"minitems,omitempty"`),
).NeedBuilder().Underlying("array")
Map := b.Type("Map", b.TypeVar("Items", seed.Symbol("TypeBuilder")),
	b.Field("Pattern", seed.Symbol("string")).Tag(`json:"pattern,omitempty"`),
).NeedBuilder().Underlying("map")

Field := b.Type("Field",
	b.Field("Name", seed.Symbol("string")).Tag(`json:"-"`),
	b.Field("Typ", seed.Symbol("TypeBuilder")).Tag(`json:"-"`),
	b.Field("Description", seed.Symbol("string")).Tag(`json:"description,omitempty"`),
	b.Field("Required", seed.Symbol("bool")).Tag(`json:"-"`),
).Constructor(
	b.Arg("Name", seed.Symbol("string")),
	b.Arg("Typ", seed.Symbol("TypeBuilder")),
).NeedBuilder().Underlying("field") //?

Object := b.Type("Object",
	b.Field("Fields", seed.Symbol("[]*FieldType")).Tag(`json:"-"`),
	b.Field("Strict", seed.Symbol("bool")).Tag(`json:"-"`),
).Constructor(
	b.Arg("Fields", seed.Symbol("*FieldType")).Variadic(),
).NeedBuilder().Underlying("object")

fmt.Fprintln(os.Stderr, Type, Bool, Int, String, Array, Map, Field, Object)
// fmt.Fprintln(os.Stderr, Param, ActionInput, ActionOutput, Action)

// emit
return cmd.Do(b)
podhmopodhmo

seed packageのリファクタリング

  • seedのリファクタリング
    • ok 型変数のtemplateをわかりやすくするためにmethodに寄せる
    • ok seedのbuilderをunexportedにして使用感を試す
    • ok metadataをunexported fieldにする
    • ok doc stringを追加する
    • ok metadataが保持するのはmetadataだけにする (templateでの.Metadataを無くす)
podhmopodhmo

テンプレート中で .Metadata とアクセスする必要がなくなった。
あと、Metadataというフィールド経由で値に直接アクセスできると混乱しそうなのでGetMetadata()経由でアクセスできるようにした。factoryかsetterかをコメントに書いておくとLSPでの補完にも表示されるので雑なdoc stringを書いた (英語に自信がない)

podhmopodhmo
  • pointerに関するsetterとかもtransformで対応するべきかは悩ましい
  • Argsの値を小文字ではじめるとやばいのはUsedの問題
podhmopodhmo

ところで、metadataはmetadataのみ4というコードとrefの相性が悪い

podhmopodhmo

一旦metadataを保持するのはmetadataのみという制約は消す。TypeBuilderを保持して良いことにする。

テキトーにroutingのコードを書いて動くところまではきた。長かった。

podhmopodhmo

unionの実装

色々な型の値を可変長引数で取りたいときに欲しくなる。こういう感じ。

Array := b.Type("Array", b.TypeVar("Items", seed.Symbol("TypeBuilder")),
		b.Field("MaxItems", seed.Symbol("int64")).Tag(`json:"maxitems,omitempty"`),
		b.Field("MinItems", seed.Symbol("int64")).Tag(`json:"minitems,omitempty"`),
	).NeedBuilder().Underlying("array")

型変数(TypeVar)の定義とフィールド(Field)の定義を一括で受け取れるType()を定義したい。

podhmopodhmo

こんな感じで分岐する必要がある。これは手書き。

// Type is factory method for Type builder.
func (b *Builder) Type(name string, typeVarOrFieldList ...typeAttr) *Type {
	tvars := make([]*TypeVarMetadata, 0, len(typeVarOrFieldList))
	fields := make([]*FieldMetadata, 0, len(typeVarOrFieldList))
	for _, tattr := range typeVarOrFieldList {
		switch t := tattr.(type) {
		case *TypeVar:
			tvars = append(tvars, t.metadata)
		case *Field:
			fields = append(fields, t.metadata)
		default:
			panic(fmt.Sprintf("unexpected type: %T", tattr))
		}
	}

	t := &Type{
		typeBuilder: &typeBuilder[*Type]{metadata: &TypeMetadata{
			Name:       Symbol(name),
			Underlying: name,
			TVars:      tvars,
			Fields:     fields,
			Used:       map[string]bool{},
		}},
	}
	t.ret = t
	b.metadata.Types = append(b.metadata.Types, t.metadata)
	return t
}
podhmopodhmo

それぞれはBuilderなので

TypeVar := DefineType("TypeVar", Type(...)).NeedBuilder()
Field := DefineType("Field", Type(...)).NeedBuilder()

TypeVarOrField := Union(TypeVar, Field) 

とかで大丈夫?このときのConstructorの値がどうなるか?

podhmopodhmo
Type(
  Field("Fields", "[]*Field"),
  Field("TypeVars", "[]*TypeVar"),
).Constructor(
  Arg("TypeVarsAndFields", "TypeVarOrField").Variadic(),
)

どこに渡すかの値がないな。今までは引数名でいい感じに計算してたarg=field

podhmopodhmo

名前で探すのはUsed経由でConstructorやSetterがやっていたけれど。
Type経由で探すような機構を生やしたくないな。例えば同じ型のGoodXXX,BadXXXみたいなものへ分配しようとしたときに死ぬ。

束縛先を指定するしかないか。AssignTo("Fields", "TypeVars")

podhmopodhmo

variadicみたいなノリでメタデータを増やすのは問題なさそう。
あと、名前を明示的に指定できると、違う名前のフィールドに代入できるようになるので自然な感じのものが書けそう。デフォルト値をtoLowerにしてあげるのが良いだろう。

ここに複数の値を入れるとすると混乱があるんだろうか?Unionの定義ときに名前を気にしたくないな。。とはいえ、Unionが与えられていたら分岐するのでは?

それならsliceが与えられていたら分岐する?ならTransform()で対応できるんだろうか?

podhmopodhmo

こういうコードは行けるのか。とりあえず困るまではTransform()でお茶を濁すか。ToSlice2とか作ろう。

package main

import "fmt"

func main() {
	var Ob struct {
		X int
		y int
	}
	Ob.X, Ob.y = 10, 20
	fmt.Println(Ob)
}
podhmopodhmo

つまりこんな感じになれば良い。

// union
TypeVar := DefineType("TypeVar", Type(...)).NeedBuilder()
Field := DefineType("Field", Type(...)).NeedBuilder()

TypeVarOrField := Union(TypeVar, Field) 

// builder
Object := DefineType("Object", Type(
	Field("Fields", "[]*Field"),
	Field("TypeVars", "[]*TypeVar"),
).Constructor(
	Arg("tattrs", "TypeVarOrField").Variadic().
		AssignFields("Fields", "TypeVars").
		Transform(func(v string) string {
			return fmt.Sprintf(`toSlice2(v TypeVarOrField) (*Field, *TypeVar) { return ... })`)
		}),
))
podhmopodhmo

こんなswitchで分岐するgenerics不毛だなと思いつつ、text/templateの中で別途型毎にループしたりしなかったりをしたくないとなると書かなくちゃいけない?

https://go.dev/play/p/Vvr2giBBtOK

Union("X","Y").As("XorY")で増えるとかやりたい気がしたけれど不毛な感じはする。

手書きでいけるのはいけるんだけど、一つZとか追加したときに追加し忘れが発生したくないな。

podhmopodhmo

genericsを生成する関数でも作っておけば良いのでは?
あとUnionはDefineType()でnamedにするべき?

podhmopodhmo

seed.SymbolはUnionの対象に含まれるのだっけ?Union自体は*Typeではない?

podhmopodhmo
func (b *Builder) Input(args ...inputArg) *Input {
	var body *Body
	var params []*Param
	for _, a := range args {
		switch a := a.(type) {
		case *Body:
			body = a
		case *Param:
			params = append(params, a)
		}
	}
	t := &Input{
		InputBuilder: &InputBuilder[*Input]{
			_Type: &_Type[*Input]{rootbuilder: b, metadata: &TypeMetadata{Name: "", underlying: "Input"}},
			metadata: &InputMetadata{
				Body: body, Params: params,
			},
		},
	}
	t.ret = t
	return t
}


// inputArg is the one of pseudo sum types (union).
type inputArg interface {
	inputarg()
}

func (t *Param) inputarg() {}

func (t *Body) inputarg() {}
podhmopodhmo

builderの価値

  • parser 入力を文法で縛るもの
  • builder 文法上での入力に制約や制限を付けて補助するもの

という感じで見ると両者は近しい。応用先として何があるか考えてみる

podhmopodhmo
  • 定義と変換(e.g. enum)
  • クエリービルダー (e.g. SQL)

ちょっと視点を変えると対応関係を記述したいという話になるかも?

  • 型の変換
podhmopodhmo

マクロ的ななにか

text/templateの中でslicesと単数の値を同じように扱おうとしたときにいわゆるlist.mapのような関数が欲しくなる。

さらにこれにunionへの対応などを考えたときにコードが辛くなっていく。

以下のようなインターフェイスを考えることはできるか?

type Macro interface {
  Define() string
}

関数がこのようなメソッドも保持しておけば便利なのでは?

podhmopodhmo

openapiの生成が一段落したあと

何をやると良いんだろう?目処が付いたらあとは細かな調整そうなので興味がなくなってきた。

同じような試みとして以下をやってみるか

  • go code
  • gql
  • protobuf

builderとしての利用を試すのに

  • SQL builder

とかやってみるのが良いのかどちらが良いんだろ?

podhmopodhmo

openapiの細々としたものを追加する

  • 不足していそうなフィールドを埋める
    • numberがなかった
  • いわゆるOpenAPIのrootの定義を用意する
    • infoとか
    • この辺はbuilderである必要はない気がする
      • builderにしないとdefault値が埋め込めないのか
      • builderにするとbuilderの名前空間を汚染してしまう
podhmopodhmo

色々やってみてopenapi的には不足しているもの

  • oneOf,allOf,anyOf,not -- 必要?
    • oneOfはunionとして定義したいかも?
  • zero値をdefaultとして渡せないかも? (pointer setter)
podhmopodhmo

☠️ いくつか嫌なところがあるな

  • 至る所でFormatのSetterが存在する
  • b.Body()にFieldが渡せてしまう
  • Bodyに渡すTitleが消える?(body.SchemaのTitle?)
  • Tagsが設定されていない?
podhmopodhmo

利用できる型をより制限する

b.Body()にFieldが渡せてしまう

これは結構つらい感じ。理由はFieldも実態としてはTypeとして扱われるので

// 正解
b.Output(b.Object(b.Field("name", b.String())))

// 失敗
b.Output(b.Field("name", b.String()))
podhmopodhmo

現状以下の種類しかない

  • Type().NeedBuilder() -> target型のbuilderを生成
  • Type() -> metadata的なstructを生成
  • Unionで生成した型を利用

Unionで生成してそれを利用する形にすることはできるのでは?

podhmopodhmo

全部がTypeBuilderとかになってしまっている。

type TypeBuilder interface {
	GetTypeMetadata() *TypeMetadata
	writeType(io.Writer) error // see: ./to_string.go
	toSchemer                  // see: ./to_schema.go
}
podhmopodhmo

とりあえず以下のようなコードを書いて制限はできる

type Type interface {
  TypeBuilder
  typ()
podhmopodhmo

InterfaceMethods()のようなメソッドを用意すれば良いか。

podhmopodhmo

定義する場所

  • グローバル変数
  • 何かの関数の中

前者だとimportできるしかし、vscodeでの補完が嬉しくない

globalのvar部分

テキトーな関数の中 (main)

podhmopodhmo
  • 最悪関数の中で頑張ると良いかもしれない
func Definitions() (Definitions struct { Person *openapigen.Object}) {
  Definitions.Person = openapigen.Define("Person", b.Object(...))
  return
}
podhmopodhmo

何度も何度も名前を書かなくてはいけないのが嫌な感じはする。以下あたりを使って何かできないのかな?(少なくともActionの名前はこれを使える?)

  • runtime.Caller()
  • runtime.FuncForPC()
podhmopodhmo

pagination

paginationがほしいというよりは、paginationという例を使って、何らかのhelper関数を定義することの利点を伝える例がほしい?

例えば、この辺を参考に例を作る

スキーマファースト開発のためのOpenAPI(Swagger)設計規約 | フューチャー技術ブログ

podhmopodhmo

Typeを受け取ってTypeを返す関数を書く

// with pagination
func Pagination(b *openapigen.Builder, typ openapigen.Type) *openapigen.Object {
	return b.Object(
		b.Field("totalCount", b.Int()),
		b.Field("hasMore", b.Bool()),
		b.Field("cursor", b.String()),
		b.Field("data", typ).Doc("response data of api"),
	).Doc("totalCount, hasMore, cursor fields are metadata for pagination")
}

こんな関数を書いてあげると手軽にpaginationを追加できる。

	ListTask := b.Action("ListTask",
		b.Input(b.Param("sort", b.String().Enum([]string{"createdAt", "-createdAt"})).AsQuery()),
		b.Output(Pagination(b, b.Array(Task))),
	).Doc("paginated list task")
podhmopodhmo
  "paths": {
    "/tasks": {
      "get": {
        "operationId": "ListTask",
        "description": "paginated list task",
        "parameters": [
          {
            "name": "sort",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "createdAt",
                "-createdAt"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "totalCount, hasMore, cursor fields are metadata for pagination",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "totalCount, hasMore, cursor fields are metadata for pagination",
                  "properties": {
                    "totalCount": {
                      "type": "integer"
                    },
                    "hasMore": {
                      "type": "boolean"
                    },
                    "cursor": {
                      "type": "string"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Task"
                      },
                      "description": "response data of api"
                    }
                  },
                  "required": [
                    "totalCount",
                    "hasMore",
                    "cursor",
                    "data"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "description": "default error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "task"
        ]
      }
    },

podhmopodhmo

実はminifiyという意味ではallOfを使うというのもアリなのかもしれない?(openapi的な観点からは)

podhmopodhmo

allOfは概念としてprotobuf,graphqlに対応できるんだろうか?