DEV Community

Hideaki Ishii
Hideaki Ishii

Posted on • Edited on

active_model_serializers with PORO (Plain-Old Ruby Object)

Recently, I worked on a Rails API project.

In the project, I fetched external data and made POROs which were compliant with active_model_serializers with the data.
Then our APIs returned the POROs serialized.

Today, I introduce the development flow a little bit.

Environment

  • Rails 6.0.0.beta3
  • active_model_serializers 0.10.9
  • factory_bot 5.0.2

Directory structure

  • app/models
    • POROs
  • app/serializers
    • Serializers
  • app/controllers
    • Endpoints

Model

As an example, let’s consider Image model which has a URL and size information.

active_model_serializers provides ActiveModelSerializers::Model for POROs like this, which is so easy to use.

If you need to deal with a more complicated case, you would be able to implement and use a model which is compliant with this specification instead.

# app/models/image.rb class Image < ActiveModelSerializers::Model attributes :url, :size end # app/models/image/size.rb class Image::Size < ActiveModelSerializers::Model attributes :width, :height end 
Enter fullscreen mode Exit fullscreen mode

Serializer

# app/serializers/image_serializer.rb class ImageSerializer < ActiveModel::Serializer attributes :url, :type has_one :size end # app/serializers/image/size_serializer.rb class Image::SizeSerializer < ActiveModel::Serializer attributes :width, :height end 
Enter fullscreen mode Exit fullscreen mode

Defining relations like has_one, we can use include option conveniently on endpoints.

For example, render json: image, include: '*' returns JSON including size and render json: image, include: '' returns JSON without size.

Controller

We can use serializers easily in controllers. All we have to do is create a model instance and pass it to render method.

Then active_model_serializers finds a suitable serializer for an instance given and serialize it, and the response returns.

# app/controllers/v1/images_controller.rb module V1 class ImagesController < ApplicationController def show render json: image, include: params[:include] end private def image @image ||= Image.new(image_attrs) end def image_attrs @image_attrs ||= fetch_data_somehow # Fetch external data end end end 
Enter fullscreen mode Exit fullscreen mode

Testing

When testing ActiveRecord models, we can use factory_bot and make the factories like:

FactoryBot.define do factory :user do name { Faker::Name.name } end end 
Enter fullscreen mode Exit fullscreen mode

Usage:

> user = FactoryBot.create(:user) 
Enter fullscreen mode Exit fullscreen mode

But in this case, FactoryBot#create does not work well because there is no store (in addition, it’s not needed to store them).

Plus, ActiveModelSerializers::Model is based on ActiveModel, so the initializer requires a hash named attributes.

If we omit the argument attributes, attributes = {} will be given as the default, then serialization does not work well expectedly.

# spec/factories/images.rb FactoryBot.define do factory :image do url { Faker::Internet.url } size { build(:image_size) } end end # spec/factories/image/sizes.rb FactoryBot.define do factory :image_size, class: 'Image::Size' do width { rand(100..500) } height { rand(100..500) } end end 
Enter fullscreen mode Exit fullscreen mode

Usage:

> image = FactoryBot.create(:image) => NoMethodError: undefined method `save!`... > image = FactoryBot.build(:image) > image.attributes => {} > image.to_json => "{}" 
Enter fullscreen mode Exit fullscreen mode

factory_bot provides initialize_with to override initializers.

Also, it provides skip_create to skip creation.

# spec/factories/images.rb FactoryBot.define do factory :image do skip_create initalize_with { new(attributes) } url { Faker::Internet.url } size { build(:image_size) } end end # spec/factories/image/sizes.rb FactoryBot.define do factory :image_size, class: 'Image::Size' do skip_create initalize_with { new(attributes) } width { rand(100..500) } height { rand(100..500) } end end 
Enter fullscreen mode Exit fullscreen mode

Usage:

> image = FactoryBot.create(:image) => #<Image:...> The result is the same as one from `build`🙌 > image = FactoryBot.build(:image) > image.attributes => { "url" => ..., "size" => ... } > image.to_json => "{\"url\":...,\"size\":...}" 
Enter fullscreen mode Exit fullscreen mode

To avoid writing initialize_with and skip_create many times, I eventually prepared a specific DSL like:

if defined?(FactoryBot) module FactoryBot module Syntax module Default class DSL # Custom DSL for ActiveModelSerializers::Model # Original: https://github.com/thoughtbot/factory_bot/blob/v5.0.2/lib/factory_bot/syntax/default.rb#L15-L26 def serializers_model_factory(name, options = {}, &block) factory = Factory.new(name, options) proxy = FactoryBot::DefinitionProxy.new(factory.definition) if block_given? proxy.instance_eval do skip_create initialize_with { new(attributes) } instance_eval(&block) end end FactoryBot.register_factory(factory) proxy.child_factories.each do |(child_name, child_options, child_block)| parent_factory = child_options.delete(:parent) || name serializers_model_factory(child_name, child_options.merge(parent: parent_factory), &child_block) end end end end end end end 
Enter fullscreen mode Exit fullscreen mode

The factory implementation turned out like:

# spec/factories/images.rb FactoryBot.define do serializers_model_factory :image do url { Faker::Internet.url } size { build(:image_size) } end end 
Enter fullscreen mode Exit fullscreen mode

Then we can use it in specs easily like:

# spec/serializers/image_serializer_spec.rb require 'rails_helper' RSpec.describe ImageSerializer, type: :serializer do let(:resource) { ActiveModelSerializers::SerializableResource.new(model, options) } let(:model) { build(:image) } let(:options) { { include: '*' } } describe '#url' do subject { resource.serializable_hash[:url] } it { is_expected.to eq model.url } end ... end 
Enter fullscreen mode Exit fullscreen mode

Summary

We can use active_model_serializers without ActiveRecord easily.

References

Top comments (0)