jsonapi-realizer
This library handles incoming json:api payloads and turns them, via an adapter system, into data models for your business logic.
Using
In order to use this library you'll want to have some models:
class Photo < ApplicationRecord
belongs_to :photographer, class_name: "Profile"
end
class Profile < ApplicationRecord
has_many :photos
end
Note: They don't have to be ActiveRecord models, but we have built-in support for that library (adapter-based).
Second you'll need some realizers:
class PhotoRealizer
include JSONAPI::Realizer::Resource
register :photos, class_name: "Photo", adapter: :active_record
has_one :photographer, as: :profiles
has :title
has :src
end
class ProfileRealizer
include JSONAPI::Realizer::Resource
register :profiles, class_name: "Profile", adapter: :active_record
has_many :photos
has :name
end
You can define special properties on attributes and relationships realizers:
has_many :doctors, as: :users, includable: false
has :title, selectable: false
Once you've designed your resources, we just need to use them! In this example, we'll use controllers from Rails:
class PhotosController < ApplicationController
def create
realization = JSONAPI::Realizer.create(params, headers: request.headers)
ProcessPhotosService.new(realization.model)
render json: JSONAPI::Serializer.serialize(realization.model)
end
def index
realization = JSONAPI::Realizer.index(params, headers: request.headers, type: :photos)
render json: JSONAPI::Serializer.serialize(realization.models, is_collection: true)
end
end
Notice that we pass realization.model
to ProcessPhotosService
, that's because jsonapi-realizer
doesn't do the act of saving, creating, or destroying! We just ready up the records for you to handle (including errors).
Policies
Most times you will want to control what a person sees when they as for your data. We have created interfaces for this use-case and we'll show how you can use pundit (or any PORO) to constrain your in/out.
First up is the policy itself:
class PhotoPolicy < ApplicationPolicy
class Scope < ApplicationPolicy::Scope
def resolve
case
when relation.with_role_state?(:administrator)
relation
when requester.with_onboarding_state?(:completed)
relation.where(photographer: requester)
else
relation.none
end
end
def sanitize(action, params)
case action
when :index
params.permit(:fields, :include, :filter)
else
params
end
end
end
def index?
requester.with_onboarding_state?(:completed)
end
end
class PhotoRealizer
include JSONAPI::Realizer::Resource
register :photos, class_name: "Photo", adapter: :active_record
has_one :photographer, as: :profiles
has :title
has :src
end
class PhotosController < ApplicationController
def index
# See: pundit for `policy_scope()`
realization = JSONAPI::Realizer.index(
policy(Photo).sanitize(:index, params),
headers: request.headers,
type: :posts,
scope: policy_scope(Photo)
)
# See: pundit for `authorize()`
(realization.relation)
render json: JSONAPI::Serializer.serialize(realization.models, is_collection: true)
end
end
Adapters
There are two core adapters:
:active_record
, which assumes an ActiveRecord-like interface.:memory
, which assumes aSTORE
Hash-like on the model class.
An adapter must provide the following interfaces:
find_via
, describes how to find the modelfind_many_via
, describes how to find many modelsassign_attributes_via
, describes how to write a set of propertiesassign_relationships_via
, describes how to write a set of relationshipsincludes_via
, describes how to eager include related modelssparse_fields_via
, describes how to only return certain fields
You can also provide custom adapter interfaces like below, which will use active_record
's find_many_via
, assign_relationships_via
, update_via
, includes_via
, and sparse_fields_via
:
class PhotoRealizer
include JSONAPI::Realizer::Resource
register :photos, class_name: "Photo", adapter: :active_record
adapter.find_via do |model_class, id|
model_class.where { id == id or slug == id }.first
end
adapter.assign_attributes_via do |model, attributes|
model.update_columns(attributes)
end
has_one :photographer, as: :profiles
has :title
has :src
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
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: ,
links: serialized_links,
jsonapi: serialized_jsonapi,
fields: serialized_fields(realization),
include: serialized_includes(realization),
namespace: ::V1
)
end
private def
{
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 = JSONAPI::Realizer.index(
policy(Account).sanitize(:index, params),
headers: request.headers,
scope: policy_scope(Account),
type: :accounts
)
realization.relation
render json: serialize(realization)
end
def create
realization = JSONAPI::Realizer.create(
policy(Account).sanitize(:create, params),
headers: request.headers,
scope: policy_scope(Account)
)
realization.relation
render json: serialize(realization)
end
end
end
jsonapi-home
I'm already using jsonapi-realizer and it's sister project jsonapi-serializers in a new gem of mine that allows services to be discoverable: jsonapi-home.
Notes
A successful JSON:API request can be annotated as:
JSONAPIRequest -> (BusinessLayer -> JSONAPIRequest -> (Record | Array<Record>)) -> JSONAPIResponse
The jsonapi-serializers
library provides this shape:
JSONAPIRequest -> (Record | Array<Record>) -> JSONAPIResponse
But it leaves fetching/creating/updating/destroying the records up to you! This is where jsonapi-realizer comes into play, as it provides this shape:
BusinessLayer -> JSONAPIRequest -> (Record | Array<Record>)
Installing
Add this line to your application's Gemfile:
gem "jsonapi-realizer", "4.1.0"
And then execute:
$ bundle
Or install it yourself with:
$ gem install jsonapi-realizer
Contributing
- Read the Code of Conduct
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request