Upgrade to Pro — share decks privately, control downloads, hide ads and more …

WebAPIのバリデーションを、型の力でいい感じにする

takuya kikuchi
September 29, 2023
74

 WebAPIのバリデーションを、型の力でいい感じにする

Web API LT会 - vol.3 のLT資料です #webapilt

takuya kikuchi

September 29, 2023
Tweet

More Decks by takuya kikuchi

Transcript

  1. confidential ©Showcase Gig 自己紹介
 • takuya kikuchi • twitter: @_pochi

    • Engineer Group Manager @ Showcase Gig • モバイルオーダープラットフォームを作っています • たまに実店舗も作ります
  2. 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]; }
  3. 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 コンパイルエラー
  4. 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 = "[email protected]" printMailAddress (mailAddress) } func printMailAddress (mailAddress string) { fmt.Printf("%s\n", mailAddress) } コンパイルOK 🤗 コンパイルOK 😢
  5. confidential ©Showcase Gig しかし基本データ型には限界がある
 • そもそもユーザーから任意の値が入力されるものはどうしようもない import "fmt" func main()

    { var str string // ユーザー入力を受け取る _, err := fmt.Scan(&str) if err != nil { // 読み取り失敗 return } printMailAddress(str) } コンパイルOK。ただしメアド以外も printできてしまう。
  6. 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メソッドの引数も、その型を受け取るようにする
  7. 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) }
  8. 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 }
  9. 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 }
  10. confidential ©Showcase Gig 先程作ったメールアドレス型を使います
 • MailAddressRepositoryの引数を、string から MailAddress型に変更 type MailAddressRepository

    interface { Update(userID int, mailAddress MailAddress) error } type MailAddressRepository interface { Update(userID int, mailAddress string) error }
  11. 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 }
  12. 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 }
  13. confidential ©Showcase Gig 宣伝
 • APIが好きな人 • 型が好きな人 • Goが好きな人

    • gRPCが好きな人 • エンジニアリングが好きな人 • POSが好きな人 • 次世代店舗を作りたい人 https://www.showcase-gig.com/recruit/engineer/