RailsでWeb APIを作っていて、外部のサービスからリソースを取得し、DBには保存しないもののレスポンスに含めてクライアントに返したい、ということがありました。このとき次のモジュールが役立ったので紹介します。
モジュールの説明
ActiveModel::Model
はActive Recordとも互換性のあるインタフェースでRails上のモデルとして扱える機能がいろいろ入るモジュールです。ActiveModel::Attributes
はAttributes APIを提供するモジュールです。最後の ActiveModel::Serializers::JSON
は有名なActiveModelSerializers gemと名前は似ているのですが別物の、rails/railsの activemodel
gem内に含まれるモジュールです。
実現方法
POROに ActiveModel::Serializers::JSON
をincludeして render :json
に渡すと、Attributes APIの attribute
で定義した属性からなるJSONをシリアライズできるようになります。これを利用して、次のようなクラスを作りました。
まず、外部サービスから得られるリソースを表すクラスを作り、上述したモジュールをincludeします。
class ExternalServiceResource include ActiveModel::Model include ActiveModel::Attributes include ActiveModel::Serializers::JSON end
次に、ExternalServiceResource
をもとに具体的なリソースを表すクラスを作ります。ここでは外部ブログサービスから記事リソースを取得しているとします。
class Article < ExternalServiceResource attribute :title, :string attribute :content, :string attribute :status, :integer attribute :created_at, :datetime DRAFT = 0 PUBLIC = 1 DELETED = 2 def unavailable? status.in?([DRAFT, DELETED]) end end class Blog # ... def articles raw_response = client.articles raw_articles = JSON.parse(raw_response)['articles'] raw_articles .map { |raw_article| Article.new(raw_article) } # 生JSONをモデルオブジェクトへ変換 .reject(&:unavailable?) end private def client # 外部サービスのWeb APIを叩いてJSONを返すクライアント end end
これらを使うと、外部サービスのリソースも含んだJSONを返すエンドポイント自体は、シンプルに実装できます。
class ArticlesController < ActionController::API # articles#index で外部ブログ記事のうちpublicなものを取得できる def index render json: { articles: blog.articles } end private def blog # Blogのインスタンスを返す end end
pros/cons
この方法の利点は、サードパーティgemのActiveModelSerializersを使うのに比べると、Railsのモジュールだけで実現できてシンプルというところや、モデルとして外部リソースを表現できるので、そのリソースに関するロジックをモデルに置くことができるという点が挙げられます。
逆に、欠点として、自前実装になるのでうまくやらないと負債になりそうな点や、リソースの構造がネストするときに、Attributes APIでは ActiveModel::Type.register
を使って自前の型を定義していくと実装がやや面倒という点です。ただし、ネストしているリソースについて型を定義せず、次のように型を指定せずに属性を定義することもできるにはできます。
# Article#author に入れるためのモデル class Author < ExternalServiceResource attribute :name # ... end class Article < ExternalServiceResource attribute :title, :string attribute :content, :string attribute :status, :integer attribute :created_at, :datetime # Authorのオブジェクトを入れるつもりだが、型指定はとくになし attribute :author # ... end