Slide 1

Slide 1 text

confidential WebAPIのバリデーションを、型の力でいい感じにする
 Web API LT会 - vol.3 takuya kikuchi / Showcase Gig

Slide 2

Slide 2 text

confidential ©Showcase Gig 自己紹介
 ● takuya kikuchi ● twitter: @_pochi ● Engineer Group Manager @ Showcase Gig ● モバイルオーダープラットフォームを作っています ● たまに実店舗も作ります

Slide 3

Slide 3 text

confidential ©Showcase Gig APIが受け取るパラメータのバリデーション
 ちゃんとできてますか? SDK, OpenAPI, gRPC, etc… 「そんな入力予期してなかった・・・」 「そんな使い方しないで・・・」

Slide 4

Slide 4 text

confidential ©Showcase Gig 素直なアプローチ紹介
 gRPCの例 protoc-gen-validate を使ってバリデータの自動生成ができるよ 🐔 参考: 【Go】gRPCのリクエストバリデータを自動生成する https://note.com/scg_tech/n/nb12a33bfd391 import "github.com/envoyproxy/protoc-gen-validate/validate/validate.proto"; ~~~~~~~~~~~ service TestServer { rpc Test(TestMessage) returns (Result) {} } ~~~~~~~~~~~ message TestMessage { // 0~100間の整数 int32 seisuu = 1 [(validate.rules).int32 = {gte:0, lt: 100}]; // floatで0~1の値 double fudou = 2 [(validate.rules).double = {gte: 0, lte: 1}]; // アルファベットと数値で、5〜30文字のrepeated repeated string mojiretsu = 3 [(validate.rules).repeated.items.string = {pattern: "^[a-z0-9]{5,30}$", min_len: 5, max_len:30}]; // RFC 1034で解釈可能なメールアドレス string mail_address = 4 [(validate.rules).string.email = true]; }

Slide 5

Slide 5 text

confidential ©Showcase Gig 型の話をします


Slide 6

Slide 6 text

confidential ©Showcase Gig 型はいいぞ
 ● 静的型付け言語では、コンパイル時に型チェックをしてくれる ● 型の明らかな渡し間違いはすぐ気づくことができる import "fmt" func main() { var intValue int = 12345 printString (intValue) } func printString (str string) { fmt.Printf("%s\n", str) } import "fmt" func main() { var str string = "文字列だよ" printString (str) } func printString (str string) { fmt.Printf("%s\n", str) } コンパイルOK コンパイルエラー

Slide 7

Slide 7 text

confidential ©Showcase Gig しかし基本データ型には限界がある
 ● メールアドレスのみを受け入れたいのだけど、コンパイラさん気づいてくれない import "fmt" func main() { var mailAddress string = "メアドではないよ" printMailAddress (mailAddress) } func printMailAddress (mailAddress string) { fmt.Printf("%s\n", mailAddress) } import "fmt" func main() { var mailAddress string = "hoge@example.com" printMailAddress (mailAddress) } func printMailAddress (mailAddress string) { fmt.Printf("%s\n", mailAddress) } コンパイルOK 🤗 コンパイルOK 😢

Slide 8

Slide 8 text

