ryota21silvaの技術ブログ

Funna(ふんな)の技術ブログ

これまで学んだ技術の備忘録。未来の自分が救われることを信じて

selializerを使ったAPIの作成と、リクエストスペックについて

jsonのserializerとは?

jsonを生成する仕組みのこと。
この生成したJSONのデータをレスポンスとして返す。

ActiveRecordは ActiveModel::Serialization を include している。
なので、そのまま以下のような記述もできる。

module Api
  module V1
    class ArticlesController < BaseController
      def index
        articles = Article.all
        render json: articles
      end
    end
  end
end

PostmanでAPIを叩くとJSONレスポンスが返ってきていることが分かる。

[今回の要件]

fast_jsonapiというserializerを使って、記事一覧のjson responseを返す。
fast_jsonapi/readme

Articleモデル。Userとは一対多の関係。

class Article < ApplicationRecord
  belongs_to :user
  has_many :comments, dependent: :destroy

  has_one_attached :eye_catch
  enum status: { draft: 0, in_review: 10, published: 20, archived: 30 }
end

Articleモデルのシリアライザーを作る

$ be rails g serializer Article title contents status                                    
Running via Spring preloader in process 21798
      create  app/serializers/article_serializer.rb

リアライザーの設定

(app/serializers/article_serializer.rb)
class ArticleSerializer
  include FastJsonapi::ObjectSerializer
  attributes :title, :contents, :status
  belongs_to :user
end

APIのコントローラ
Articleのデータから、jsonシリアライズする。
コメントアウトしているが、to_jsonというメソッドを使うのもあり。

(app/controllers/api/v1/articles_controller.rb)
# frozen_string_literal: true

module Api
  module V1
    class ArticlesController < BaseController
      def index
        articles = Article.all
        json_string = ArticleSerializer.new(articles).serialized_json
        # json_string = ArticleSerializer.new(articles).serializable_hash.to_json
        render json: json_string
      end
    end
  end
end

PostmanでAPIを叩いてみると、ちゃんと記事一覧のJSONレスポンスが取れた!

