外部サービスのリソースをJSONシリアライザブルなActive Modelとして表現する
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