action_extractor gem

action_extractorという、Railsでアクションへの入力値を表明するためのGemをつくって試してみている。

使い方

例えば、IDを元にリレーショナルデータベースから記事を1件取得して、その本文とタイトルを更新する、というアクションがあるとする。

def update
  article = Article.find(params[:id])
  arttcle.update!(body: params[:body], title: params[:title])
  redirect_to article
end

今回つくったGemを導入すると、これが次のように書き換えられる。

extract(
  article_id: {
    from: :path,
    schema: {
      type: 'integer'
    }
  },
  body: {
    from: :form_data,
    schema: {
      type: 'string',
    }
  },
  title: {
    from: :form_data,
    schema: {
      type: 'string',
    }
  }
).on \
def update(article_id:, body:, title:)
  article = Article.find(article_id)
  arttcle.update!(body: body, title: title)
  redirect_to article
end

要するに、HTTPリクエストの中から参照したいデータについて、長たらしいDSLでいい感じに宣言しておくと、アクションのキーワード引数としてそれを渡してくれるようになる。これにより、次の利点を期待している:

  • どんな入力値に依存しているのか明確になる
  • 引数が入力値であるという一貫性が生まれる
  • 何らかの情報源から入力値を参照するという処理を共通化できる
  • バリデーション機能を提供できる
  • 機械向けと人間向けにドキュメントを生成できる

背景

引数として #params の値を受け取れるようにするという実装は、例えばRails 3に統合されたMerbのmerb-action-argsというGemがあって、いまよく使われるものだとaction_argsというGemがその手の拡張を提供している。

RESTfulなWebサービスの使用を記述する仕様としては、いま広く使われているものだとOpenAPIがあって、今回つくっているGemもその仕様の恩恵にあずかれる形にしようとしている。このGemを組み込むことで、少なくとも入力値に関する制約が定義されるようになるので、OpenAPIドキュメントを生成する素材として、実装とリンクした情報が使えるようになるはず。

#params についてのバリデーション機能をDSLで提供しつつ、そのメタデータが外部から参照できるような実装として、例えば拙作ながらweak_parametersというGemがある。テスト実行時に得られる情報からWeb APIドキュメントを自動生成するautodocというGemがあるのだけど、このドキュメント生成時に使う情報としてこのメタデータを使うという活用例がある。

また、Railsのコントローラー向けにDSLを提供しつつメタデータを別の形で活用するという話で言えば、garage Gemの構想時にも似たような考えを持ちながら設計していたと思う。

これらはどれも、まだOpenAPIのようなものが台頭していなかった頃につくったものなので、昨今の開発情勢や様々な目的意識を元に今一度何か軽量なものを再設計してみるとどうなるか、という実験でつくってみたのが今回のGemだ。

最近よくactix-webというフレームワークに触れていて、型安全な形でリクエストの情報にアクセスする方法を提供するExtractorsという仕組みがたいへん気に入っており、今回のGemに大いに影響を与えている。actix-webのExtractorsは、FromRequest というTraitを実装した型を用意すればどんな情報でも取得できるようになっていて、面白い使い方だと、アプリケーションの設定情報やコネクションプールなんかも引数として受け渡せるようになっている。このおかげで、単なるリクエスト情報へのアクセサーという位置付けに留まらず、リクエストハンドラーにおける外部依存性を引数という形で表現できるようになっていて、これでDependency Injectionが実現されている。

どんな情報でも取得できるという拡張性は欲しかったので、今回のGemでも自前の抽出器を定義できるようになっている。例えば、URLクエリーから値を抽出するという意味で from: :query というのが使いたければ、次のようなコードを書けば実装できるようにしてみている。即ち、Base を継承して #call を持った Query というクラスをつくれば良い。

class ActionExtractor
  module Extractors
    class Query < Base
      def call
        @controller.request.query_parameters[name.to_sym]
      end
    end
  end
end

出力値の表明

入力が引数で表現されるようになると、出力が戻り値で表現されていないことの違和感がより一層際立ってしまう。ここの対称性を支持するなら、出力についても何らかの仕組みが必要なのではないかと思っている。

例えば、レスポンスも戻り値で表現することに決めて、何らかのインターフェースを満たす値がアクションから返された場合に、それを元に自動的にレスポンスを組み立ててあげる仕組みを用意し、戻り値に使うクラスをDSLで表明しておくことでOpenAPIのメタデータを用意するとか。

Rubyでは型で何かを表明するという仕組みも慣習も比較的乏しく、今回はDSLを使ってもらうようなAPIを用意したが、RBS等で型付けする慣習が広まり、実行時にそれらの情報が使えるようになれば、actix-webのExtractorsのような形でこれらを型で表明することもできるようになるかもしれない。戻り値を型で表現するのは自然な流れだと思うので、そういう世界線になれば出力値によるレスポンス形式の表明という課題も解決されるかもしれない。

Railsのコントローラーへの期待

良い機会なので、現在のRailsのコントローラーについて、自分がいまいち上手く付き合えていないところ、つまり変更を期待しているところについて触れておきたい。

Railsのコントローラーは、HTTPリクエストを受けるたびに、コントローラークラスのインスタンスを1つ生成し、特定のインスタンスメソッド(これをアクションと呼ぶ)を実行するという実装になっている。

Railsのコントローラーでは、受け取ったHTTPリクエストに関する情報がインスタンス変数に格納されている。例えばアクションにおける処理を分割する際、インスタンスメソッドを新しく生やすということが行われがちである。こうすると、引数を介して持ち回らなくとも、HTTPリクエストに関する情報にアクセスするようなコードを簡単に分割できる。こういう設計だと、やりたいことを書き下しやすい一方、「あるアクションがHTTPリクエストのどんな情報に依存しているのか」を調べ上げるのが難しくなってしまう。この問題が、今回のGemで解決したいと思っていた問題の1つ。

また、Railsにおいては、1つのコントローラークラスに1つのアクションを定義するという形ではなく、同じモデルに関心のある幾つかのアクションをまとめて1つのコントローラークラスに定義するという慣習があるため、前述の問題は更に加速する。これについては、慣習を変え、1コントローラー1アクションとするのが妥当な解決策だろうと思う。アクション間での処理の共通化の具合が、共通のモジュールをincludeしている、共通の祖先クラスを持つ、というように継承ツリーで関係性が表現されるようになるというのが良いところだと思う。


あとがき

長々と書いてしまった。

先週末はKaigi on Rails 2021に参加した。オンラインカンファレンスの気軽に参加しやすい利点を享受しながらも、reBakoでの仮想空間を利用した雑談なんかも相まって、カンファレンスの良いところってこういうところだよなという気持ちも思い出せて、非常に楽しめた。このイベントを盛り上げてくれたすべての人々に感謝。

しかし夜に用事があって懇親会に参加できなかったので、最近考えていたことを話す機会が取れず、もやりとしてGemを書いた(?)という感じでした。最高なんで全部これでいきましょうという雰囲気では全くなくて、これどうなんだろうというスタンスで考えているところなので、雑に意見もらえると嬉しい。