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

isugata 〜ISUCONベンチマーカーのためのカッコいいHTTPレスポンスバリデーターを作る

logica
December 23, 2023

isugata 〜ISUCONベンチマーカーのためのカッコいいHTTPレスポンスバリデーターを作る

ISUCONベンチマーカーを実装する際は、リクエストを送った後、返って来たレスポンスが題意に沿っているかバリデーションする必要があります。ステータスコードからJSONボディまで、レスポンスのありとあらゆる部分がバリデーション対象です。
愚直に要素ごとに列挙して書けばいいじゃないかって?カッコよくないしめんどくさい!Functional Option Pattern使ってカッコよくライブラリ化しようぜ!っていうのをやってみましたというお話です。
https://github.com/logica0419/isugata
https://pkg.go.dev/github.com/logica0419/isugata

logica

December 23, 2023
Tweet

More Decks by logica

Other Decks in Technology

Transcript

  1. 軽く自己紹介 logicaです traQ(traPで使われてるメッセージングサービス)の バックエンドメンテナンスリーダーをしてます ISUCON歴 ISUCON11: 予選敗退 ISUCON12: 学生枠で決勝進出、13位 ISUCON13:

    所用で残念ながら欠席 「作問運営になりたい!」と公言している少数民族です ISUCON夏祭りでオリジナルISUCON問題を公開しました
  2. HTTPレスポンスのバリデーション Goの *http.Response の、よく確認されるとこだけ抜き出す type Response struct { Status string

    StatusCode int Header Header Body io.ReadCloser ContentLength int64 } …意外と愚直にやれるのでは?
  3. 愚直なバリデーション 例えばこんな風にできそう var res *http.Response if res.StatusCode != 200 {

    return errors.New("status code is wrong") } var body User _ = json.NewDecoder(res.Body).Decode(&body) if body.ID != 1 { return errors.New("body is wrong") } 大量に書くとなるとちょっとめんどくさいかも 共通のロジックをいろんなとこに書くのも微妙
  4. ユーティリティー化されたバリデーション ちょっとユーティリティー化してみる func ValidateStatusCode(res *http.Response, code int) error { if

    res.StatusCode != code { return errors.New("status code is wrong") } return nil } var res *http.Response if err := ValidateStatusCode(res, 200); err != nil { return err } ちょっとスマートに書けるようになった
  5. ユーティリティー化されたバリデーション レスポンスのありとあらゆる部分がバリデーション対象なので、 毎回以下のようになる err := ValidateStatusCode(res, 200) err := ValidateContentType(res,

    "application/json") var body User _ = json.NewDecoder(res.Body).Decode(&body) if body.ID != 1 { ~~ } 毎回res渡すのめんどくさいし危ない resが途中でどうなるか保証がない Bodyに対するバリデーションは共通化しにくい Bodyに対する共通処理がどこかで抜けるかも
  6. Functional Option Pattern type Server struct { // 色んなフィールド }

    type Option func(*Server) error func NewServer(opts ...Option) (*Server, error) { s := &Server{} for _, opt := range opts { if err := opt(s); err != nil { return nil, err } } return s, nil }
  7. 簡単に紹介 基幹になるのは以下二つ type ValidateOpt func(*http.Response) error func Validate(res *http.Response, opts

    ...ValidateOpt) error しっかりFunctional Option Patternが活かされている resを全てのoptsに通すことでバリデーションを行う 自分でValidateOpt型の関数を作ることで、 オリジナルのバリデーションを行うことも可能
  8. メタデータ系 func WithStatusCode(code int) ValidateOpt { return func(res *http.Response) error

    { // 割愛 } } func WithContentType(contentType string) ValidateOpt { return func(res *http.Response) error { // 割愛 } } ValidateOpt を生成する関数を定義することで、任意の値を照合する
  9. JSON Body系 type JSONValidateOpt[V any] func(body V) error func WithJSONValidation[V

    any]( opts ...JSONValidateOpt[V] ) ValidateOpt { return func(res *http.Response) error { // デコードとoptsの実行 } } ジェネリクスを使って、様々な構造体のバリデーションを まとめることに成功 Array(配列)型は、長さなどのバリデーションがしたかったため 別に JSONArrayValidateOpt 型を用意(中身はほぼ一緒)
  10. どうだ!カッコいいだろ!!! err := isugata.Validate(res, isugata.WithStatusCode(http.StatusOK), isugata.WithContentType("application/json"), isugata.WithJSONArrayValidation( isugata.JSONArrayLengthEquals[user](2), isugata.JSONArrayValidateOrder( func(u

    user) int { return u.ID }, isugata.Asc, ), isugata.JSONArrayValidateEach( func(body user) error { if body.Name != fmt.Sprintf("test%d", body.ID) { return errors.New("body is wrong") } return nil }, ), ), )