Zeals TECH BLOG

チャットボットでネットにおもてなし革命を起こす、チャットコマース『Zeals』を開発する株式会社Zealsの技術やエンジニア文化について発信します。現在本ブログは更新されておりません。新ブログ: https://medium.com/zeals-tech-blog

fast_jsonapi gemを1.2から1.5へアップデートしたら躓いた点と対応策

f:id:zeals-engineer:20200124000233p:plain

こんにちは、分析基盤を担当しつつ Rails エンジニアのサポートもやっている鍵本です。

本日は Fast JSON API のバージョンアップをした時に突然テストが落ちて困った話をします。

github.com

背景

Zealsは、LINEおよびFacebook Messenger上で動作するチャットボットサービスで、WebのForm体験をチャット上で完結させることができます。この Zeals のアーキテクチャは同じくテックブログの記事『進化し続ける「Zeals」サービスを支えるアーキテクチャについて』でご紹介しているとおり、バックエンドを Ruby on Rails と React + Redux (& TypeScript) で実装しています。

tech.zeals.co.jp

Rails は DB から必要なデータを準備し JSON 形式に変換したものをフロント側の React に渡しています。その JSON 形式に変換する部分に Fast JSON API を用いています。アップデート前はバージョン 1.2 を用いておりテストコードも問題なく通っていたのですが、1.5 に gem のアップデートをしたところ突然テストが通らなくなりました。

テストが落ちる状態の再現

サンプルプログラムを使ってテストが落ちた状態を再現してみます。

属性として size, color を持つ Shirt モデルを作成します。

class Shirt < ApplicationRecord
  enum size: { 'XS': 0, 'S': 1, 'M': 2, 'L': 3, 'XL': 4 }
end

Shirt モデルのためのシリアライザを以下のように作成します。

class ShirtSerializer < ActiveModel::Serializer
  include FastJsonapi::ObjectSerializer

  attributes :size, :color
end

Shirt モデルをシリアライズして JSON を返すコントローラを作成します。

class ShirtController < ApplicationController
  def show
    @shirt = Shirt.find(params[:id])

    shirt_json = ShirtSerializer.new(@shirt).serializable_hash[:data][:attributes]

    render json: shirt_json
  end
end

ShirtController#show は {size: サイズ, color: 色} を返すので、例えば size: 'M' となる Shirt を生成して、その値が正しく得られることをテストするコードを用意します。

require 'rails_helper'

RSpec.describe ShirtController, type: :controller do
  describe '#show' do
    let(:shirt) { create(:shirt, size: 'M', color: 'black') }

    subject { JSON.parse(response.body, symbolize_names: true) }

    example 'response の size が M である' do
      get :show, params: { id: shirt.id }

      expect(subject[:size]).to eq shirt.size
    end
  end
end

fast_jsonapi のバージョンを 1.2 としてテストすると問題なく成功します。

$ bundle exec rspec spec/controllers/shirt_controller_spec.rb
.

Finished in 0.02659 seconds (files took 1.39 seconds to load)
1 example, 0 failures

ここで fast_jsonapi のバージョンを 1.5 にアップデートすると以下のような結果を得ます。

$ bundle exec rspec spec/controllers/shirt_controller_spec.rb
F

Failures:

  1) ShirtController#show response の size が M である
     Failure/Error: shirt_json = ShirtSerializer.new(@shirt).serializable_hash[:data][:attributes]

     NoMethodError:
       undefined method `each' for #<Shirt:0x00007f9478a3f318>
     # ./app/controllers/shirt_controller.rb:5:in `show'
     # ./spec/controllers/shirt_controller_spec.rb:10:in `block (3 levels) in <top (required)>'

Finished in 0.01942 seconds (files took 1.43 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/controllers/shirt_controller_spec.rb:9 # ShirtController#show response の size が M である

エラーの原因

fast_jsonapi の serializable_hash メソッドは ActiveRecord::Relation オブジェクト か否かで処理を分けているのですが、その判定方法が 1.2 以前とそれ以降で変わってしまったことが原因でした。具体的には 1.3 以降では以下のような条件で判定しています。

      resource.respond_to?(:size) && !resource.respond_to?(:each_pair)

我々の Shirt モデルには enum で size という属性を持っているため size メソッドが存在します。そのため ActiveRecord::Relation オブジェクトであると間違って判定され、

@shirt.each do |s|
  ...
end

のような処理が実行され、each メソッドが存在しない というエラーが出たということでした。因みに 1.2 以前では以下のような判定だったので問題なかったというわけです。

      resource.respond_to?(:each) && !resource.respond_to?(:each_pair)

対応方法

テーブルスキーマを変更して列名を size から別のものに変更すればいいのですが、影響範囲が大きいためシリアライズする際にオプションを渡すことで対応することとしました。すなわち、

class ShirtController < ApplicationController
  def show
    @shirt = Shirt.find(params[:id])
    options = { is_collection: false }

    shirt_json = ShirtSerializer.new(@shirt, options).serializable_hash[:data][:attributes]

    render json: shirt_json
  end
end

@shirt が ActiveRecord::Relation オブジェクトでないことがわかっているから採用できる方法です。これで先のテストコードが成功することがわかります。

まとめ

今回は gem の一つである fast_jsonapi のアップデートで躓いた話についてご紹介しました。モデルの属性値として予約語を用いていなければ防げた問題だと思いますが、size という一般用語だったので仕方なかったかも知れません。とはいえ、このあたりは設計時に注意をしないといけない話ですね。

最後に

Zealsでは、発見した課題をワクワクしながら技術で解決するエンジニアを求めています!ご興味がある方は、ぜひ下記募集からご応募ください!

hrmos.co