pythonのクラス定義からUnmarshalJSON()でのvalidation付きのgoのstruct定義を生成する
pythonのクラス定義からUnmarshalJSON()でのvalidation付きのgoのstruct定義を生成してみる。(より細かなバリエーションは https://github.com/podhmo/egoist/tree/master/examples/e2e/generate-structkit にある)
なぜUnmarshalJSON()付きでの生成?
required/unrequiredのvalidationはしつつも、unmarshal後のstructでポインターのフィールドをなるべく持ちたくない。昔いろいろこのあたりの挙動について考えたりしていた。詳細はこのあたりに。
- goでJSONのunmarshal時のrequiredなfieldの扱いについて
- goでrequired/unrequiredに対応しつつpointerを使わないunmarshalJSONのエラーメッセージについて
その時の結論としてvalidation付きのUnmarshalJSON()を実装するのがマシだろうということになった。
しかし真面目に実装しようとすると手が止まる程度には辛い。自分で手書きするのはつらすぎたので、生成することにした。本当は依存ゼロが嬉しいが、validation errorを扱うのにmapで情報を保持するmulti errorが欲しくなった。既存のライブラリが見つかればそちらを使いたかったがなかったので書いた1。
結局欲しいものは
なぜUnmarshalJSON()付きのstruct生成をegoistに設けようと思ったかというと、結局欲しいものは以下の様なLoad関数だけなのでは?と言う発想を持ったため。名前に関してはLoad()ではなくBind()という名前でも良いかもしれない。
func Load(io.Reader, interface{}) error
例えば、設定ファイルの読み込みに関してもファイルIOを受け取り、この関数でvalidation付きで読み込めば良いし。あるいはREST APIのことを考えても、net/httpのHandlerの中で、RequestのBodyを受け取り、この関数でvalidation付きで読み込めば良い。あるいは内部的なAPIだけを考えるならフォームデータでのPOSTなどは考えなくて良い。するとio.Readerに対する対応だけである程度は十分なのではという考えになった。
出力のことを考えるなら、validation済みのものだけを内部では保持しているはずなので、何も気にしなくて良い。というわけで以下があれば良いという結論になった。
- struct定義
- UnmarshalJSON()
hello world的なtutorial
ここからはegoistの利用方法の話。使い方の説明も兼ねてhello world的なtutorialをしてみる。
structの生成
egoist init structkit
で雛形が生成される。
$ egoist init structkit level:INFO message:create definitions.py name:egoist.cliL23
生成されたdefinitions.pyで生成してあげると、利用例のコードが出力される。
$ python definitions.py generate [D] create ./model [F] create ./model/objects.go
自動で生成されたdefinitions.pyに含まれるのは以下の様なクラス定義。
definitions.py (の一部)
class Author: name: str # createdAt: datetime.date class Article: title: str author: t.Optional[Author] content: str comments: t.List[Comment] class Comment: author: Author content: str
クラス定義以外の部分のコードは後で貼るが、app.include("egoist.generators.define_struct_set")
をしていることで、@app.define_struct_set(...)
が使える様になっている。もちろんこのあたりの名前は暫定的なものでAPIは変わる可能性がある。関数名のmodels__objects.go
を利用して出力するファイル名はmodels/objects.go
になる。
definitions.py (の一部)
from __future__ import annotations import typing as t from egoist.app import App, SettingsDict, parse_args settings: SettingsDict = {"rootdir": "", "here": __file__} app = App(settings) app.include("egoist.directives.define_struct_set") # (このあたりにクラス定義。実際の利用ではimportしてきても良い) @app.define_struct_set("egoist.generators.structkit:walk") def model__objects() -> None: from egoist.generators.structkit import runtime, structkit with runtime.generate(structkit, classes=[Article]) as m: m.package("model") if __name__ == "__main__": for argv in parse_args(sep="-"): app.run(argv)
structの利用
生成されたstructを使ってみる。loadパッケージという名前のパッケージを作り、これを呼び出すmain.goを作って生成されたstructのloadを確認してみることにする。
loadパッケージの作成
テキトーに作る。
$ go mod init m $ mkdir -p load $ edit load/load.go # editorで編集
load/load.go
package load import ( "encoding/json" "io" "github.com/k0kubun/pp" ) func Load(r io.Reader, ob interface{}) error { decoder := json.NewDecoder(r) return decoder.Decode(ob) } func LoadAndPrint(r io.Reader, ob interface{}) error { if err := Load(r, ob); err != nil { return err } pp.Println(ob) return nil }
cmd/load/main.goの作成
この辺でrepl的なものがあればそれで動作チェックがしたいかもしれない。あるいはテストを書いてそれで動作チェックとするか。面倒なので今回はCLIを作成して試すことにする。
CLIの作成方法については以前書いたので工程の詳細は省略する。
テキトーにcmd/load/main.go
を作る。
# 手抜きのためにclikitを使って生成したdefinitions.pyからコピーしてくる $ (cd /tmp && egoist init clikit) $ edit definitions.py # editorで編集 $ python definitions.py generate [F] no change ./model/objects.go [D] create ./cmd/load [F] create ./cmd/load/main.go
空のデータを与えてみて試してみる。エラーが出た。
$ go run cmd/load/main.go --filename <(echo '{}') 2020/05/16 07:05:26 !!Error -- { "summary": "title, required", "messages": { "comments": [ { "text": "required" } ], "content": [ { "text": "required" } ], "title": [ { "text": "required" } ] } } exit status 1
- comments
- content
- title
がなかった。
今度は正しそうなデータを与えてみる。loadできた。細かい話をすると、commentsは必須(pythonでOptional
ではない)だったので、空配列であっても渡す必要があった。
$ go run cmd/load/main.go --filename <(echo '{"title": "hello world", "content": "Hello worlld, this is my first article. ...", "comments": []}') &model.Article{ Title: "hello world", Author: (*model.Author)(nil), Content: "Hello worlld, this is my first article. ...", Comments: []model.Comment{}, }
今度は子オブジェクトで間違えてみる。commentsに空オブジェクトを渡してみる。今度は以下のフィールドが不足している。
- comments.author
- comments.content
何番目のどの要素が間違えたなどの情報が無いので不親切かもしれない。あるいは、今回自分で定義したLoad()
の部分で、エラーメッセージと共に渡されたio.Readerの値を表示するエラーでwrapしてあげても良かったかもしれない。
$ go run cmd/load/main.go --filename <(echo '{"comments": [{}], "title": "hello world", "content": "Hello worlld, this is my first article. ..."}') 2020/05/16 07:11:52 !!Error -- { "summary": "comments, author, required", "messages": { "comments.author": [ { "text": "required" } ], "comments.content": [ { "text": "required" } ] } }
structの改変 -- 明示的なunrequired
先程のcommentsフィールドの扱いを変えてみる。基本的には以下の様なルールで変換される。
- 通常のフィールド -> 値扱いでrequired
- Optionalなフィールド -> ポインター扱いでunrequired
しかし、配列に関しては、値扱いでunrequiredになってほしい。このような場合にはmetadataでunrequiredを直接指定してあげる。
+from egoist.generators.structkit.runtime import metadata, field settings: SettingsDict = {"rootdir": "", "here": __file__} app = App(settings) @@ -18,7 +19,7 @@ class Article: title: str author: t.Optional[Author] content: str - comments: t.List[Comment] + comments: t.List[Comment] = field(metadata=metadata(required=False))
生成し直した後に、commentsを省略した値を渡してみる。今度は大丈夫。
$ python definitions.py generate [F] update ./model/objects.go [F] no change ./cmd/load/main.go $ go run cmd/load/main.go --filename <(echo '{"title": "hello world", "content": "Hello worlld, this is my first article. ..."}') &model.Article{ Title: "hello world", Author: (*model.Author)(nil), Content: "Hello worlld, this is my first article. ...", Comments: []model.Comment{}, }
structの改変 -- tagsの追加
出力されるstruct定義はのコードは以下の様になっている(unmarshalJSON()を除いたもの)。タグ部分を触りたいこともあると思う。今度はjsonだけではなくyamlのフィールドもつけてみる。
// this file is generated by egoist.generators.structkit type Article struct { Title string `json:"title"` Author *Author `json:"author"` Content string `json:"content"` Comments []Comment `json:"comments"` } type Author struct { Name string `json:"name"` } type Comment struct { Author Author `json:"author"` Content string `json:"content"` }
set_metadata_handler()
でtagsの取扱を変える。デフォルトではjsonタグだけを設定していた。
+from egoist.generators.structkit.runtime import set_metadata_handler, Metadata settings: SettingsDict = {"rootdir": "", "here": __file__} app = App(settings) @@ -31,6 +32,13 @@ class Comment: def model__objects() -> None: from egoist.generators.structkit import runtime, structkit + @runtime.set_metadata_handler + def metadata_handler( + cls: t.Type[t.Any], *, name: str, info: t.Any, metadata: runtime.Metadata + ) -> None: + """Yaml also added""" + metadata["tags"] = {"json": [name.rstrip("_")], "yaml": [name.rstrip("_")]} + with runtime.generate(structkit, classes=[Article]) as m: m.package("model")
生成結果はこのように変わる。yamlタグが追加された。
type Article struct { - Title string `json:"title"` - Author *Author `json:"author"` - Content string `json:"content"` - Comments []Comment `json:"comments"` + Title string `json:"title" yaml:"title"` + Author *Author `json:"author" yaml:"author"` + Content string `json:"content" yaml:"content"` + Comments []Comment `json:"comments" yaml:"comments"` } func (a *Article) UnmarshalJSON(b []byte) error { @@ -58,7 +58,7 @@ func (a *Article) UnmarshalJSON(b []byte) error { } type Author struct { - Name string `json:"name"` + Name string `json:"name" yaml:"name"` } func (a *Author) UnmarshalJSON(b []byte) error { @@ -85,8 +85,8 @@ func (a *Author) UnmarshalJSON(b []byte) error { } type Comment struct { - Author Author `json:"author"` - Content string `json:"content"` + Author Author `json:"author" yaml:"author"` + Content string `json:"content" yaml:"content"` } func (c *Comment) UnmarshalJSON(b []byte) error {
今回利用したdefinitions.py
今回利用したdefinitions.pyは以下。
definitions.py
from __future__ import annotations import typing as t from egoist.app import App, SettingsDict, parse_args from egoist.generators.structkit.runtime import metadata, field from egoist.generators.structkit.runtime import set_metadata_handler, Metadata settings: SettingsDict = {"rootdir": "", "here": __file__} app = App(settings) app.include("egoist.directives.define_struct_set") app.include("egoist.directives.define_cli") class Author: name: str # createdAt: datetime.date class Article: title: str author: t.Optional[Author] content: str comments: t.List[Comment] = field(metadata=metadata(required=False)) class Comment: author: Author content: str @app.define_struct_set("egoist.generators.structkit:walk") def model__objects() -> None: from egoist.generators.structkit import runtime, structkit @runtime.set_metadata_handler def metadata_handler( cls: t.Type[t.Any], *, name: str, info: t.Any, metadata: runtime.Metadata ) -> None: """Yaml also added""" metadata["tags"] = {"json": [name.rstrip("_")], "yaml": [name.rstrip("_")]} with runtime.generate(structkit, classes=[Article]) as m: m.package("model") # cli @app.define_cli("egoist.generators.clikit:walk") def cmd__load(*, filename: str) -> None: from egoist.generators.clikit import runtime, clikit with runtime.generate(clikit) as m: os_pkg = m.import_("os") load_pkg = m.import_("m/load") model_pkg = m.import_("m/model") f = m.symbol("f") ob = m.symbol("ob") err = m.symbol("err") m.stmt(f"{f}, {err} := {os_pkg.Open(filename)}") m.stmt(f"if {err} != nil {{") with m.scope(): m.return_(err) m.stmt("}") m.stmt(f"defer {f}.Close()") m.stmt(f"var {ob} {model_pkg.Article}") p = m.let("p", f"&{ob}") m.return_(load_pkg.LoadAndPrint(f, p)) if __name__ == "__main__": for argv in parse_args(sep="-"): app.run(argv)
出力されたファイルたち
長くなってしまうのでこれまで頑なに貼っていなかったが、生成されたgoのファイルは以下のようなもの。
その前に全体のファイル構造を貼っておく。
$ tree . . ├── Makefile ├── cmd │ └── load │ └── main.go ├── definitions.py ├── go.mod ├── go.sum ├── load │ └── load.go └── model └── objects.go 4 directories, 7 files
structの出力部分は後々ファイルを分割して出力する機能もつけるかもしれない。しかし、ファイルを分割して出力する機能をつけようとすると、削除されたクラスや参照への対応がめんどくさい。不要になったファイルを消すことができない。そのあたりをどうするかは考える必要がありそう。
models/objects.go
package model import ( "github.com/podhmo/maperr" "encoding/json" ) // this file is generated by egoist.generators.structkit type Article struct { Title string `json:"title" yaml:"title"` Author *Author `json:"author" yaml:"author"` Content string `json:"content" yaml:"content"` Comments []Comment `json:"comments" yaml:"comments"` } func (a *Article) UnmarshalJSON(b []byte) error { var err *maperr.Error // loading internal data var inner struct { Title *string `json:"title"`// required Author *json.RawMessage `json:"author"` Content *string `json:"content"`// required Comments *json.RawMessage `json:"comments"` } if rawErr := json.Unmarshal(b, &inner); rawErr != nil { return err.AddSummary(rawErr.Error()) } // binding field value and required check { if inner.Title != nil { a.Title = *inner.Title } else { err = err.Add("title", maperr.Message{Text: "required"}) } if inner.Author != nil { a.Author = &Author{} if rawerr := json.Unmarshal(*inner.Author, a.Author); rawerr != nil { err = err.Add("author", maperr.Message{Error: rawerr}) } } if inner.Content != nil { a.Content = *inner.Content } else { err = err.Add("content", maperr.Message{Text: "required"}) } if inner.Comments != nil { a.Comments = []Comment{} if rawerr := json.Unmarshal(*inner.Comments, &a.Comments); rawerr != nil { err = err.Add("comments", maperr.Message{Error: rawerr}) } } } return err.Untyped() } type Author struct { Name string `json:"name" yaml:"name"` } func (a *Author) UnmarshalJSON(b []byte) error { var err *maperr.Error // loading internal data var inner struct { Name *string `json:"name"`// required } if rawErr := json.Unmarshal(b, &inner); rawErr != nil { return err.AddSummary(rawErr.Error()) } // binding field value and required check { if inner.Name != nil { a.Name = *inner.Name } else { err = err.Add("name", maperr.Message{Text: "required"}) } } return err.Untyped() } type Comment struct { Author Author `json:"author" yaml:"author"` Content string `json:"content" yaml:"content"` } func (c *Comment) UnmarshalJSON(b []byte) error { var err *maperr.Error // loading internal data var inner struct { Author *json.RawMessage `json:"author"`// required Content *string `json:"content"`// required } if rawErr := json.Unmarshal(b, &inner); rawErr != nil { return err.AddSummary(rawErr.Error()) } // binding field value and required check { if inner.Author != nil { if rawerr := json.Unmarshal(*inner.Author, &c.Author); rawerr != nil { err = err.Add("author", maperr.Message{Error: rawerr}) } } else { err = err.Add("author", maperr.Message{Text: "required"}) } if inner.Content != nil { c.Content = *inner.Content } else { err = err.Add("content", maperr.Message{Text: "required"}) } } return err.Untyped() }
cmd/load/main.goの方も載せる。名前がcmd__load
扱いなのは後で変えると思う。
cmd/load/main.go
package main import ( "flag" "os" "log" "m/load" "m/model" ) // this file is generated by egoist.generators.clikit // Option ... type Option struct { Filename string // for `-filename` Args []string // cmd.Args } func main() { opt := &Option{} cmd := flag.NewFlagSet("cmd__load", flag.ContinueOnError) cmd.StringVar(&opt.Filename, "filename", "", "-") if err := cmd.Parse(os.Args[1:]); err != nil { if err != flag.ErrHelp { cmd.Usage() } os.Exit(1) } opt.Args = cmd.Args() if err := run(opt); err != nil { log.Fatalf("!!%+v", err) } } func run(opt *Option) error { f, err := os.Open(opt.Filename) if err != nil { return err } defer f.Close() var ob model.Article p := &ob return load.LoadAndPrint(f, p) return nil }
おわり
おわり。gistはこちら
future works
おそらくまだまだ先になるがwebAPIを作るときに何に乗っかろうかを調べてみたりした。
- gin https://gist.github.com/podhmo/8f6cac73528a1acaa28c348ab57829d0
- go-chi https://gist.github.com/podhmo/58d8f6d52adfba41e349a529d08210a0
- echo https://gist.github.com/podhmo/6c551832f564bdde306e9dd724e8546c
現状gin,echo,go-chiを調べた結果go-chiが優勢。
追記
より細かなバリエーションは https://github.com/podhmo/egoist/tree/master/examples/e2e/generate-structkit にある。
-
もっと良いライブラリがあれば教えてほしい。捨てられるなら捨てたい。↩
egoistで生成するCLI定義のカスタマイズ方法について
作っていたミニフレームワークの最初のバージョンをリリースしたと言う話という記事を書きました。
とはいえ、この記事だけだと何が何だか分からなかったので、作られたコマンドを拡張していきながらもう少し詳しく説明するような記事を書きました。
関数定義とCLIのコマンドの定義
definitions.pyを覗いてみましょう。このような関数定義があると思います。
@app.define_cli("egoist.generators.clikit:walk") def hello(*, name: str) -> None: """hello message""" from egoist.generators.clikit import runtime, clikit with runtime.generate(clikit): runtime.printf("hello %s\n", name)
一度余分なコードを消してみて心の目で見てみましょう。関数定義のsignature部分だけに集中してみてください。このような形で。
def hello(*, name: str) -> None: """hello message""" ...
python上で、定義された関数を呼び出す場合は以下の様になりますよね。
hello(name="world")
心の目で見てみると、以下のようなCLIでのコマンドの実行も似たようなものだと思えてきませんか?
$ hello --name world
つまり、そういうことです。
definitions.pyの中身
それではdefinitions.pyの中身を真面目に覗いて行きましょう。egoist init
で生成されるdefinitions.pyは以下のようなコードになっています(このコードは現在のバージョンでのことであり、暫定的なものである可能性があります)。
設定(settings)の"rootdir"に"cmd/"がついているので、先程のpython definitions.py generate
で"cmd/hello/main.go"が生成されたというわけです。
(hereの意味やincludeの意味はとりあえず省略して次に進みます。includeは各自が機能を自作するためのhookです。)
definitions.py
from egoist.app import App, SettingsDict settings: SettingsDict = {"rootdir": "cmd/", "here": __file__} app = App(settings) app.include("egoist.directives.define_cli") @app.define_cli("egoist.generators.clikit:walk") def hello(*, name: str) -> None: """hello message""" from egoist.generators.clikit import runtime, clikit with runtime.generate(clikit): runtime.printf("hello %s\n", name) if __name__ == "__main__": app.run()
helloという名前の関数なのでhello/main.goが生成されました。またdocstringの"hello message"は先頭のヘルプメッセージとして使われています。関数のbody部分は見慣れないものがあるかもしれませんが、nameという引数を使ってprintfしているだけですね。
出力する位置を変えたい?
出力する位置を変えたい場合は関数名の __
がファイルパス上の /
に対応してます。いい感じに名前をつけてください。foo__bar__boo
とかそういう形で。
コアの部分はもちろんgoで
このフレームワークは、pythonの定義からgoのコードを生成することを意図してはいますが、goのコードをpythonで書くということは意図していません。これはちょっとした注意点です。
複雑なロジックやコアの機能などはもちろん素直にgoで書いてください。このフレームワークが埋めようとするのは、主に機能と機能の間をつなぐような糊となるような部分です(グルーコード)。
これは、自分で機能を自作するときにも気をつけたほうが良いところかもしれません。
各自がansibleなどのクックブックのように拡張していくことを意図しています。フリーライドせずに自分たちの力で育てていく必要があります。これは、自分たちの内部のコードと上手くつなげるためのコード生成で、かつ、自分たちの内部のコードのことは自分たちしか知らないはずなので、自然とそうなります
ちなみに、自分の好みの機能を提供することを奨励する実際には形のないフレームワークなので、実質的にはフレームワークフレームワークあるいはツールキットと呼んだほうが良いものかもしれません。
生成されたgoコード
pythonから生成されたgoのコードの一部を抜粋します。まぁそれはそうという感じですね。特筆事項はなさそうです。
一つあるとしたら、python中のname: str
という引数定義が、使われている場所では opt.Name
になっている点でしょうか?
func run(opt *Option) error { fmt.Printf("hello %s\n", opt.Name) return nil }
なんとなく雰囲気がわかったので、少しだけ改変をしていこうと思います。
コマンドラインオプションを追加してみる
コマンドラインオプションを追加してみましょう。関数定義のsignatureを使っているのでなんとなくやるべき事はわかりそうですね。definitions.pyのhello()
の引数部分を変えてみましょう。
app.include("egoist.directives.define_cli") @app.define_cli("egoist.generators.clikit:walk") -def hello(*, name: str) -> None: +def hello(*, name: str, age: int = 20, who: str = "foo") -> None: """hello message""" from egoist.generators.clikit import runtime, clikit with runtime.generate(clikit): - runtime.printf("hello %s\n", name) + runtime.printf("%s(%d): hello %s\n", who, age, name) if __name__ == "__main__":
誰が言ったかわかるようにwhoとageを追加してみました。デフォルト値もある程度は気にしてくれます。それでは生成し直してみましょう。ちなみに、実行後に再度実行してみたときにはno changesと表示されます。このときmtimeも更新されません。
$ python definitions.py generate [F] update cmd/hello/main.go # 再実行 $ python definitions.py generate [F] no change cmd/hello/main.go
生成されたコマンドを実行してみます。このように何度も何度も定義部分を変えての生成を繰り返す点がyomanやcookiecutterのようなscaffoldとは違う点です。個人的にはこのあたりの挙動はインフラのデプロイに近いものなのではないか?というような感覚でいたりします。
実際に使ってみましょう。
$ go run cmd/hello/main.go --name="world" foo(20): hello world $ go run cmd/hello/main.go --who=F --age=90 --name="M" F(90): hello M
なるほどと言う感じですね。ヘルプにも-age
と-who
が現れる様になりました。
$ go run cmd/hello/main.go --help hello - hello message Usage: -age int - (default 20) -name string - -who string - (default "foo") exit status 1
ヘルプメッセージの追加
ちょっとだけ寂しいのでヘルプメッセージも追加してみましょう。ここはちょっとだけ複雑ですが、runtimeモジュールを利用します。runtime.get_cli_options()
でCLIのオプション部分つまりフラグの部分の値が取れるのでそこにhelpを追加してみてください。このようなdiffになります。
def hello(*, name: str, age: int = 20, who: str = "foo") -> None: """hello message""" from egoist.generators.clikit import runtime, clikit + options = runtime.get_cli_options() + options.name.help = "the name of target person" + options.age.help = "age of subject" + options.who.help = "name of subject" + with runtime.generate(clikit): runtime.printf("%s(%d): hello %s\n", who, age, name)
生成後のヘルプメッセージを見てみます
$ python definitions.py generate [F] update cmd/hello/main.go $ go run cmd/hello/main.go --help hello - hello message Usage: -age int age of subject (default 20) -name string the name of target person -who string name of subject (default "foo") exit status 1
良い感じですね。
go側で定義した関数とつなぐ
ここまできてようやく本題です。egoistの本来の趣旨は機能と機能をつなぐ糊です。pythonで定義した部分をCLIとして露出させてもあんまり嬉しくないですね。既存のgoのコードをCLIとして扱うという方が理に叶っていそうです。
internal/helloというような形でHello部分をパッケージ化してみましょう。そしてその関数を呼ぶ形に変えていきましょう。
internal/hello パッケージ
ここは特に特別なこともないのでサクッと作ってみます。
(今回はテキトーに書いている部分ですが、真面目に利用するときには例えばclean architectureのusecaseの実行部分が使われると思います)
$ go mod init m go: creating new go.mod: module m $ mkdir -p internal/hello $ e internal/hello/hello.go # エディタで編集
internal/hello/hello.go
package hello import "fmt" // Hello ... func Hello(name string, age int, who string) { fmt.Printf("%s(%d): hello %s\n", who, age, name) }
はい。go.modでのこのパッケージのトップレベルのパッケージ名は"m"なので"m/internal/hello"がパッケージパスです。
definitions.py
作ったhelloパッケージを利用するようにdefinitions.pyを書き換えましょう。runtime.generate()
はcontext managerになっていて、内部でコードの出力に利用する中間表現を露出してくれます。prestringのModuleというオブジェクトなのですが、詳しい話は省略します。
とりあえず使い方は以下だけ覚えておいてください。
- m.import_() -- パッケージのimport
- m.stmt() -- コードを一行印字
def hello(*, name: str, age: int = 20, who: str = "foo") -> None: options.age.help = "age of subject" options.who.help = "name of subject" - with runtime.generate(clikit): - runtime.printf("%s(%d): hello %s\n", who, age, name) + with runtime.generate(clikit) as m: + hello_pkg = m.import_("m/internal/hello") + m.stmt(hello_pkg.Hello(name, age, who)) # m.stmtを忘れずに
m.import_("m/internal/hello")
で返ってきたオブジェクトを利用すると、あたかもimport対象のパッケージで公開されている関数を呼んだかのように使えます(対応している構文は限定的です)。スタブなどを生成しているわけではないので、型の恩恵は受けられません。
生成してみましょう。
$ python definitions.py generate [F] update cmd/hello/main.go
go側のdiffは以下の様になります。importも追加されているのが嬉しい点です。
import ( "fmt" "os" "log" + "m/internal/hello" ) // this file is generated by egoist.generators.clikit @@ -44,6 +45,6 @@ func main() { } func run(opt *Option) error { - fmt.Printf("%s(%d): hello %s\n", opt.Who, opt.Age, opt.Name) + hello.Hello(opt.Name, opt.Age, opt.Who) return nil }
実行もしてみましょう。動いてますね。
$ go run cmd/hello/main.go --name="world" foo(20): hello world
疲れたので今日はこの辺でおしまいにします。本当はまだまだ語りたい以下の様な機能があります。
まとめ
長くなってきてしまいましたが、この辺でまとめを。
- 以前から作っていたgoのみにフレームワークのプロトタイプができた
- 今回はCLIの生成が主
- 何度も実行されるコード生成がscaffoldとのちがい
- 機能と機能の糊となるコードこそを生成したい
- go側のパッケージをimportして呼び出す事ができた
gist