Slide 1

Slide 1 text

森塚 真年(@sanfrecce_osaka) 2023/11/15 GENBA #1 〜RubyとRails開発の現場〜 #GENBA Input object ではじめる 入力値検証

Slide 2

Slide 2 text

森塚 真年 U GitHub: @sanfrecce-osakc U Twitter(X): @sanfrecce_osakc U Qiita: @sanfrecce_osakc U from: 大阪府枚方2 U 趣味: コミュニティ・勉強@ U 好きな Ruby の機能: パターンマッ3 U 最近の興味: orthoses-rail8 U 所属: 株式会社エンペイ 自己紹介

Slide 3

Slide 3 text

概要 1 サービb 1 集金業務支援サービス enpa6 1 口座振替業務支援サービス koufuri) 1 モバイル決済アプリ enpayウォレッe 1 技 1 バックエンド: Ruby / Rails / Go / HasurA 1 フロントエンド: Flutter / React / Vue 株式会社エンペイ https://www.enpay.co.jp/

Slide 4

Slide 4 text

はじめに

Slide 5

Slide 5 text

最近の予定 10月 0 1週目: 会社のイベントで東# 0 3週目: 大江戸Ruby会議で東# 0 4週目: Kaigi on Railsで東京 11月 0 11/15: このイベントで東# 0 11/20: 会社のイベントで東京 12月/1月 0 12/15 RubyConf Taiwan 202— 0 1/13 福岡Rubyフェスタ202i 0 1/20 BuriKaigi

Slide 6

Slide 6 text

毎週のように 遠征している謎

Slide 7

Slide 7 text

本編

Slide 8

Slide 8 text

こんなこと ありませんか?

Slide 9

Slide 9 text

呼び出し側の実装ミス e.g. https://example.com/undefined

Slide 10

Slide 10 text

あちこちで実装される クエリパラメータの 変換ロジック e.g. ids.split(‘,’).map(&:to_i)

Slide 11

Slide 11 text

Fat Controller & Model e.g. rubocop:disable Metrics/ClassLength

Slide 12

Slide 12 text

そこで

Slide 13

Slide 13 text

https://speakerdeck.com/daikimiura/upgrow?slide=23 参考資料 – Upgrow: Railsアプリの保守性を高めるためのShopify のアプローチ / Upgroƒ – Input object を使ってリクエストパラメータを検証す y – ぼくがかんがえたさいきょうの Input object Input object パターン

Slide 14

Slide 14 text

Text enpay での場合 方針 g 属性ごとに Input object 作R g API として想定外のもののみバリデーションの対& g クエリを発行するような処理は行わない ディレクトリ構成 g model† g input† g boolean_input.r• g ids_input.r• g keywords_input.r• g type† g integer_array_type.r• g string_array_type.rb

Slide 15

Slide 15 text

enpay での利用例

Slide 16

Slide 16 text

Controller(action) class < def => : end before_action index_inputs [{ ids }] HogeController ApplicationController :validate_params index # ...

Slide 17

Slide 17 text

Controller(private methods) private def unless & end def end def ||= :: new : end head , index_inputs.all?( ) [ids_input] . (ids params[ ]) validate_params index_inputs ids_input :bad_request :valid? @ids_input :ids Inputs IdsInput

Slide 18

Slide 18 text

Input object module class include :: new : def ** super ** end def unless & end def : end end end attribute , . , default { } validate ( args) ( args.compact_blank) errors.add( , ) ids.all?( ) ( ) { ids } Inputs IdsInput Types IntegerArrayType -> Input :ids :should_be_positive_all :base :positive? '' ' ' initialize should_be_positive_all deconstruct_keys 有効なIDを指定してください _

Slide 19

Slide 19 text

Input object(Concern) module extend :: do include :: include :: end end included Input ActiveSupport ActiveModel ActiveModel Concern Model Attributes

Slide 20

Slide 20 text

カスタムタイプ module class < :: :: & end end end cast_value(value) value.split( ).map( ) Types IntegerArrayType ActiveModel Type Value def [[:blank:] :to_i /, ]*/

Slide 21

Slide 21 text

しばらく運用してみて h 更新系APIでも利用され始めg h 導入当初の対象は 参照系API のみだっg h private で大量に生える xxx_input メソッ h 複数のアクションで利用され始めg h アクションごとの分岐が増えてきた その後 action_name private def case in then in then end def end def end def end validate_params xxx_input yyy_input zzz_input ' ' ' ' index create # ... # ... # ...

Slide 22

Slide 22 text

方針変更(予定) 方針 ˜ 属性ごとに Input object 作ƒ ˜ => Controller の action ごとに Input object 作T ˜ => 共通のインターフェースを提8 ˜ API として想定外のもののみバリデーションの対f ˜ クエリを発行するような処理は行わな ˜ generator の提供(未実装) ディレクトリ構成 ˜ model{ ˜ input{ ˜ controller{ ˜ xxx_controllev ˜ index_input.r„ ˜ create_input.r„ ˜ xxx_input.r„ ˜ type{ ˜ xxx_type.rb

Slide 23

Slide 23 text

変更後の実装

Slide 24

Slide 24 text

共通インターフェース module def #{ } end def #{ } #{ } end end send( .to_sym) .constantize Inputs input input_class " " " " action_name _input ::Inputs::Controllers:: .class :: action_name.classify Input self

Slide 25

Slide 25 text

基底Controller class < include end ApplicationController ::ActionController::API Inputs

Slide 26

Slide 26 text

Controller(action) class < def => : end def => : end before_action input { ids } input { hoges } HogeController ApplicationController :validate_params index create # ...

Slide 27

Slide 27 text

Controller(private methods) private def unless end def ||= new end def end head , input.valid? input_class. (create_params) validate_params create_input create_params :bad_request @create_input # ... # Strong Parameter の処理

Slide 28

Slide 28 text

Input object module module module class include :: :: new :: :: new :: :: : : : in : def ** super ** end def : : : end end end end end attribute , attribute , . attribute , . ( , array ), default { [] } validates , inclusion { : } validates , presence ( args) ( args.compact_blank) ( ) { name , status , hoges } Inputs Controllers HogeController CreateInput Types SymbolType Types InputType Inputs -> Input :name :string :status :fugas FugaInput true :status :fugas true %[ ] todo doing done initialize deconstruct_keys _

Slide 29

Slide 29 text

ご清聴 ありがとうございました