こんにちは、TIGの村田です。春の入門祭り連載 2日目の本記事では、フューチャー製OSSであるreguerrに入門しようと思います。
入門の途中でエラーと遭遇したため、途中からエラーハンドリング編に突入しています。入門祭りということで、エラーハンドリングの一例として「こんな風に考えるんだなー」と思いつつ読んでいただければ幸いです。
また、最終的にはエラーハンドリングを元に、OSSへPRを投げています。そういったOSSとの向き合い方を感じて頂くきっかけになれば良いなと思っています。
では本編に入っていきます。
reguerrとは reguerrはエラーハンドリング向けのソースコードを自動生成してくれるGo製のライブラリです。フューチャーの案件でも採用実績があり、体系的なエラー定義とそれに伴うハンドリングが重要となってくるエンタープライズシステムでの利用に足る機能をreguerrは有しています。
入門してみる 下準備 まずはフューチャーのGitHubリポジトリ上reguerr のreadmeに沿ってコマンドをインストールします。
go install github.com/future-architect/reguerr/cmd/reguerr 
以下のようにヘルプコマンドが実行できればインストール成功です。
$ reguerr -h Usage:   reguerr [command ] Available Commands:   generate    generate reguerr code   help         Help about any command    validate    validate input file Flags:   -h, --help    help  for  reguerr Use "reguerr [command] --help"  for  more information about a command . 
自動生成してみる generate コマンドのヘルプを見てみると、-fでインプットファイルを指定すれば良いことが分かります。
$ reguerr generate --help  generate reguerr code Usage:   reguerr generate [flags] Flags:       --defaultErrorLevel string   change default log  level(Trace,Debug,Info,Warn,Error,Fatal)       --defaultStatusCode int      change default status code (default -1)   -f, --file string                input go file   -h, --help                        help  for  generate 
readmeに沿って、以下ファイルをexample.goとして作成します。
package  exampleimport  (	"gitlab.com/future-architect/reguerr"  ) var  (	 	PermissionDeniedErr = reguerr.New("1001" , "permission denied" ).Build() 	 	UpdateConflictErr = reguerr.New("1002" , "other user updated: key=%s" ).Build() 	 	InvalidInputParameterErr = reguerr.New("1003" , "invalid input parameter: %v" ). 		Label(0 ,"payload" , map [string ]interface {}{}). 		Build() ) 
そしてexample.goをインプットファイルにして自動生成を実行。
reguerr generate -f example.go 
example_gen.goとexample_gen.mdの2つのファイルが作成されます。
package  exampleimport  (        "errors"          "github.com/future-architect/reguerr"  ) func  NewPermissionDeniedErr (err error )         return  PermissionDeniedErr.WithError(err) } func  IsPermissionDeniedErr (err error ) bool  {        var  cerr *reguerr.ReguError         if  as := errors.As(err, &cerr); as {                 if  cerr.Code() == PermissionDeniedErr.Code() {                         return  true                  }         }         return  false  } func  NewUpdateConflictErr (err error , arg1 interface {})         return  UpdateConflictErr.WithError(err).WithArgs(arg1) } func  IsUpdateConflictErr (err error ) bool  {        var  cerr *reguerr.ReguError         if  as := errors.As(err, &cerr); as {                 if  cerr.Code() == UpdateConflictErr.Code() {                         return  true                  }         }         return  false  } func  NewInvalidInputParameterErr (err error , payload map [string ]interface {})         return  InvalidInputParameterErr.WithError(err).WithArgs(payload) } func  IsInvalidInputParameterErr (err error ) bool  {        var  cerr *reguerr.ReguError         if  as := errors.As(err, &cerr); as {                 if  cerr.Code() == InvalidInputParameterErr.Code() {                         return  true                  }         }         return  false  } 
インプットファイルで定義されたエラーパターンをもとに、それぞれ以下2種の関数が作成されています。
引数で受け取ったエラーを、定義した任意のエラーへ変換して返してくれる関数 
引数で受け取ったエラーが、定義したエラーとエラー内容が一致しているか判定してくれる関数 
 
