Skip to content

krainboltgreene/jsonapi-realizer.rb

Repository files navigation

jsonapi-realizer

This library handles incoming json:api payloads and turns them, via an adapter system, into native data models. While designed with rails in mind, this library doesn't require rails to use. I'm already using jsonapi-realizer and it's sister project jsonapi-materializer in a gem of that allows http json services to be discoverable: jsonapi-home.

Using

In order to use this library you'll want to have some models:

class Profile < ApplicationRecord
  has_many :photos
end
class Photo < ApplicationRecord
  belongs_to :photographer, class_name: "Profile"
end

Note: They don't have to be ActiveRecord models, but we have built-in support for that library (via an adapter).

Second you'll need some realizers:

class ProfileRealizer
  include JSONAPI::Realizer::Resource

  type :profiles, class_name: "Profile", adapter: :active_record

  has_many :photos, class_name: "PhotoRealizer"

  has :name
end
class PhotoRealizer
  include JSONAPI::Realizer::Resource

  type :photos, class_name: "Photo", adapter: :active_record

  has_one :photographer, as: :profiles, class_name: "ProfileRealizer"

  has :title
  has :src
end

Now that we have these we can invoke them in the controller:

class PhotosController < ApplicationController
  def create
    realizer = PhotoRealizer.new(
      :intent => :create,
      :parameters => params,
      :headers => request.headers
    )

    realizer.object.save!

    render json: realizer.object.to_json
  end

  def index
    realizer = PhotoRealizer.new
      :intent => :index,
      :parameters => params,
      :headers => request.headers
    )

    render json: realizer.object.to_json
  end
end

Notice that we have to handle creating the model ourselves with realizer.object.save!. jsonapi-realizer doesn't act on a request, it only prepares you to act on the request.

Adapters

There are two core adapters:

  1. :active_record, which assumes an ActiveRecord-like interface.
  2. :memory, which assumes a STORE Hash-like on the model class.

An adapter must provide the following interfaces:

  1. find_many(scope), describes how to find many records
  2. find_one(scope, id), describes how to find one record
  3. filtering(scope, filters), describes how to filter records by a set of properties
  4. sorting(scope, sorts), describes how to sort records
  5. paginate(scope, per, offset), describes how to page records
  6. write_attributes(model, attributes), describes how to write a set of properties
  7. write_relationships(model, relationships), describes how to write a set of relationships
  8. include_relationships(scope, includes), describes how to eager include related models

You can also provide custom adapter interfaces like below:

JSONAPI::Realizer.configuration do |let|
  let.adapter_mappings = {
    active_record_postgres_pagination: PostgresActiveRecordPaginationAdapter
  }
end
module PostgresActiveRecordPaginationAdapter < JSONAPI::Realizer::Adapter::ActiveRecord
  def paginate(scope, per, offset)
    scope.offset(offset).limit(per)
  end
end
class PhotoRealizer
  include JSONAPI::Realizer::Resource

  type :photos, class_name: "Photo", adapter: :active_record_postgres_pagination
end

rails

If you want to use jsonapi-realizer in development mode you'll want to turn on eager_loading (by setting it to true in config/environments/development.rb) or by adding app/realizers to the eager_load_paths.

rails and pundit and jsonapi-serializers

While this gem contains nothing specifically targeting rails or pundit or jsonapi-serializers (a fantastic gem) I've already written some seamless integration code. This root controller will handle exceptions in a graceful way and also give you access to a clean interface for serializing:

module V1
  class ApplicationController < ::ApplicationController
    include Pundit

    after_action :verify_authorized, except: :index
    after_action :verify_policy_scoped, only: :index

    rescue_from JSONAPI::Realizer::Error::MissingAcceptHeader, with: :missing_accept_header
    rescue_from JSONAPI::Realizer::Error::InvalidAcceptHeader, with: :invalid_accept_header
    rescue_from Pundit::NotAuthorizedError, with: :access_not_authorized

    private def missing_accept_header
      head :not_acceptable
    end

    private def invalid_accept_header
      head :not_acceptable
    end

    private def access_not_authorized
      head :unauthorized
    end

    private def pundit_user
      current_account
    end

    private def serialize(realization)
      JSONAPI::Serializer.serialize(
        if realization.respond_to?(:models) then realization.models else realization.model end,
        is_collection: realization.respond_to?(:models),
        meta: serialized_metadata,
        links: serialized_links,
        jsonapi: serialized_jsonapi,
        fields: serialized_fields(realization),
        include: serialized_includes(realization),
        namespace: ::V1
      )
    end

    private def serialized_metadata
      {
        api: {
          version: "1"
        }
      }
    end

    private def serialized_links
      {
        discovery: {
          href: "/"
        }
      }
    end

    private def serialized_jsonapi
      {
        version: "1.0"
      }
    end

    private def serialized_fields(realization)
      realization.fields if realization.fields.any?
    end

    private def serialized_includes(realization)
      realization.includes if realization.includes.any?
    end
  end
end

You can see this resource controller used below:

module V1
  class AccountsController < ::V1::ApplicationController
    def index
      realization = PhotoRealizer.new(
        :intent => :index,
        :scope => policy_scope(Account),
        :parameters => policy(Account).sanitize(:index, params),
        :headers => request.headers
      )

      authorize realization.object

      render json: serialize(realization)
    end

    def create
      realization = PhotoRealizer.new(
        :intent => :create,
        :scope => policy_scope(Account),
        :parameters => policy(Account).sanitize(:create, params),
        :headers => request.headers
      )

      authorize realization.object

      render json: serialize(realization)
    end
  end
end

Installing

Add this line to your application's Gemfile:

$ bundle add jsonapi-realizer

Or install it yourself with:

$ gem install jsonapi-realizer

Contributing

  1. Read the Code of Conduct
  2. Fork it
  3. Create your feature branch (git checkout -b my-new-feature)
  4. Commit your changes (git commit -am 'Add some feature')
  5. Push to the branch (git push origin my-new-feature)
  6. Create new Pull Request

About

Turn JSON:API requests into real records

Resources

License

Stars

Watchers

Forks

Packages

No packages published