confidential ©Showcase Gig しかし基本データ型には限界がある
 ● そもそもユーザーから任意の値が入力されるものはどうしようもない import "fmt" func main() { var str string // ユーザー入力を受け取る _, err := fmt.Scan(&str) if err != nil { // 読み取り失敗 return } printMailAddress(str) } コンパイルOK。ただしメアド以外も printできてしまう。

Slide 9

Slide 9 text

confidential ©Showcase Gig さてどうする


Slide 10

Slide 10 text

confidential ©Showcase Gig 「メールアドレス」型を作ろう
 ● 正しいメアド入力以外はオブジェクトを生成できないようにする type MailAddress string func NewMailAddress (mailAddress string) (MailAddress , error) { if !isValidMailAddress (mailAddress) { return "", errors.New("メアドではないので NG") } return MailAddress (mailAddress) , nil } func printMailAddress(mailAddress MailAddress) { fmt.Printf("%s\n", mailAddress) } 型とコンストラクタを定義。コンストラクタで、「正しいメールアド レスかどうか」をチェックする printMailAddressメソッドの引数も、その型を受け取るようにする

Slide 11

Slide 11 text

confidential ©Showcase Gig 「メールアドレス」型を活用する
 ● コンパイル時チェックはできないが、「メールアドレスじゃない文字列が渡された場合」のエラーハンドリング実装が強制さ れる ○ なので、「うっかりメアドじゃない文字列が渡されてクラッシュ!」みたいなことが回避できる func main() { var str string // ユーザー入力を受け取る _, err := fmt.Scan(&str) if err != nil { // 読み取り失敗 return } // 入力された文字列から、 MailAddressオブジェクト生成 mailAddress, err := NewMailAddress (str) if err != nil { // メールアドレスではない文字列が渡された return } // MailAddress を出力する printMailAddress (mailAddress) }

Slide 12

Slide 12 text

confidential ©Showcase Gig 本題。WebAPIの話


Slide 13

Slide 13 text

confidential ©Showcase Gig メールアドレスを登録するWebAPI
 ● メールアドレスを受け取ってDBに記録するエンドポイントのコントローラをイメージしてください ● MailAddressRepositoryは単にSQLを呼ぶだけの実装だと思ってください // メールアドレスを登録するエンドポイントの実装 func updateMailAddressController (userID int, mailAddress string) error { // DBアクセス用のリポジトリ取得 repository := newMailAddressRepository () // DBに書き込み err := repository. Update(userID, mailAddress) if err != nil { // DB書き込みエラー return errors.New("internal error" ) } // 正常終了 return nil } type MailAddressRepository interface { Update(userID int, mailAddress string) error }

Slide 14

Slide 14 text

confidential ©Showcase Gig 問題点
 ● mailAddress のバリデーションをしていない。 ○ 渡されてくるmailAddress次第でいろんなエラーが起きてしまう ■ メアドじゃなくても登録できる(それはダメ!) ■ 文字列が長すぎたらDBへのクエリでエラーが起きて internal error (カッコ悪い!) ● メアドじゃない文字列が渡されたらクライアントエラーにしてあげたい // メールアドレスを登録するエンドポイントの実装 func updateMailAddressController (userID int, mailAddress string) error { // DBアクセス用のリポジトリ取得 repository := newMailAddressRepository () // DBに書き込み err := repository. Update(userID, mailAddress) if err != nil { // DB書き込みエラー return errors.New("internal error" ) } // 正常終了 return nil }

Slide 15

Slide 15 text

confidential ©Showcase Gig 先程作ったメールアドレス型を使います
 ● MailAddressRepositoryの引数を、string から MailAddress型に変更 type MailAddressRepository interface { Update(userID int, mailAddress MailAddress) error } type MailAddressRepository interface { Update(userID int, mailAddress string) error }

Slide 16

Slide 16 text

confidential ©Showcase Gig するとどうなるか
 ● 元のコードに戻ります。 ● mailAddressが string なのでコンパイルエラーになります // メールアドレスを登録するエンドポイントの実装 func updateMailAddressController (userID int, mailAddress string) error { // DBアクセス用のリポジトリ repository := newMailAddressRepository () // DBに書き込み err := repository. Update(userID, mailAddress) if err != nil { // DB書き込みエラー return errors.New("internal error" ) } // 正常終了 return nil }

Slide 17

Slide 17 text

confidential ©Showcase Gig あとはコンパイルエラーを解消する
 ● コンパイルエラーを解消するために、引数で渡された文字列をMailAddress型に変換します ● その際にエラーハンドリングがごく自然に実装されます // メールアドレスを登録するエンドポイントの実装 func updateMailAddressController (userID int, mailAddressStr string) error { // DBアクセス用のリポジトリ repository := newMailAddressRepository () mailAddress, err := NewMailAddress(mailAddressStr) if err != nil { // 渡された文字列がメールアドレスではない return errors.New("client error") } // DBに書き込み err = repository. Update(userID, mailAddress) if err != nil { // DB書き込みエラー return errors.New("internal error" ) } // 正常終了 return nil }

Slide 18

Slide 18 text

confidential ©Showcase Gig まとめ
 リクエストパラメータをバリデーションしたいあなたへ → バリデーションしたいということは、その値には何らかのルールがある → そのルールを表現する、専用の型を用意してみよう → 専用の型の利用を強制しよう → すると、パラメータのバリデーションは自然に実装される

Slide 19

Slide 19 text

confidential ©Showcase Gig おしまい


Slide 20

Slide 20 text

confidential ©Showcase Gig 宣伝
 ● APIが好きな人 ● 型が好きな人 ● Goが好きな人 ● gRPCが好きな人 ● エンジニアリングが好きな人 ● POSが好きな人 ● 次世代店舗を作りたい人 https://www.showcase-gig.com/recruit/engineer/