mdファイルは以下のような内容になっています。エラーが自動的に表形式で整理されるので、各種ドキュメンテーションの際に活躍してくれそうです。
# Error Code List | CODE |           NAME           | LOGLEVEL | STATUSCODE |           FORMAT            | |------|--------------------------|----------|------------|-----------------------------| | 1001 | PermissionDeniedErr      | Error    |        500 | permission denied           | | 1002 | UpdateConflictErr        | Error    |        500 | other user updated: key=%s  | | 1003 | InvalidInputParameterErr | Error    |        500 | invalid input parameter: %v | 
自動生成の引数をいじってみる generate コマンドの引数には --defaultStatusCode などの可変パラメータが存在していました。次はこちらをいじってみます。
$ reguerr generate -f example.go --defaultStatusCode 300 $ cat  example_gen.md | CODE |           NAME           | LOGLEVEL | STATUSCODE |           FORMAT            | |------|--------------------------|----------|------------|-----------------------------| | 1001 | PermissionDeniedErr      | Error    |        300 | permission denied           | | 1002 | UpdateConflictErr        | Error    |        300 | other user updated: key=%s  | | 1003 | InvalidInputParameterErr | Error    |        300 | invalid input parameter: %v | 
ステータスコードが指定通りに変更されていることが確認できました。
エラーハンドリングしてみる --defaultErrorLevel をいじってデフォルトのエラーレベルを変更しようとしたのですが、エラーが出てしまいました。
$ reguerr generate -f example.go --defaultErrorLevel Info Usage:   reguerr generate [flags] Flags:       --defaultErrorLevel string   change default log  level(Trace,Debug,Info,Warn,Error,Fatal)       --defaultStatusCode int      change default status code (default -1)   -f, --file string                input go file   -h, --help                        help  for  generate unknown error level 
渡している文字列が悪いのか、渡し方が悪いのか、はたまた元のソースコードにバグが存在しているのか。末尾に出ている unknown error level がエラーログなので、ソースコードを追って原因を探ってみます。
リポジトリを漁ってみると、reguerr.goの47行目 に該当のエラー文言がありました。
func  NewLevel (s string ) error ) {	switch  strings.ToLower(s) { 	case  strings.ToLower(Trace.String()): 		return  Trace, nil  	case  strings.ToLower(Debug.String()): 		return  Debug, nil  	case  strings.ToLower(Info.String()): 		return  Info, nil  	case  strings.ToLower(Warn.String()): 		return  Warn, nil  	case  strings.ToLower(Error.String()): 		return  Error, nil  	case  strings.ToLower(Fatal.String()): 		return  Fatal, nil  	default : 		return  Trace, errors.New("unknown error level" )   	} } 
コマンドの実行引数で渡している Info の文字列が NewLevel 関数の引数 s として渡っていくのだろうと思いますが、このswitch文の中でdefaultに突入、該当のエラーが発生しているだろうと推測されます。
この NewLevel 関数自体も呼び元がいるはずなので探ってみると、cmd配下のroot.go内71行目 にて呼び出されていることが確認できました。
var  opts []gen.Optionif  errLevel != ""  {	level, err := reguerr.NewLevel(errLevel + "Level" )   	if  err != nil { 		return  err 	} 	opts = append (opts, gen.DefaultErrorLevel(level)) } if  statusCode != -1  {	opts = append (opts, gen.DefaultStatusCode(statusCode)) } 
ここで --defaultErrorLevel と --defaultStatusCode にて設定された値を処理しているようです。
期待する挙動は、先程のswitch文の中で strings.ToLower(s) の値が strings.ToLower(Info.String()) の値と一致することなのですが、そうなってないようなので何が起きているかもう少し探ってみます。
NewLevel のタイミングで各々の値が実際どうなっているのか確認できるようにログを仕込んでみました。
fmt.Printf("[USER]strings.ToLower(s)=%v\n" , strings.ToLower(s)) fmt.Printf("[USER]strings.ToLower(Info.String())=%v\n" , strings.ToLower(Info.String())) 
これで再度generateを試してみます。
reguerr generate -f example.go --defaultErrorLevel Info [USER]strings.ToLower(s)=infolevel [USER]strings.ToLower(Info.String())=info Usage:   reguerr generate [flags] Flags:       --defaultErrorLevel string   change default log  level(Trace,Debug,Info,Warn,Error,Fatal)       --defaultStatusCode int      change default status code (default -1)   -f, --file string                input go file   -h, --help                        help  for  generate unknown error level 
ログが出ました。まず、コマンド引数として渡している部分は infolevel という文字列になっていました。たしかに NewLevel の呼び元で以下のように呼び出していましたね。
reguerr.NewLevel(errLevel + "Level" ) 
引数で渡されたエラーレベルの文言に Level という文字列を付け加え、それがlowercaseに変換されるのでプログラム上違和感はないです。
ただ、マッチ対象文字列は level という文字列を含まないのでこれが原因と考えられます。NewLevel の呼び出し方を以下のように変えてみます。
level, err := reguerr.NewLevel(errLevel) 
以下コマンドで再度generateを実行。エラーなく終了しました。
reguerr generate -f example.go --defaultErrorLevel Info 
生成されたマークダウンファイルを覗いてみると、LOGLEVEL部が想定通り Info に変わっていることを確認できました。
$ cat  example_gen.md | CODE |           NAME           | LOGLEVEL | STATUSCODE |           FORMAT            | |------|--------------------------|----------|------------|-----------------------------| | 1001 | PermissionDeniedErr      | Info     |        500 | permission denied           | | 1002 | UpdateConflictErr        | Info     |        500 | other user updated: key=%s  | | 1003 | InvalidInputParameterErr | Info     |        500 | invalid input parameter: %v | 
また、goファイル側ではデフォルトのエラーレベルを変更するinit処理が追加されていることを確認できました。
func  init ()         reguerr.DefaultErrorLevel = reguerr.Info } 
OSSにPRを投げてみる 動作確認を元に以下の変更を加え、プルリクエスト を作成しました。
OSSの挙動でおかしいと思われる点があった際に「このOSS使えねえ!」と騒ぐのではなくissueを起票するかPRをあげよとどこかのエラい人から教わったので、私も例に漏れずそのように行動したいと思います。このPRが少しでも世界平和に繋がることを祈っています。
などと言いつつ、執筆時点(2022.04)でこの修正が全体を鑑みた上でベストなのかどうかは判断しきれていないのが正直なところです。ただ、そこはコードオーナーのレビューに任せ、修正案のたたき台としてこのPRが機能するといいなという気持ちでPRをあげることにします。
まとめ さて、今回はフューチャー製OSSであるreguerrに入門しつつ、エラーハンドリングしつつOSSへPRをあげるということに入門してみました。
私のPRがマージされた暁には、本記事で触れているエラーに直面することは無くなるのですが、エラーハンドリングの考え方やOSSとの向き合い方が皆さんの参考になればと思っています。
春の入門祭り連載、次回は戸田さんです。お楽しみに!