{
    "data": [
        {
            "id": "1",
            "type": "article",
            "attributes": {
                "title": "title-1",
                "contents": "contents-1",
                "status": "draft"
            },
            "relationships": {
                "user": {
                    "data": {
                        "id": "1",
                        "type": "user"
                    }
                }
            }
        },
        {
            "id": "2",
            "type": "article",
            "attributes": {
                "title": "title-2",
                "contents": "contents-2",
                "status": "draft"
            },
            "relationships": {
                "user": {
                    "data": {
                        "id": "2",
                        "type": "user"
                    }
                }
            }
        },
...
......
........

APIのリクエストスペック

  • HTTP 動詞に対応するメソッド(get 、post 、delete 、 patch)を使う。
  • HTTPのレスポンスのステータスコードと、実際のデータを含んだレスポンスボディを返せばよい。

以下【Rails】APIテストの書き方より、ステータスコードの定義を参照。

200: OK - リクエストは成功し、レスポンスとともに要求に応じた情報が返される。
401: Unauthorized - 認証失敗。認証が必要である。
403: Forbidden - 禁止されている。リソースにアクセスすることを拒否された。
404: Not Found - 未検出。リソースが見つからなかった。

テストコード

(spec/requests/api/v1/articles_spec.rb)
require 'rails_helper'

RSpec.describe "Api::V1::Articles", type: :request do
  describe "GET /api/v1/articles" do
    it "掲示板一覧を返す" do
      user = create(:user)
      create_list(:article, 10, user: user)
      get api_v1_articles_path, headers: {
          'CONTENT_TYPE': 'application/json',
          'ACCEPT': 'application/json'
        }
      # JSON.parse(body)でもいけたけど、response.bodyにしておく
      json = JSON.parse(response.body)
      expect(response).to have_http_status(200)
      expect(json["data"].count).to eq(10)
    end
  end
end
JSON.parseについて

与えられた JSON 形式の文字列を Ruby オブジェクトに変換して返してくれる。
JSON形式の文字列からRubyのHashへ変換することで、APIで取得したJSONデータをRubyで使えるようになるって感じ。

JSON.parse(response.body)["data"]の結果

JSON.parse(response.body)["data"]
=> [{"id"=>"1", "type"=>"article", "attributes"=>{"title"=>"title-1", "contents"=>"contents-1", "status"=>"draft"}, "relationships"=>{"user"=>{"data"=>{"id"=>"1", "type"=>"user"}}}},
 {"id"=>"2", "type"=>"article", "attributes"=>{"title"=>"title-2", "contents"=>"contents-2", "status"=>"draft"}, "relationships"=>{"user"=>{"data"=>{"id"=>"2", "type"=>"user"}}}},
 {"id"=>"3", "type"=>"article", "attributes"=>{"title"=>"title-3", "contents"=>"contents-3", "status"=>"draft"}, "relationships"=>{"user"=>{"data"=>{"id"=>"3", "type"=>"user"}}}},
 {"id"=>"4", "type"=>"article", "attributes"=>{"title"=>"title-4", "contents"=>"contents-4", "status"=>"draft"}, "relationships"=>{"user"=>{"data"=>{"id"=>"4", "type"=>"user"}}}},
 {"id"=>"5", "type"=>"article", "attributes"=>{"title"=>"title-5", "contents"=>"contents-5", "status"=>"draft"}, "relationships"=>{"user"=>{"data"=>{"id"=>"5", "type"=>"user"}}}},
 {"id"=>"6", "type"=>"article", "attributes"=>{"title"=>"title-6", "contents"=>"contents-6", "status"=>"draft"}, "relationships"=>{"user"=>{"data"=>{"id"=>"6", "type"=>"user"}}}},
 {"id"=>"7", "type"=>"article", "attributes"=>{"title"=>"title-7", "contents"=>"contents-7", "status"=>"draft"}, "relationships"=>{"user"=>{"data"=>{"id"=>"7", "type"=>"user"}}}},
 {"id"=>"8", "type"=>"article", "attributes"=>{"title"=>"title-8", "contents"=>"contents-8", "status"=>"draft"}, "relationships"=>{"user"=>{"data"=>{"id"=>"8", "type"=>"user"}}}},
 {"id"=>"9", "type"=>"article", "attributes"=>{"title"=>"title-9", "contents"=>"contents-9", "status"=>"draft"}, "relationships"=>{"user"=>{"data"=>{"id"=>"9", "type"=>"user"}}}},
 {"id"=>"10", "type"=>"article", "attributes"=>{"title"=>"title-10", "contents"=>"contents-10", "status"=>"draft"}, "relationships"=>{"user"=>{"data"=>{"id"=>"10", "type"=>"user"}}}}]
リクエストヘッダーをセットしておく。

これはヘッダ情報がないとテストが通らないことがあるため。ちょっと勉強不足ではある。

get api_v1_articles_path, headers: {
      'CONTENT_TYPE': 'application/json',
      'ACCEPT': 'application/json'
    }
headers情報をlet使って格納しておくのも良い
let(:headers) do
  { 'Content-Type' => 'application/json',
    'Accept' => 'application/json'
  }
end
数字を変数(numとか)に入れておくのもアリ
RSpec.describe 'Api::V1::Articles', type: :request do
  describe 'GET /articles' do
    let(:article_num) { 10 }

    before do
      user = create(:user)
      create_list(:article, article_num, user: user)
    end

    it 'returns fincle articles in json format' do
      get api_v1_articles_path, headers: { CONTENT_TYPE: 'application/json', ACCEPT: 'application/json', }

      expect(JSON.parse(body)['data'].count).to eq(article_num)
      expect(response).to be_successful
      expect(response).to have_http_status(:ok)
    end
  end
end