gos builderをいい感じに定義したい
- 補完の効くいい感じのコードを書きたい
- 余計な補完候補は排除したい
- kin-openapiやgo-aws-sdkの利用経験からstructで覆っても楽にならない
あと
- yamlにはモジュールがなくimportができず他のファイルに定義されたSymbolを補完できない
- cueとかはlanguage serverがない
- protobufは悪くない候補。しかし既存のコードが多すぎてトップダウンに新規開発以外で使えない。
無理なら諦めてdenoとかpythonでやる。
denoのオブジェクトって順序気にしたっけ?そういうyamlのライブラリとかあったっけ?(まぁnodeのなにかがあるのでは?)
以前呟いたtweet
inspired by
最終的にmountのようなことがしたくなりそこでインタプリタが必要になるかもしれない。yaegiが活きることがあるかも?
とくにインタプリタが嬉しいのは該当箇所のファイル行を表示したりするときかも?
ところで、structのフィールドのコメントだったり関数ではなくメソッドの定義箇所を取ろうとしたときにはunsafeを使ってsymtabを見たり大変だった。そのときはcode to schemaの方針。
それをベースにしたWAFを作ったりしてた。
しかし、今回はschema to codeの話。
昔やっていたpythonの記事(正直なところこの方針も自分一人でやる分には悪くない)
connect-goとかgqlgenとかも知ってるけれど。validationとかどうしてるんだろ?
もう少し詳しく文脈を追加すると
- 宣言的な記述はオプション指定だけに留まり、parse部分とruntime部分を常に書く必要がある
- 既存の言語の定義の機能に相乗りすると、結局自由度が限定されずに辛い(goのタグ、goでコメントに記述してastとかをparse)フリースタイルになる
- 自作言語はlanguage serverの対応が辛い
とりあえずgoでbuilderを使ってやっていく。仮に手書きでopenAPIのサブセットのbuilderを作りその知見を利用してbuilderを生成したい。
genericsを使うことでreturn selfの問題は解決できる。structを渡したらsetter methodが生えるようにしたい。
このままのコードだと型毎にフィールドの型を定義する必要があり死ぬ。
埋め込みをgenericsで使いたい。
builderを生成するgeneratorを作るときに設定可能なメタデータをstructで定義して渡すようにしたい。
メタデータをマージする方法はどういう方法があるだろう?
jsonのunmarshalを経由するのは何か嫌。
とりあえず、順序付きのdictに変換することを試みる。
マージのやつ思い出した。これか。
marshallではなくunmarshalなら
だったが今回は使えない。
全部のbuilder毎にto schemaを書いてくのは不毛かも…。
とはいえ出力方法を別ファイルでカスタマイズというのは最終的な緊急脱出ハッチとしては便利そう。
ただフィールドが増えるたびにそれ用のコードを付け足すのは不毛では?
一旦は渡したstructにいい感じに値が入ることを目的としてやる。
ところで未設定みたいな状態もありboolに関しても3つの状態が欲しいかもしれない。全部のプリミティブなフィールドはポインタでomitempty的なタグを付けるべきかもしれない。
orderedMapを利用してdeepにmergeする関数を書こうとしたらびっくりするくらい面倒だった。
とりあえず、それっぽく作った (maplib.Merge())
メタデータとして取れる設定がそれぞれの階層毎に存在する
- type共通の設定 (e.g. description, format)
- fieldの持つ設定 (e.g. description, required)
- 各種type毎に個別の設定 (e.g. stringならpattern, arrayならmaxitems)
あと個別にToSchema()というメソッドを実装することになった。
これはemitter的なものを考えれば自前で実装しなければいけないのは自然かもしれない。
この手書きの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"`
}
以下のような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)
}
}
future works
- builderの生成
- openapiに関連する話なら、pathsの定義ができる必要がある
- openapiに関連しない話なら、もっと他の型の組み合わせが可能化を考える
- (別の箇所で作ったenumの定義をこのbuilderで利用することはできるか?)
: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中で必要になってしまうので、これが補完の候補に載ってしまうのがちょっと煩い。
とりあえず、APIの定義が可能か?というところから始めていけば良いかも知れない?
(複雑さを求めてpatternPropertiesを作ったけど複数の型が持たせられる定義だとobjectにある感じかも?)
最終的には簡単なスタックマシン的なもので実行できるような形になっていれば便利ななのでは?と思ったりしてる。
生成するとして入力の種類はどのようなものがあるか?
- 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に渡される入力になる。
zodの記述とかが読みにくくように感じるのと同様にbuilderでの記述も最高に読みやすいとは言えない。
yamlが辛いのはファイルが複数に分かれてから。goto definitionが辛かったり。参照が二段になってくると構造が追えなかったりする。
これはbuilderで解決するんだろうか?
enumの定義は実は一番小さな考え事の例として良いのでは?
- goのenumは手書きでやる分にはけっこう重複した記述が必要(タイポが怖い)
- openapi docにしろprotobufにしろdescriptionと一緒に管理しようとするとけっこう重複した記述が必要(タイポが怖い)
markupの手書きは常に辛いし生成のソースとしてどちらも不適切なのかもしれない。
そろそろ機能のプロトタイプは動くようになったので名前などをわかりやすく整理する
- 各層毎のオプションなので、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()
などを定義するべきかもしれない。
そろそろbuilderという名前のパッケージで作業をするのが辛くなってきた。
とりあえずプロトタイプなので/prototype
というパッケージ名にする。後々monorepoにやるかもしれないが、go.workも一旦消すことにする。
enum用のbuilderを作るか。 ここで言っていたやつ
genumとかパッケージ名のprefixにgを付けるか。 (goimports friendly)
:thought-balloon: ここで一度手書きしてから、builderの生成に入る。
メソッドが型変数を持てないのがめんどくさい。こういうコードが書けない。
// simple
genum.Define(b.Enum[int]("OneTwo",
b.Value(1).Name("One"),
b.Value(2).Name("Two"),
))
結局Int(),String()とか用意しないとだめなのか?
こんな感じになる?まぁ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")
valueの型がカオスにならない?それならBuilder自体が型変数を持つようにすれば良いのでは?
builderを分けた場合にemitするのがちょっと面倒かも?
例えばopenapiDocにここでの定義を追加しようとしたときにどう扱うのが良いんだろう?converterが必要になりそう。それは自前で定義できたほうが良い?
Enumにオプションとか不要かもと思っていたけれど、Parse()関数を作るか作らないか?みたいなオプションがあったりするのか。
Toplevelで管理したいもの以外はTypeにならないというふうに考えると自然か。。
あー、valueを型変数にしてしまうとswitchで分岐できないのか。
とりあえず、reflectでごまかす。
Example testsを書くときに、タブの部分とコメントの部分を別の文字に変えたくなった。
つまるところグローバルな設定が欲しくなった。BuilderがConfigを持てるような設計である必要があるのかも?
差分を確認
- EnumのBuilderはConfigを持つ
- EnumのBuilderは型変数を持つ
- OASのBuilderは参照や再帰を持つ (reference )
思ったこと
- gormftをかけたい
- type_部分のコードが煩雑?
- TypeMetadataは常に必要になる
- OASのObjectのFieldとenumのValueに対応するコードをどう扱えば良いのかわからない。
genericsで書き切ったあとだけどメソッドを分けるべきだったかも?型変数を取る実装は間違いかもしれない。
次はOASのendpointをActionみたいな形で定義できるか試す感じにするか。
endpointを登録できるようにしてみる。どのような記述が良さそうなんだろう?
Actionの定義とrouterへの登録は分けるべき?
getFoo := b.Action(
b.Input(
b.Path("id", b.Int()),
),
b.Output(
Foo,
),
)
router.Get("/foo/{id}") // application/jsonのcontent-typeは自動で補完?
componentのbuilderと分かれたほうが便利なんだろうか?
とりあえずは一緒にしてみることにする。
くっつけた場合はこう。必須なのだからくっつけるべきという話はあるかもしれない。
postFoo := prototype.Post("/foo",
b.Action(
b.Input(
b.Body("body", Foo),
),
b.Output(NoContent).Status(200),
),
)
schemaの登録をDefine()
にまかせているのだから同様に分かれていても良い気がする。
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を登録できることになる。変数名を参照することは流石に無理なのでは?
一度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層の記述であって頻繁に変わりうるものなのでは?
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をすれば良いのでは?
openapiに引きずられたくない…
必須かそうでないか?で決まるのでは?Input,Outputは必須ではない気がした。
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.
b.Action(b.Input(b.Body(), ...))
みたいな形より b.Action(b.Body(), ..., b.Path(...))
のほうが良いかと思ったがこれはopenapiに縛られているような気がする。
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")
actionって関数では?
メソッドにするとタグにしやすい。
そしてprotobufにしやすい。
Serviceというククリを作るかどうか?あるいは全部に明示的にタグを付ける。
そもそもroutingの定義はrouterにやるからあまり意識しなくて良いのでは?
operationIdのようなものを自動で導出したい場合に楽かも?という感じ。
packageも必要?
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風にするとどうなるんだろ?
importされる感じだから
r := prototype.NewRouter(b)
r.Group("person", func(r *prototype.Grouped){
r.Method("POST", "/person", actions.CreatePerson)
})
こんな感じになるのでは? Group == Service (protobuf)
path,headers,query,cookieとかはどうしよう。あとrequestBody
メタデータを入力としている段階でどのような構造でstruct同士がつながるかわかるようになっていてほしい(これの作成方法はあまりわかり易くなくても良い)。
builderの定義の方に価値があり、toschemaとかtostringは正直なところおまけなのだけど、そちらも含めて良い感じにやりたい。
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}
現状はfunc
ではなくaction[Input,Output]
として作っている。
product type :: action{input, output}, input{params}, output{return}
FieldとParameterをどうやって定義すれば良いんだろう?
とりあえず、enumから順に置き換えていけば良さそう?
手書きで書いてたenumのbuilderを生成した。 👣
次はprototypeで作っていたやつを復活させる。
2段階で進める
- typeの定義の対応
- action (endpoint) の定義の対応
typeの定義の対応
以下の機能が不足していた
- referenceの対応
- 型変数を取るbuilderの対応
- build targetのmetadata (例えばType)
いろいろいじってmain.goを作り直した。
- genericsに対応したtext/templateが地獄 (とは言え末尾カンマが許されているので直書きがまだ許されるという感じではある)
- 引数を取れるように調整したのがだいぶいい感じ。
- strings.Join()を受け取るような関数をどう扱えば良いかわからない
- ポインター対応やslice対応はまだ
descriptionとかも渡したくなってしまった。手書きのbuilderも生成し直すか。。
辛かった。直した。
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)
seed packageのリファクタリング
- seedのリファクタリング
- ok 型変数のtemplateをわかりやすくするためにmethodに寄せる
- ok seedのbuilderをunexportedにして使用感を試す
- ok metadataをunexported fieldにする
- ok doc stringを追加する
- ok metadataが保持するのはmetadataだけにする (templateでの.Metadataを無くす)
テンプレート中で .Metadata
とアクセスする必要がなくなった。
あと、Metadataというフィールド経由で値に直接アクセスできると混乱しそうなのでGetMetadata()経由でアクセスできるようにした。factoryかsetterかをコメントに書いておくとLSPでの補完にも表示されるので雑なdoc stringを書いた (英語に自信がない)
- pointerに関するsetterとかもtransformで対応するべきかは悩ましい
- Argsの値を小文字ではじめるとやばいのはUsedの問題
ところで、metadataはmetadataのみ4というコードとrefの相性が悪い
一旦metadataを保持するのはmetadataのみという制約は消す。TypeBuilderを保持して良いことにする。
テキトーにroutingのコードを書いて動くところまではきた。長かった。
手書きのコードを消した
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()を定義したい。
こんな感じで分岐する必要がある。これは手書き。
// 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
}
それぞれはBuilderなので
TypeVar := DefineType("TypeVar", Type(...)).NeedBuilder()
Field := DefineType("Field", Type(...)).NeedBuilder()
TypeVarOrField := Union(TypeVar, Field)
とかで大丈夫?このときのConstructorの値がどうなるか?
Type(
Field("Fields", "[]*Field"),
Field("TypeVars", "[]*TypeVar"),
).Constructor(
Arg("TypeVarsAndFields", "TypeVarOrField").Variadic(),
)
どこに渡すかの値がないな。今までは引数名でいい感じに計算してたarg=field
名前で探すのはUsed経由でConstructorやSetterがやっていたけれど。
Type経由で探すような機構を生やしたくないな。例えば同じ型のGoodXXX,BadXXXみたいなものへ分配しようとしたときに死ぬ。
束縛先を指定するしかないか。AssignTo("Fields", "TypeVars")
variadicみたいなノリでメタデータを増やすのは問題なさそう。
あと、名前を明示的に指定できると、違う名前のフィールドに代入できるようになるので自然な感じのものが書けそう。デフォルト値をtoLowerにしてあげるのが良いだろう。
ここに複数の値を入れるとすると混乱があるんだろうか?Unionの定義ときに名前を気にしたくないな。。とはいえ、Unionが与えられていたら分岐するのでは?
それならsliceが与えられていたら分岐する?ならTransform()
で対応できるんだろうか?
こういうコードは行けるのか。とりあえず困るまではTransform()でお茶を濁すか。ToSlice2とか作ろう。
package main
import "fmt"
func main() {
var Ob struct {
X int
y int
}
Ob.X, Ob.y = 10, 20
fmt.Println(Ob)
}
つまりこんな感じになれば良い。
// 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 ... })`)
}),
))
こんなswitchで分岐するgenerics不毛だなと思いつつ、text/templateの中で別途型毎にループしたりしなかったりをしたくないとなると書かなくちゃいけない?
Union("X","Y").As("XorY")
で増えるとかやりたい気がしたけれど不毛な感じはする。
手書きでいけるのはいけるんだけど、一つZとか追加したときに追加し忘れが発生したくないな。
genericsを生成する関数でも作っておけば良いのでは?
あとUnionはDefineType()でnamedにするべき?
seed.SymbolはUnionの対象に含まれるのだっけ?Union自体は*Typeではない?
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() {}
builderの価値
- parser 入力を文法で縛るもの
- builder 文法上での入力に制約や制限を付けて補助するもの
という感じで見ると両者は近しい。応用先として何があるか考えてみる
- 定義と変換(e.g. enum)
- クエリービルダー (e.g. SQL)
ちょっと視点を変えると対応関係を記述したいという話になるかも?
- 型の変換
マクロ的ななにか
text/templateの中でslicesと単数の値を同じように扱おうとしたときにいわゆるlist.mapのような関数が欲しくなる。
さらにこれにunionへの対応などを考えたときにコードが辛くなっていく。
以下のようなインターフェイスを考えることはできるか?
type Macro interface {
Define() string
}
関数がこのようなメソッドも保持しておけば便利なのでは?
openapiの生成が一段落したあと
何をやると良いんだろう?目処が付いたらあとは細かな調整そうなので興味がなくなってきた。
同じような試みとして以下をやってみるか
- go code
- gql
- protobuf
builderとしての利用を試すのに
- SQL builder
とかやってみるのが良いのかどちらが良いんだろ?
openapiの細々としたものを追加する
- 不足していそうなフィールドを埋める
- numberがなかった
- いわゆるOpenAPIのrootの定義を用意する
- infoとか
- この辺はbuilderである必要はない気がする
- builderにしないとdefault値が埋め込めないのか
- builderにするとbuilderの名前空間を汚染してしまう
利用できる型をより制限する
b.Body()にFieldが渡せてしまう
これは結構つらい感じ。理由はFieldも実態としてはTypeとして扱われるので
// 正解
b.Output(b.Object(b.Field("name", b.String())))
// 失敗
b.Output(b.Field("name", b.String()))
現状以下の種類しかない
- Type().NeedBuilder() -> target型のbuilderを生成
- Type() -> metadata的なstructを生成
- Unionで生成した型を利用
Unionで生成してそれを利用する形にすることはできるのでは?
全部がTypeBuilderとかになってしまっている。
type TypeBuilder interface {
GetTypeMetadata() *TypeMetadata
writeType(io.Writer) error // see: ./to_string.go
toSchemer // see: ./to_schema.go
}
とりあえず以下のようなコードを書いて制限はできる
type Type interface {
TypeBuilder
typ()
InterfaceMethods()のようなメソッドを用意すれば良いか。
定義する場所
- グローバル変数
- 何かの関数の中
前者だとimportできるしかし、vscodeでの補完が嬉しくない
globalのvar部分
テキトーな関数の中 (main)
- 最悪関数の中で頑張ると良いかもしれない
func Definitions() (Definitions struct { Person *openapigen.Object}) {
Definitions.Person = openapigen.Define("Person", b.Object(...))
return
}
何度も何度も名前を書かなくてはいけないのが嫌な感じはする。以下あたりを使って何かできないのかな?(少なくともActionの名前はこれを使える?)
- runtime.Caller()
- runtime.FuncForPC()
pkg/callerinfoを作ってみた
pagination
paginationがほしいというよりは、paginationという例を使って、何らかのhelper関数を定義することの利点を伝える例がほしい?
例えば、この辺を参考に例を作る
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")
"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"
]
}
},
実はminifiyという意味ではallOfを使うというのもアリなのかもしれない?(openapi的な観点からは)
allOfは概念としてprotobuf,graphqlに対応できるんだろうか?