Note: RailsAPI::Resources is an experiment that extracts the Resource
class from JSONAPI::Resources ("JR"). This project
should be considered a work in progress and may be abandoned at any point. This README was quickly extracted from
JR and certainly contains inaccurate information. In addition features may be added or removed at any point. Please do
not base production software on this library.
- [Installation] (#installation)
- [Usage] (#usage)
- [Resources] (#resources)
- [RailsAPI::Resource] (#railsapiresource)
- [Attributes] (#attributes)
- [Primary Key] (#primary-key)
- [Model Name] (#model-name)
- [Model Hints] (#model-hints)
- [Relationships] (#relationships)
- [Callbacks] (#callbacks)
- [Namespaces] (#namespaces)
- [Resources] (#resources)
- [Contributing] (#contributing)
- [License] (#license)
Add the gem to your application's Gemfile
:
gem 'railsapi-resources'
And then execute:
$ bundle
Or install it yourself as:
$ gem install railsapi-resources
Resources define the public interface to your API. A resource defines which attributes are exposed, as well as relationships to other resources.
Resource definitions should by convention be placed in a directory under app named resources, app/resources
. The class
name should be the single underscored name of the model that backs the resource with _resource.rb
appended. For example,
a Contact
model's resource should have a class named ContactResource
defined in a file named contact_resource.rb
.
Resources must be derived from RailsAPI::Resource
, or a class that is itself derived from RailsAPI::Resource
.
For example:
class ContactResource < RailsAPI::Resource
end
Resources that are not backed by a model (purely used as base classes for other resources) should be declared as abstract.
Because abstract resources do not expect to be backed by a model, they won't attempt to discover the model class or any of its relationships.
class BaseResource < RailsAPI::Resource
abstract
has_one :creator
end
class ContactResource < BaseResource
end
Resources that are immutable should be declared as such with the immutable
method.
Immutable resources can be used as the basis for a heterogeneous collection. Resources in heterogeneous collections can still be mutated through their own type-specific endpoints.
class VehicleResource < RailsAPI::Resource
immutable
has_one :owner
attributes :make, :model, :serial_number
end
class CarResource < VehicleResource
attributes :drive_layout
has_one :driver
end
class BoatResource < VehicleResource
attributes :length_at_water_line
has_one :captain
end
Any of a resource's attributes that are accessible must be explicitly declared. Single attributes can be declared using
the attribute
method, and multiple attributes can be declared with the attributes
method on the resource class.
For example:
class ContactResource < RailsAPI::Resource
attribute :name_first
attributes :name_last, :email, :twitter
end
This resource has 4 defined attributes: name_first
, name_last
, email
, twitter
, as well as the automatically
defined attributes id
and type
. By default these attributes must exist on the model that is handled by the resource.
A resource object wraps a Ruby object, usually an ActiveModel
record, which is available as the @model
variable.
This allows a resource's methods to access the underlying model.
For example, a computed attribute for full_name
could be defined as such:
class ContactResource < RailsAPI::Resource
attributes :name_first, :name_last, :email, :twitter
attribute :full_name
def full_name
"#{@model.name_first}, #{@model.name_last}"
end
end
By default all attributes are assumed to be fetchable. The list of fetchable attributes can be filtered by overriding
the fetchable_fields
method.
Here's an example that prevents guest users from seeing the email
field:
class AuthorResource < RailsAPI::Resource
attributes :name, :email
model_name 'Person'
has_many :posts
def fetchable_fields
if (context[:current_user].guest)
super - [:email]
else
super
end
end
end
fetchable_fields
is only a hint to other components using the resource. It is not enforced by the resource.
By default all attributes are assumed to be updatable and creatable. To prevent some attributes from being accepted by
the update
or create
methods, override the self.updatable_fields
and self.creatable_fields
methods on a resource.
This example prevents full_name
from being set:
class ContactResource < RailsAPI::Resource
attributes :name_first, :name_last, :full_name
def full_name
"#{@model.name_first}, #{@model.name_last}"
end
def self.updatable_fields(context)
super - [:full_name]
end
def self.creatable_fields(context)
super - [:full_name]
end
end
The context
is not by default used by the ResourceController
, but may be used if you override the controller methods.
By using the context you have the option to determine the creatable and updatable fields based on the user.
updatable_fields
and creatable_fields
are only hints to other components using the resource. They not enforced by the resource.
By default all attributes are assumed to be sortable. To prevent some attributes from being sortable, override the
self.sortable_fields
method on a resource.
Here's an example that prevents sorting by post's body
:
class PostResource < RailsAPI::Resource
attributes :title, :body
def self.sortable_fields(context)
super(context) - [:body]
end
end
It is possible to flatten Rails relationships into attributes by using getters and setters. This can become handy if a relation needs to be created alongside the creation of the main object which can be the case if there is a bi-directional presence validation. For example:
# Given Models
class Person < ActiveRecord::Base
has_many :spoken_languages
validates :name, :email, :spoken_languages, presence: true
end
class SpokenLanguage < ActiveRecord::Base
belongs_to :person, inverse_of: :spoken_languages
validates :person, :language_code, presence: true
end
# Resource with getters and setter
class PersonResource < RailsAPI::Resource
attributes :name, :email, :spoken_languages
# Getter
def spoken_languages
@model.spoken_languages.pluck(:language_code)
end
# Setter (because spoken_languages needed for creation)
def spoken_languages=(new_spoken_language_codes)
@model.spoken_languages.destroy_all
new_spoken_language_codes.each do |new_lang_code|
@model.spoken_languages.build(language_code: new_lang_code)
end
end
end
Resources are always represented using a key of id
. The resource will interrogate the model to find the primary key.
If the underlying model does not use id
as the primary key and does not support the primary_key
method you
must use the primary_key
method to tell the resource which field on the model to use as the primary key. Note:
this must be the actual primary key of the model.
By default only integer values are used for primary key.
You can override the default resource key type on a per-resource basis by calling key_type
in the resource class,
with the same allowed values as the resource_key_type
configuration option.
class ContactResource < RailsAPI::Resource
attribute :id
attributes :name_first, :name_last, :email, :twitter
key_type :uuid
end
The name of the underlying model is inferred from the Resource name. It can be overridden by use of the model_name
method. For example:
class AuthorResource < RailsAPI::Resource
model_name 'Person'
attribute :name
has_many :posts
end
Resource instances are created from model records. The determination of the correct resource type is performed using a
simple rule based on the model's name. The name is used to find a resource in the same module (as the originating
resource) that matches the name. This usually works quite well, however it can fail when model names do not match
resource names. It can also fail when using namespaced models. In this case a model_hint
can be created to map model
names to resources. For example:
class AuthorResource < RailsAPI::Resource
attribute :name
model_name 'Person'
model_hint model: Commenter, resource: :special_person
has_many :posts
has_many :commenters
end
Note that when model_name
is set a corresponding model_hint
is also added. This can be skipped by using the
add_model_hint
option set to false. For example:
class AuthorResource < RailsAPI::Resource
model_name 'Legacy::Person', add_model_hint: false
end
Model hints inherit from parent resources, but are not global in scope. The model_hint
method accepts model
and
resource
named parameters. model
takes an ActiveRecord class or class name (defaults to the model name), and
resource
takes a resource type or a resource class (defaults to the current resource's type).
Related resources need to be specified in the resource. These may be declared with the relationship
or the has_one
and the has_many
methods.
Here's a simple example using the relationship
method where a post has a single author and an author can have many
posts:
class PostResource < RailsAPI::Resource
attributes :title, :body
relationship :author, to: :one
end
And the corresponding author:
class AuthorResource < RailsAPI::Resource
attribute :name
relationship :posts, to: :many
end
And here's the equivalent resources using the has_one
and has_many
methods:
class PostResource < RailsAPI::Resource
attributes :title, :body
has_one :author
end
And the corresponding author:
class AuthorResource < RailsAPI::Resource
attribute :name
has_many :posts
end
The relationship methods (relationship
, has_one
, and has_many
) support the following options:
class_name
- a string specifying the underlying class for the related resource. Defaults to theclass_name
property on the underlying model.foreign_key
- the method on the resource used to fetch the related resource. Defaults to<resource_name>_id
for has_one and<resource_name>_ids
for has_many relationships.acts_as_set
- allows the entire set of related records to be replaced in one operation. Defaults to false if not set.polymorphic
- set to true to identify relationships that are polymorphic.relation_name
- the name of the relation to use on the model. A lambda may be provided which allows conditional selection of the relation based on the context.
to_one
relationships support the additional option:
foreign_key_on
- defaults to:self
. To indicate that the foreign key is on the related resource specify:related
.
Examples:
class CommentResource < RailsAPI::Resource
attributes :body
has_one :post
has_one :author, class_name: 'Person'
has_many :tags, acts_as_set: true
end
class ExpenseEntryResource < RailsAPI::Resource
attributes :cost, :transaction_date
has_one :currency, class_name: 'Currency', foreign_key: 'currency_code'
has_one :employee
end
class TagResource < RailsAPI::Resource
attributes :name
has_one :taggable, polymorphic: true
end
class BookResource < RailsAPI::Resource
# Only book_admins may see unapproved comments for a book. Using
# a lambda to select the correct relation on the model
has_many :book_comments, relation_name: -> (options = {}) {
context = options[:context]
current_user = context ? context[:current_user] : nil
unless current_user && current_user.book_admin
:approved_book_comments
else
:book_comments
end
}
...
end
The polymorphic relationship will require the resource and controller to exist, although routing to them will cause an error.
class TaggableResource < RailsAPI::Resource; end
class TaggablesController < RailsAPI::ResourceController; end
ActiveSupport::Callbacks
is used to provide callback functionality, so the behavior is very similar to what you may be
used to from ActiveRecord
.
For example, you might use a callback to perform authorization on your resource before an action.
class BaseResource < RailsAPI::Resource
before_create :authorize_create
def authorize_create
# ...
end
end
The types of supported callbacks are:
before
after
around
Callbacks can be defined for the following RailsAPI::Resource
events:
:create
:update
:remove
:save
:create_to_many_link
:replace_to_many_links
:create_to_one_link
:replace_to_one_link
:remove_to_many_link
:remove_to_one_link
:replace_fields
RailsAPI::Resources supports namespacing of resources. With namespacing you can version your API.
- Fork it ( http://github.com/cerebris/railsapi-resources/fork )
- 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 a new Pull Request
Copyright 2016 Cerebris Corporation. MIT License (see LICENSE for details).