Skip to content

Repository files navigation

JSONAPI::Resources Gem VersionBuild StatusCode Climate

Join the chat at https://gitter.im/cerebris/jsonapi-resources

JSONAPI::Resources, or "JR", provides a framework for developing a server that complies with the JSON API specification.

Like JSON API itself, JR's design is focused on the resources served by an API. JR needs little more than a definition of your resources, including their attributes and relationships, to make your server compliant with JSON API.

JR is designed to work with Rails 4.0+, and provides custom routes, controllers, and serializers. JR's resources may be backed by ActiveRecord models or by custom objects.

Table of Contents

  • [Demo App] (#demo-app)
  • [Client Libraries] (#client-libraries)
  • [Installation] (#installation)
  • [Usage] (#usage)
    • [Resources] (#resources)
      • [JSONAPI::Resource] (#jsonapiresource)
      • [Context] (#context)
      • [Attributes] (#attributes)
      • [Primary Key] (#primary-key)
      • [Model Name] (#model-name)
      • [Model Hints] (#model-hints)
      • [Relationships] (#relationships)
      • [Filters] (#filters)
      • [Pagination] (#pagination)
      • [Included relationships (side-loading resources)] (#included-relationships-side-loading-resources)
      • [Resource meta] (#resource-meta)
      • [Custom Links] (#custom-links)
      • [Callbacks] (#callbacks)
    • [Controllers] (#controllers)
      • [Namespaces] (#namespaces)
      • [Error Codes] (#error-codes)
      • [Handling Exceptions] (#handling-exceptions)
      • [Action Callbacks] (#action-callbacks)
    • [Operation Processors] (#operation-processors)
    • [Serializer] (#serializer)
      • [Serializer options] (#serializer-options)
      • [Formatting] (#formatting)
      • [Key Format] (#key-format)
    • [Routing] (#routing)
      • [Nested Routes] (#nested-routes)
  • [Configuration] (#configuration)
  • [Contributing] (#contributing)
  • [License] (#license)

Demo App

We have a simple demo app, called Peeps, available to show how JR is used.

Client Libraries

JSON API maintains a (non-verified) listing of client libraries which should be compatible with JSON API compliant server implementations such as JR.

Installation

Add JR to your application's Gemfile:

gem 'jsonapi-resources'

And then execute:

$ bundle

Or install it yourself as:

$ gem install jsonapi-resources

Usage

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 file 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.

JSONAPI::Resource

Resources must be derived from JSONAPI::Resource, or a class that is itself derived from JSONAPI::Resource.

For example:

classContactResource < JSONAPI::Resourceend

A jsonapi-resource generator is avaliable

rails generate jsonapi:resource contact
Abstract Resources

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.

classBaseResource < JSONAPI::Resourceabstracthas_one:creatorendclassContactResource < BaseResourceend
Immutable Resources

Resources that are immutable should be declared as such with the immutable method. Immutable resources will only generate routes for index, show and show_relationship.

Immutable for Readonly

Some resources are read-only and are not to be modified through the API. Declaring a resource as immutable prevents creation of routes that allow modification of the resource.

Immutable Heterogeneous Collections

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.

classVehicleResource < JSONAPI::Resourceimmutablehas_one:ownerattributes:make,:model,:serial_numberendclassCarResource < VehicleResourceattributes:drive_layouthas_one:driverendclassBoatResource < VehicleResourceattributes:length_at_water_linehas_one:captainend# routesjsonapi_resources:vehiclesjsonapi_resources:carsjsonapi_resources:boats

In the above example vehicles are immutable. A call to /vehicles or /vehicles/1 will return vehicles with types of either car or boat. But calls to PUT or POST a car must be made to /cars. The rails models backing the above code use Single Table Inheritance.

Context

Sometimes you will want to access things such as the current logged in user (and other state only available within your controllers) from within your resource classes. To make this state available to a resource class you need to put it into the context hash - this can be done via a context method on one of your controllers or across all controllers using ApplicationController.

For example:

classApplicationController < JSONAPI::ResourceControllerdefcontext{current_user: current_user}endend# Specific resource controllers derive from ApplicationController# and share its contextclassPeopleController < ApplicationControllerend# Assuming you don't permit user_id (so the client won't assign a wrong user to own the object)# you can ensure the current user is assigned the record by using the controller's context hash.classPeopleResource < JSONAPI::Resourcebefore_savedo@model.user_id=context[:current_user].idif@model.new_record?endend

You can put things that affect serialization and resource configuration into the context.

Attributes

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:

classContactResource < JSONAPI::Resourceattribute:name_firstattributes:name_last,:email,:twitterend

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:

classContactResource < JSONAPI::Resourceattributes:name_first,:name_last,:email,:twitterattribute:full_namedeffull_name"#{@model.name_first}, #{@model.name_last}"endend
Attribute Delegation

Normally resource attributes map to an attribute on the model of the same name. Using the delegate option allows a resource attribute to map to a differently named model attribute. For example:

classContactResource < JSONAPI::Resourceattribute:name_first,delegate: :first_nameattribute:name_last,delegate: :last_nameend
Fetchable Attributes

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:

classAuthorResource < JSONAPI::Resourceattributes:name,:emailmodel_name'Person'has_many:postsdeffetchable_fieldsif(context[:current_user].guest)super - [:email]elsesuperendendend

Context flows through from the controller to the resource and can be used to control the attributes based on the current user (or other value).

Creatable and Updatable Attributes

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:

classContactResource < JSONAPI::Resourceattributes:name_first,:name_last,:full_namedeffull_name"#{@model.name_first}, #{@model.name_last}"enddefself.updatable_fields(context)super - [:full_name]enddefself.creatable_fields(context)super - [:full_name]endend

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.

Sortable Attributes

JR supports sorting primary resources by multiple sort criteria.

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:

classPostResource < JSONAPI::Resourceattributes:title,:bodydefself.sortable_fields(context)super(context) - [:body]endend

JR also supports sorting primary resources by fields on relationships.

Here's an example of sorting books by the author name:

classBook < ActiveRecord::Basebelongs_to:authorendclassAuthor < ActiveRecord::Basehas_many:booksendclassBookResource < JSONAPI::Resourceattributes:title,:bodydefself.sortable_fields(context)super(context) << :"author.name"endend

The request will look something like:

GET /books?include=author&sort=author.name
Attribute Formatting

Attributes can have a Format. By default all attributes use the default formatter. If an attribute has the format option set the system will attempt to find a formatter based on this name. In the following example the last_login_time will be returned formatted to a certain time zone:

classPersonResource < JSONAPI::Resourceattributes:name,:emailattribute:last_login_time,format: :date_with_timezoneend

The system will lookup a value formatter named DateWithTimezoneValueFormatter and will use this when serializing and updating the attribute. See the Value Formatters section for more details.

Flattening a Rails relationship

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 ModelsclassPerson < ActiveRecord::Basehas_many:spoken_languagesvalidates:name,:email,:spoken_languages,presence: trueendclassSpokenLanguage < ActiveRecord::Basebelongs_to:person,inverse_of: :spoken_languagesvalidates:person,:language_code,presence: trueend# Resource with getters and setterclassPersonResource < JSONAPI::Resourceattributes:name,:email,:spoken_languages# Getterdefspoken_languages@model.spoken_languages.pluck(:language_code)end# Setter (because spoken_languages needed for creation)defspoken_languages=(new_spoken_language_codes)@model.spoken_languages.destroy_allnew_spoken_language_codes.eachdo |new_lang_code|
@model.spoken_languages.build(language_code: new_lang_code)endendend

Primary Key

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 allowed for primary key. To change this behavior you can set the resource_key_type configuration option:

JSONAPI.configuredo |config|
# Allowed values are :integer(default), :uuid, :string, or a procconfig.resource_key_type=:uuidend
Override key type on a resource

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.

classContactResource < JSONAPI::Resourceattribute:idattributes:name_first,:name_last,:email,:twitterkey_type:uuidend
Custom resource key validators

If you need more control over the key, you can override the #verify_key method on your resource, or set a lambda that accepts key and context arguments in config/initializers/jsonapi_resources.rb:

JSONAPI.configuredo |config|
config.resource_key_type=->(key,context){key && String(key)}end

Model Name

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:

classAuthorResource < JSONAPI::Resourceattribute:namemodel_name'Person'has_many:postsend

Model Hints

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:

classAuthorResource < JSONAPI::Resourceattribute:namemodel_name'Person'model_hintmodel: Commenter,resource: :special_personhas_many:postshas_many:commentersend

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:

classAuthorResource < JSONAPI::Resourcemodel_name'Legacy::Person',add_model_hint: falseend

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).

Relationships

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:

classPostResource < JSONAPI::Resourceattributes:title,:bodyrelationship:author,to: :oneend

And the corresponding author:

classAuthorResource < JSONAPI::Resourceattribute:namerelationship:posts,to: :manyend

And here's the equivalent resources using the has_one and has_many methods:

classPostResource < JSONAPI::Resourceattributes:title,:bodyhas_one:authorend

And the corresponding author:

classAuthorResource < JSONAPI::Resourceattribute:namehas_many:postsend
Options

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 the class_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.
  • always_include_linkage_data - if set to true, the relationship includes linkage data. Defaults to false if not set.

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:

classCommentResource < JSONAPI::Resourceattributes:bodyhas_one:posthas_one:author,class_name: 'Person'has_many:tags,acts_as_set: trueendclassExpenseEntryResource < JSONAPI::Resourceattributes:cost,:transaction_datehas_one:currency,class_name: 'Currency',foreign_key: 'currency_code'has_one:employeeendclassTagResource < JSONAPI::Resourceattributes:namehas_one:taggable,polymorphic: trueend
classBookResource < JSONAPI::Resource# Only book_admins may see unapproved comments for a book. Using# a lambda to select the correct relation on the modelhas_many:book_comments,relation_name: ->(options={}){context=options[:context]current_user=context ? context[:current_user] : nilunlesscurrent_user && current_user.book_admin:approved_book_commentselse:book_commentsend}
...
end

The polymorphic relationship will require the resource and controller to exist, although routing to them will cause an error.

classTaggableResource < JSONAPI::Resource;endclassTaggablesController < JSONAPI::ResourceController;end

Filters

Filters for locating objects of the resource type are specified in the resource definition. Single filters can be declared using the filter method, and multiple filters can be declared with the filters method on the resource class.

For example:

classContactResource < JSONAPI::Resourceattributes:name_first,:name_last,:email,:twitterfilter:idfilters:name_first,:name_lastend

Then a request could pass in a filter for example http://example.com/contacts?filter[name_last]=Smith and the system will find all people where the last name exactly matches Smith.

Default Filters

A default filter may be defined for a resource using the default option on the filter method. This default is used unless the request overrides this value.

For example:

classCommentResource < JSONAPI::Resourceattributes:body,:statushas_one:posthas_one:authorfilter:status,default: 'published,pending'end

The default value is used as if it came from the request.

Applying Filters

You may customize how a filter behaves by supplying a callable to the :apply option. This callable will be used to apply that filter. The callable is passed the records, which is an ActiveRecord::Relation, the value, and an _options hash. It is expected to return an ActiveRecord::Relation.

This example shows how you can implement different approaches for different filters.

filter:visibility,apply: ->(records,value,_options){records.where('users.publicly_visible = ?',value == :public)}

If you omit the apply callable the filter will be applied as records.where(filter => value).

Note: It is also possible to override the self.apply_filter method, though this approach is now deprecated:

defself.apply_filter(records,filter,value,options)casefilterwhen:last_name,:first_name,:nameifvalue.is_a?(Array)value.eachdo |val|
records=records.where(_model_class.arel_table[filter].matches(val))endreturnrecordselserecords.where(_model_class.arel_table[filter].matches(value))endelsereturnsuper(records,filter,value)endend
Verifying Filters

Because filters typically come straight from the request, it's prudent to verify their values. To do so, provide a callable to the verify option. This callable will be passed the value and the context. Verify should return the verified value, which may be modified.

filter:ids,verify: ->(values,context){verify_keys(values,context)returnvalues},apply: ->(records,value,_options){records.where('id IN (?)',value)}
Finders

Basic finding by filters is supported by resources. This is implemented in the find and find_by_key finder methods. Currently this is implemented for ActiveRecord based resources. The finder methods rely on the records method to get an ActiveRecord::Relation relation. It is therefore possible to override records to affect the three find related methods.

Customizing base records for finder methods

If you need to change the base records on which find and find_by_key operate, you can override the records method on the resource class.

For example to allow a user to only retrieve his own posts you can do the following:

classPostResource < JSONAPI::Resourceattributes:title,:bodydefself.records(options={})context=options[:context]context[:current_user].postsendend

When you create a relationship, a method is created to fetch record(s) for that relationship, using the relation name for the relationship.

classPostResource < JSONAPI::Resourcehas_one:authorhas_many:comments# def record_for_author# relation_name = relationship.relation_name(context: @context)# records_for(relation_name)# end# def records_for_comments# relation_name = relationship.relation_name(context: @context)# records_for(relation_name)# endend

For example, you may want to raise an error if the user is not authorized to view the related records. See the next section for additional details on raising errors.

classBaseResource < JSONAPI::Resourcedefrecords_for(relation_name)context=options[:context]records=_model.public_send(relation_name)unlesscontext[:current_user].can_view?(records)raiseNotAuthorizedErrorendrecordsendend
Raising Errors

Inside the finder methods (like records_for) or inside of resource callbacks (like before_save) you can raise an error to halt processing. JSONAPI::Resources has some built in errors that will return appropriate error codes. By default any other error that you raise will return a 500 status code for a general internal server error.

To return useful error codes that represent application errors you should set the exception_class_whitelist config variable, and then you should use the Rails rescue_from macro to render a status code.

For example, this config setting allows the NotAuthorizedError to bubble up out of JSONAPI::Resources and into your application.

# config/initializer/jsonapi-resources.rbJSONAPI.configuredo |config|
config.exception_class_whitelist=[NotAuthorizedError]end

Handling the error and rendering the appropriate code is now the resonsiblity of the application and could be handled like this:

classApiController < ApplicationControllerrescue_fromNotAuthorizedError,with: :reject_forbidden_requestdefreject_forbidden_requestrenderjson: {error: 'Forbidden'},:status=>403endend
Applying Filters

The apply_filter method is called to apply each filter to the Arel relation. You may override this method to gain control over how the filters are applied to the Arel relation.

This example shows how you can implement different approaches for different filters.

defself.apply_filter(records,filter,value,options)casefilterwhen:visibilityrecords.where('users.publicly_visible = ?',value == :public)when:last_name,:first_name,:nameifvalue.is_a?(Array)value.eachdo |val|
records=records.where(_model_class.arel_table[filter].matches(val))endreturnrecordselserecords.where(_model_class.arel_table[filter].matches(value))endelsereturnsuper(records,filter,value)endend
Applying Sorting

You can override the apply_sort method to gain control over how the sorting is done. This may be useful in case you'd like to base the sorting on variables in your context.

Example:

defself.apply_sort(records,order_options,context={})iforder_options.has?(:trending)records=records.order_by_trending_scopeorder_options - [:trending]endsuper(records,order_options,context)end
Override finder methods

Finally if you have more complex requirements for finding you can override the find and find_by_key methods on the resource class.

Here's an example that defers the find operation to a current_user set on the context option:

classAuthorResource < JSONAPI::Resourceattribute:namemodel_name'Person'has_many:postsfilter:namedefself.find(filters,options={})context=options[:context]authors=context[:current_user].find_authors(filters)returnauthors.mapdo |author|
self.new(author,context)endendend

Pagination

Pagination is performed using a paginator, which is a class responsible for parsing the page request parameters and applying the pagination logic to the results.

Paginators

JSONAPI::Resource supports several pagination methods by default, and allows you to implement a custom system if the defaults do not meet your needs.

Paged Paginator

The pagedpaginator returns results based on pages of a fixed size. Valid page parameters are number and size. If number is omitted the first page is returned. If size is omitted the default_page_size from the configuration settings is used.

GET /articles?page%5Bnumber%5D=10&page%5Bsize%5D=10 HTTP/1.1
Accept: application/vnd.api+json
Offset Paginator

The offsetpaginator returns results based on an offset from the beginning of the resultset. Valid page parameters are offset and limit. If offset is omitted a value of 0 will be used. If limit is omitted the default_page_size from the configuration settings is used.

GET /articles?page%5Blimit%5D=10&page%5Boffset%5D=10 HTTP/1.1
Accept: application/vnd.api+json
Custom Paginators

Custom paginators can be used. These should derive from Paginator. The apply method takes a relation and order_options and is expected to return a relation. The initialize method receives the parameters from the page request parameters. It is up to the paginator author to parse and validate these parameters.

For example, here is a very simple single record at a time paginator:

classSingleRecordPaginator < JSONAPI::Paginatordefinitialize(params)# param parsing and validation here@page=params.to_ienddefapply(relation,order_options)relation.offset(@page).limit(1)endend
Paginator Configuration

The default paginator, which will be used for all resources, is set using JSONAPI.configure. For example, in your config/initializers/jsonapi_resources.rb:

JSONAPI.configuredo |config|
# built in paginators are :none, :offset, :pagedconfig.default_paginator=:offsetconfig.default_page_size=10config.maximum_page_size=20end

If no default_paginator is configured, pagination will be disabled by default.

Paginators can also be set at the resource-level, which will override the default setting. This is done using the paginator method:

classBookResource < JSONAPI::Resourceattribute:titleattribute:isbnpaginator:offsetend

To disable pagination in a resource, specify :none for paginator.

Included relationships (side-loading resources)

JR supports request include params out of the box, for side loading related resources.

Here's an example from the spec:

GET /articles/1?include=comments HTTP/1.1
Accept: application/vnd.api+json

Will get you the following payload by default:

{
"data": {
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON API paints my bikeshed!"
},
"links": {
"self": "http://example.com/articles/1"
},
"relationships": {
"comments": {
"links": {
"self": "http://example.com/articles/1/relationships/comments",
"related": "http://example.com/articles/1/comments"
},
"data": [
{ "type": "comments", "id": "5" },
{ "type": "comments", "id": "12" }
]
}
}
},
"included": [{
"type": "comments",
"id": "5",
"attributes": {
"body": "First!"
},
"links": {
"self": "http://example.com/comments/5"
}
}, {
"type": "comments",
"id": "12",
"attributes": {
"body": "I like XML better"
},
"links": {
"self": "http://example.com/comments/12"
}
}]
}

Resource Meta

Meta information can be included for each resource using the meta method in the resource declaration. For example:

classBookResource < JSONAPI::Resourceattribute:titleattribute:isbndefmeta(options){copyright: 'API Copyright 2015 - XYZ Corp.',computed_copyright: options[:serialization_options][:copyright],last_updated_at: _model.updated_at}endend

The meta method will be called for each resource instance. Override the meta method on a resource class to control the meta information for the resource. If a non empty hash is returned from meta this will be serialized. The meta method is called with an options hash. The options hash will contain the following:

  • :serializer -> the serializer instance
  • :serialization_options -> the contents of the serialization_options method on the controller.

Custom Links

Custom links can be included for each resource by overriding the custom_links method. If a non empty hash is returned from custom_links, it will be merged with the default links hash containing the resource's self link. The custom_links method is called with the same options hash used by for resource meta information. The options hash contains the following:

  • :serializer -> the serializer instance
  • :serialization_options -> the contents of the serialization_options method on the controller.

For example:

classCityCouncilMeeting < JSONAPI::Resourceattribute:title,:location,:approveddefcustom_links(options){minutes: options[:serializer].link_builder.self_link(self) + "/minutes"}endend

This will create a custom link with the key minutes, which will be merged with the default self link, like so:

{
"data": [
{
"id": "1",
"type": "cityCouncilMeetings",
"links": {
"self": "http://city.gov/api/city-council-meetings/1",
"minutes": "http://city.gov/api/city-council-meetings/1/minutes"
},
"attributes": {...}
},
//...
]
}

Of course, the custom_links method can include logic to include links only when relevant:

classCityCouncilMeeting < JSONAPI::Resourceattribute:title,:location,:approveddelegate:approved?,to: :modeldefcustom_links(options)extra_links={}ifapproved?extra_links[:minutes]=options[:serializer].link_builder.self_link(self) + "/minutes"endextra_linksendend```
It's also possibly to suppress the default `self` link by returning a hash with `{self: nil}`:````rubyclass Selfless < JSONAPI::Resource def custom_links(options) {self: nil} endend```#### Callbacks`ActiveSupport::Callbacks` is used to provide callback functionality, so the behavior is very similar to what you may beused to from `ActiveRecord`.For example, you might use a callback to perform authorization on your resource before an action.```rubyclass BaseResource < JSONAPI::Resource before_create :authorize_create def authorize_create # ... endend```The types of supported callbacks are:- `before`- `after`- `around`##### `JSONAPI::Resource` CallbacksCallbacks can be defined for the following `JSONAPI::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`##### `JSONAPI::Processor` CallbacksCallbacks can also be defined for `JSONAPI::Processor` events:- `:operation`: Any individual operation.- `:find`: A `find` operation is being processed.- `:show`: A `show` operation is being processed.- `:show_relationship`: A `show_relationship` operation is being processed.- `:show_related_resource`: A `show_related_resource` operation is being processed.- `:show_related_resources`: A `show_related_resources` operation is being processed.- `:create_resource`: A `create_resource` operation is being processed.- `:remove_resource`: A `remove_resource` operation is being processed.- `:replace_fields`: A `replace_fields` operation is being processed.- `:replace_to_one_relationship`: A `replace_to_one_relationship` operation is being processed.- `:create_to_many_relationship`: A `create_to_many_relationship` operation is being processed.- `:replace_to_many_relationship`: A `replace_to_many_relationship` operation is being processed.- `:remove_to_many_relationship`: A `remove_to_many_relationship` operation is being processed.- `:remove_to_one_relationship`: A `remove_to_one_relationship` operation is being processed.See [Operation Processors] (#operation-processors) for details on using OperationProcessors##### `JSONAPI::OperationsProcessor` Callbacks (a removed feature)Note: The `JSONAPI::OperationsProcessor` has been removed and replaced with the `JSONAPI::OperationDispatcher`and `Processor` classes per resource. The callbacks have been renamed and moved to the`Processor`s, with the exception of the `operations` callback which is now on the controller.### ControllersThere are two ways to implement a controller for your resources. Either derive from `ResourceController` or importthe `ActsAsResourceController` module.##### ResourceController`JSONAPI::Resources` provides a class, `ResourceController`, that can be used as the base class for your controllers.`ResourceController` supports `index`, `show`, `create`, `update`, and `destroy` methods. Just deriving your controllerfrom `ResourceController` will give you a fully functional controller.For example:```rubyclass PeopleController < JSONAPI::ResourceControllerend```Of course you are free to extend this as needed and override action handlers or other methods.A jsonapi-controller generator is avaliable```rails generate jsonapi:controller contact```###### ResourceControllerMetal`JSONAPI::Resources` also provides an alternative class to `ResourceController` called `ResourceControllerMetal`.In order to provide a lighter weight controller option this strips the controller down to just the classes needed to work with `JSONAPI::Resources`.For example:```rubyclass PeopleController < JSONAPI::ResourceControllerMetalend```Note: This may not provide all of the expected controller capabilities if you are using additional gems such as DoorKeeper.###### Serialization OptionsAdditional options can be passed to the serializer using the `serialization_options` method.For example:```rubyclass ApplicationController < JSONAPI::ResourceController def serialization_options {copyright: 'Copyright2015'} endend```These `serialization_options` are passed to the `meta` method used to generate resource `meta` values.##### ActsAsResourceController`JSONAPI::Resources` also provides a module, `JSONAPI::ActsAsResourceController`. You can include this module tomix in all the features of `ResourceController` into your existing controller class.For example:```rubyclass PostsController < ActionController::Base include JSONAPI::ActsAsResourceControllerend```#### NamespacesJSONAPI::Resources supports namespacing of controllers and resources. With namespacing you can version your API.If you namespace your controller it will require a namespaced resource.In the following example we have a `resource` that isn'tnamespaced,andonethehasnowbeennamespaced.Thereareslightdifferencesbetweenthetworesources,asmightbeseeninanewversionofanAPI:
```rubyclassPostResource < JSONAPI::Resourceattribute:titleattribute:bodyattribute:subjecthas_one:author,class_name: 'Person'has_one:sectionhas_many:tags,acts_as_set: truehas_many:comments,acts_as_set: falsedefsubject@model.titleendfilters:title,:author,:tags,:commentsfilter:idend
...
moduleApimoduleV1classPostResource < JSONAPI::Resource# V1 replaces the non-namespaced resource# V1 no longer supports tags and now calls author 'writer'attribute:titleattribute:bodyattribute:subjecthas_one:writer,foreign_key: 'author_id'has_one:sectionhas_many:comments,acts_as_set: falsedefsubject@model.titleendfilters:writerendclassWriterResource < JSONAPI::Resourceattributes:name,:emailmodel_name'Person'has_many:postsfilter:nameendendend```
Thefollowingcontrollersareused:
```rubyclass PostsController < JSONAPI::ResourceControllerendmodule Api module V1 class PostsController < JSONAPI::ResourceController end endend```Youwillalsoneedtonamespaceyourroutes:
```rubyRails.application.routes.draw do jsonapi_resources :posts namespace :api do namespace :v1 do jsonapi_resources :posts end endend```Whenanamespaced`resource`isused,anyrelated`resources`mustalsobeinthesamenamespace.#### Error codesErrorcodesareprovidedforeacherrorobjectreturned,basedontheerror.Theseerrorsare:
```rubymodule JSONAPI VALIDATION_ERROR = '100' INVALID_RESOURCE = '101' FILTER_NOT_ALLOWED = '102' INVALID_FIELD_VALUE = '103' INVALID_FIELD = '104' PARAM_NOT_ALLOWED = '105' PARAM_MISSING = '106' INVALID_FILTER_VALUE = '107' COUNT_MISMATCH = '108' KEY_ORDER_MISMATCH = '109' KEY_NOT_INCLUDED_IN_URL = '110' INVALID_INCLUDE = '112' RELATION_EXISTS = '113' INVALID_SORT_CRITERIA = '114' INVALID_LINKS_OBJECT = '115' TYPE_MISMATCH = '116' INVALID_PAGE_OBJECT = '117' INVALID_PAGE_VALUE = '118' INVALID_FIELD_FORMAT = '119' INVALID_FILTERS_SYNTAX = '120' SAVE_FAILED = '121' FORBIDDEN = '403' RECORD_NOT_FOUND = '404' NOT_ACCEPTABLE = '406' UNSUPPORTED_MEDIA_TYPE = '415' LOCKED = '423'end```Thesecodescanbecustomizedinyourappbycreatinganinitializertooverrideanyorallofthecodes.Inadditiontextualerrorcodescanbereturnedbysettingtheconfigurationoption`use_text_errors = true`.Forexample:
```rubyJSONAPI.configure do |config| config.use_text_errors = trueend```#### Handling ExceptionsBydefault,allexceptionsraiseddownstreamfromaresourcecontrollerwillbecaught,logged,anda```500 Internal Server Error```willberendered.Exceptionscanbewhitelistedintheconfigtopassthroughthehandlerandbecaughtmanually,oryoucanpassacallbackfromaresourcecontrollertoinsertlogicintotherescueblockwithoutinterruptingthecontrolflow.Thiscanbeparticularlyusefulforadditionalloggingormonitoringwithouttheaddedworkofrenderingresponses.Passablock,refertocontrollerclassmethods,orboth.Notethatmethodsmustbedefinedasclassmethodsonacontrollerandacceptoneparameter,whichispassedtheexceptionobjectthatwasrescued.
```rubyclassApplicationController < JSONAPI::ResourceControlleron_server_error:first_callback#or# on_server_error do |error|#do things#enddefself.first_callback(error)#env["airbrake.error_id"] = notify_airbrake(error)endend```#### Action Callbacks##### ensure_correct_media_typeBy default, when controllers extend functionalities from `jsonapi-resources`, the `ActsAsResourceController#ensure_correct_media_type`methodwillbetriggeredbefore`create`,`update`,`create_relationship`and`update_relationship`actions.Thismethodisreponsibleforcheckingifclient's request corresponds to the correct media type required by [JSON API](http://jsonapi.org/format/#content-negotiation-clients): `application/vnd.api+json`.In case you need to check the media type for custom actions, just make sure to call the method in your controller's`before_action`:
```rubyclass UsersController < JSONAPI::ResourceController before_action :ensure_correct_media_type, only: [:auth] def auth # some crazy auth code goes here endend```### Operation ProcessorsOperationProcessorsarecalledtoperformtheoperation(s)thatmakeuparequest.Thecontroller(throughthe`OperationDispatcher`),createsan`OperatorProcessor`tohandleeachoperation.Theprocessoriscreatedbasedontheresourcename,includingthenamespace.Ifaprocessordoes not existforaresource(namespacematters)thedefaultoperationprocessorisusedinstead.Thedefaultprocessorcanbechangedbyaconfigurationsetting.Definingacustom`Processor`allowsforcustomcallbackhandlingofeachoperationtypeforeachresourcetype.Forexample:
```rubyclass Api::V4::BookProcessor < JSONAPI::Processor after_find do unless @result.is_a?(JSONAPI::ErrorsOperationResult) @result.meta[:total_records_found] = @result.record_count end endend```Thissimpleexampleusesacallbacktoupdatetheresult's meta property with the total count of records (a redundantfeature only for example purposes), if there wasn'tanerrorintheoperation.Itisalsopossibletooverridethe`find`methodaswellifadifferentbehaviorisneeded,forexample:
```rubyclass Api::V4::BookProcessor < JSONAPI::Processor def find filters = params[:filters] include_directives = params[:include_directives] sort_criteria = params.fetch(:sort_criteria, []) paginator = params[:paginator] verified_filters = resource_klass.verify_filters(filters, context) resource_records = resource_klass.find(verified_filters, context: context, include_directives: include_directives, sort_criteria: sort_criteria, paginator: paginator) page_options = {} # Overriding the default record count logic to always include it in the meta #if (JSONAPI.configuration.top_level_meta_include_record_count || # (paginator && paginator.class.requires_record_count)) page_options[:record_count] = resource_klass.find_count(verified_filters, context: context, include_directives: include_directives) #endend```Note: Theauthorsofthisgemexpectthemostcommonusescasestobehandledusingthecallbacks.Itislikelythattheinternalfunctionalityoftheoperationprocessingmethodswillchange,atleastforseveralrevisions.Effortwillbemadetocallthisoutinreleasenotes.Youhavebeenwarned.### SerializerThe`ResourceSerializer`canbeusedtoserializearesourceintoJSONAPIcompliantJSON. `ResourceSerializer` must be initialized with the primary resource type it will be serializing. `ResourceSerializer` has a `serialize_to_hash` method that takes a resource instance or array of resource instances to serialize. For example:```rubypost=Post.find(1)JSONAPI::ResourceSerializer.new(PostResource).serialize_to_hash(PostResource.new(post,nil))```
Thisreturnsresultslikethis:
```json{ "data": { "type": "posts", "id": "1", "links": { "self": "http://example.com/posts/1" }, "attributes": { "title": "New post", "body": "A body!!!", "subject": "New post" }, "relationships": { "section": { "links": { "self": "http://example.com/posts/1/relationships/section", "related": "http://example.com/posts/1/section" }, "data": null }, "author": { "links": { "self": "http://example.com/posts/1/relationships/author", "related": "http://example.com/posts/1/author" }, "data": { "type": "people", "id": "1" } }, "tags": { "links": { "self": "http://example.com/posts/1/relationships/tags", "related": "http://example.com/posts/1/tags" } }, "comments": { "links": { "self": "http://example.com/posts/1/relationships/comments", "related": "http://example.com/posts/1/comments" } } } }}```#### Serializer optionsThe`ResourceSerializer`canbeinitializedwithsomeoptionalparameters:
##### `include`Anarrayofresources.Nestedresourcescanbespecifiedwithdotnotation.
*Purpose*: determineswhichobjectswillbesideloadedwiththesourceobjectsinan`included`section
*Example*: ```include: ['comments','author','comments.tags','author.posts']```##### `fields`Ahashofresourcetypesandarraysoffieldsforeachresourcetype.
*Purpose*: determineswhichfieldsareserializedforaresourcetype.Thisencompassesbothattributesandrelationshipidsinthelinkssectionforaresource.Fieldsareglobalforaresourcetype.
*Example*: ```fields: { people: [:email, :comments], posts: [:title, :author], comments: [:body, :post]}``````rubypost = Post.find(1)include_resources = ['comments','author','comments.tags','author.posts']JSONAPI::ResourceSerializer.new(PostResource, include: include_resources, fields: { people: [:email, :comments], posts: [:title, :author], tags: [:name], comments: [:body, :post] }).serialize_to_hash(PostResource.new(post, nil))```#### FormattingJRbydefaultusessomesimplerulestoformat(andunformat)anattributefor(de-)serialization.StringsandIntegersareoutputtoJSONasis,andallothervalueshave`.to_s`appliedtothem.Thisoutputssomethinginallcases,butitiscertainlynotcorrectforeverysituation.Ifyouwanttochangethewayanattributeis(de-)serializedyouhaveacoupleofways.Thesimplestmethodistocreateagetter(andsetter)methodontheresourcewhichoverridestheattributeandapplythe(un-)formattingthere.Forexample:
```rubyclass PersonResource < JSONAPI::Resource attributes :name, :email, :last_login_time # Setter example def email=(new_email) @model.email = new_email.downcase end # Getter example def last_login_time @model.last_login_time.in_time_zone(@context[:current_user].time_zone).to_s endend```Thisissimpletoimplementforaoneoffsituation,butnotforexampleifyouwanttoapplythesameformattingrulestoallDateTimefieldsinyoursystem.Anotherissueistheattributeontheresourcewillalwaysreturnaformattedresponse,whetheryouwantitor not.##### Value FormattersToovercometheabovelimitationsJRusesValueFormatters.ValueFormattersallowyoutocontrolthewayvaluesarehandledforanattribute.The`format`canbesetperattributeasitisdeclaredintheresource.Forexample:
```rubyclass PersonResource < JSONAPI::Resource attributes :name, :email, :spoken_languages attribute :last_login_time, format: :date_with_utc_timezone # Getter/Setter for spoken_languages ...end```AValueformatterhasa`format`andan`unformat`method.Here's the base ValueFormatter and DefaultValueFormatter forreference:```rubymodule JSONAPI class ValueFormatter < Formatter class << self def format(raw_value) super(raw_value) end def unformat(value) super(value) end ... end endendclass DefaultValueFormatter < JSONAPI::ValueFormatter class << self def format(raw_value) case raw_value when String, Integer return raw_value else return raw_value.to_s end end endend```You can also create your own Value Formatter. Value Formatters must be named with the `format` name followed by`ValueFormatter`, i.e. `DateWithUTCTimezoneValueFormatter` and derive from `JSONAPI::ValueFormatter`. It isrecommended that you create a directory for your formatters, called `formatters`.The `format` method is called by the `ResourceSerializer` as is serializing a resource. The format method takes the`raw_value` parameter. `raw_value` is the value as read from the model.The `unformat` method is called when processing the request. Each incoming attribute (except `links`) are run throughthe `unformat` method. The `unformat` method takes a `value`, which is the value as it comes in on therequest. This allows you process the incoming value to alter its state before it is stored in the model.###### Use a Different Default Value FormatterAnother way to handle formatting is to set a different default value formatter. This will affect all attributes that donot have a `format` set. You can do this by overriding the `default_attribute_options` method for a resource (or a baseresource for a system wide change).```ruby def default_attribute_options {format: :my_default} end```and```rubyclass MyDefaultValueFormatter < JSONAPI::ValueFormatter class << self def format(raw_value) case raw_value when String, Integer return raw_value when DateTime return raw_value.in_time_zone('UTC').to_s else return raw_value.to_s end end endend```This way all DateTime values will be formatted to display in the UTC timezone.#### Key FormatBy default JR uses dasherized keys as per the[JSON API naming recommendations](http://jsonapi.org/recommendations/#naming). This can be changed by specifying adifferent key formatter.For example, to use camel cased keys with an initial lowercase character (JSON'sdefault)createaninitializerandaddthefollowing:
```rubyJSONAPI.configure do |config| # built in key format options are :underscored_key, :camelized_key and :dasherized_key config.json_key_format = :camelized_keyend```Thiswillcausetheserializertousethe`CamelizedKeyFormatter`.Youcanalsocreateyourown`KeyFormatter`,forexample:
```rubyclass UpperCamelizedKeyFormatter < JSONAPI::KeyFormatter class << self def format(key) super.camelize(:upper) end endend```Youwouldspecifythisin`JSONAPI.configure`as`:upper_camelized`.### RoutingJRhasacoupleofhelpermethodsavailabletoassistyouwithsettinguproutes.##### `jsonapi_resources`Like`resources`in`ActionDispatch`,`jsonapi_resources`providesresourcefulroutesmappingbetweenHTTPverbsandURLsandcontrolleractions.ThiswillalsosetupmappingsforrelationshipURLsforaresource's relationships. For example:```rubyRails.application.routes.draw do jsonapi_resources :contacts jsonapi_resources :phone_numbersend```gives the following routes``` Prefix Verb URI Pattern Controller#Actioncontact_relationships_phone_numbers GET /contacts/:contact_id/relationships/phone-numbers(.:format) contacts#show_relationship {:relationship=>"phone_numbers"} POST /contacts/:contact_id/relationships/phone-numbers(.:format) contacts#create_relationship {:relationship=>"phone_numbers"} DELETE /contacts/:contact_id/relationships/phone-numbers/:keys(.:format) contacts#destroy_relationship {:relationship=>"phone_numbers"} contact_phone_numbers GET /contacts/:contact_id/phone-numbers(.:format) phone_numbers#get_related_resources {:relationship=>"phone_numbers", :source=>"contacts"} contacts GET /contacts(.:format) contacts#index POST /contacts(.:format) contacts#create contact GET /contacts/:id(.:format) contacts#show PATCH /contacts/:id(.:format) contacts#update PUT /contacts/:id(.:format) contacts#update DELETE /contacts/:id(.:format) contacts#destroy phone_number_relationships_contact GET /phone-numbers/:phone_number_id/relationships/contact(.:format) phone_numbers#show_relationship {:relationship=>"contact"} PUT|PATCH /phone-numbers/:phone_number_id/relationships/contact(.:format) phone_numbers#update_relationship {:relationship=>"contact"} DELETE /phone-numbers/:phone_number_id/relationships/contact(.:format) phone_numbers#destroy_relationship {:relationship=>"contact"} phone_number_contact GET /phone-numbers/:phone_number_id/contact(.:format) contacts#get_related_resource {:relationship=>"contact", :source=>"phone_numbers"} phone_numbers GET /phone-numbers(.:format) phone_numbers#index POST /phone-numbers(.:format) phone_numbers#create phone_number GET /phone-numbers/:id(.:format) phone_numbers#show PATCH /phone-numbers/:id(.:format) phone_numbers#update PUT /phone-numbers/:id(.:format) phone_numbers#update DELETE /phone-numbers/:id(.:format) phone_numbers#destroy```##### `jsonapi_resource`Like `jsonapi_resources`, but for resources you lookup without an id.#### Nested RoutesBy default nested routes are created for getting related resources and manipulating relationships. You can control thenested routes by passing a block into `jsonapi_resources` or `jsonapi_resource`. An empty block will not createany nested routes. For example:```rubyRails.application.routes.draw do jsonapi_resources :contacts do endend```gives routes that are only related to the primary resource, and none for its relationships:``` Prefix Verb URI Pattern Controller#Action contacts GET /contacts(.:format) contacts#index POST /contacts(.:format) contacts#create contact GET /contacts/:id(.:format) contacts#show PATCH /contacts/:id(.:format) contacts#update PUT /contacts/:id(.:format) contacts#update DELETE /contacts/:id(.:format) contacts#destroy```To manually add in the nested routes you can use the `jsonapi_links`, `jsonapi_related_resources` and`jsonapi_related_resource` inside the block. Or, you can add the default set of nested routes using the`jsonapi_relationships` method. For example:```rubyRails.application.routes.draw do jsonapi_resources :contacts do jsonapi_relationships endend```###### `jsonapi_links`You can add relationship routes in with `jsonapi_links`, for example:```rubyRails.application.routes.draw do jsonapi_resources :contacts do jsonapi_links :phone_numbers endend```Gives the following routes:```contact_relationships_phone_numbers GET /contacts/:contact_id/relationships/phone-numbers(.:format) contacts#show_relationship {:relationship=>"phone_numbers"} POST /contacts/:contact_id/relationships/phone-numbers(.:format) contacts#create_relationship {:relationship=>"phone_numbers"} DELETE /contacts/:contact_id/relationships/phone-numbers/:keys(.:format) contacts#destroy_relationship {:relationship=>"phone_numbers"} contacts GET /contacts(.:format) contacts#index POST /contacts(.:format) contacts#create contact GET /contacts/:id(.:format) contacts#show PATCH /contacts/:id(.:format) contacts#update PUT /contacts/:id(.:format) contacts#update DELETE /contacts/:id(.:format) contacts#destroy```The new routes allow you to show, create and destroy the relationships between resources.###### `jsonapi_related_resources`Creates a nested route to GET the related has_many resources. For example:```rubyRails.application.routes.draw do jsonapi_resources :contacts do jsonapi_related_resources :phone_numbers endend```gives the following routes:``` Prefix Verb URI Pattern Controller#Actioncontact_phone_numbers GET /contacts/:contact_id/phone-numbers(.:format) phone_numbers#get_related_resources {:relationship=>"phone_numbers", :source=>"contacts"} contacts GET /contacts(.:format) contacts#index POST /contacts(.:format) contacts#create contact GET /contacts/:id(.:format) contacts#show PATCH /contacts/:id(.:format) contacts#update PUT /contacts/:id(.:format) contacts#update DELETE /contacts/:id(.:format) contacts#destroy```A single additional route was created to allow you GET the phone numbers through the contact.###### `jsonapi_related_resource`Like `jsonapi_related_resources`, but for has_one related resources.```rubyRails.application.routes.draw do jsonapi_resources :phone_numbers do jsonapi_related_resource :contact endend```gives the following routes:``` Prefix Verb URI Pattern Controller#Actionphone_number_contact GET /phone-numbers/:phone_number_id/contact(.:format) contacts#get_related_resource {:relationship=>"contact", :source=>"phone_numbers"} phone_numbers GET /phone-numbers(.:format) phone_numbers#index POST /phone-numbers(.:format) phone_numbers#create phone_number GET /phone-numbers/:id(.:format) phone_numbers#show PATCH /phone-numbers/:id(.:format) phone_numbers#update PUT /phone-numbers/:id(.:format) phone_numbers#update DELETE /phone-numbers/:id(.:format) phone_numbers#destroy```## ConfigurationJR has a few configuration options. Some have already been mentioned above. To set configuration options create aninitializer and add the options you wish to set. All options have defaults, so you only need to set the options thatare different. The default options are shown below.If using custom classes (such as a CustomPaginator), be sure to require them at the top of the initializer before usage.```rubyJSONAPI.configure do |config| #:underscored_key, :camelized_key, :dasherized_key, or custom config.json_key_format = :dasherized_key #:underscored_route, :camelized_route, :dasherized_route, or custom config.route_format = :dasherized_route # Default Processor, used if a resource specific one is not defined. # Must be a class config.default_processor_klass = JSONAPI::Processor #:integer, :uuid, :string, or custom (provide a proc) config.resource_key_type = :integer # optional request features config.allow_include = true config.allow_sort = true config.allow_filter = true # How to handle unsupported attributes and relationships which are provided in the request # true => raises an error # false => allows the request to continue. A warning is included in the response meta data indicating # the fields which were ignored. This is useful for client libraries which send extra parameters. config.raise_if_parameters_not_allowed = true # :none, :offset, :paged, or a custom paginator name config.default_paginator = :none # Output pagination links at top level config.top_level_links_include_pagination = true config.default_page_size = 10 config.maximum_page_size = 20 # Output the record count in top level meta data for find operations config.top_level_meta_include_record_count = false config.top_level_meta_record_count_key = :record_count # For :paged paginators, the following are also available config.top_level_meta_include_page_count = false config.top_level_meta_page_count_key = :page_count config.use_text_errors = false # List of classes that should not be rescued by the operations processor. # For example, if you use Pundit for authorization, you might # raise a Pundit::NotAuthorizedError at some point during operations # processing. If you want to use Rails'`rescue_from`macroto# catch this error and render a 403 status code, you should add# the `Pundit::NotAuthorizedError` to the `exception_class_whitelist`.# Subclasses of the whitelisted classes will also be whitelisted.config.exception_class_whitelist=[]# Resource Linkage# Controls the serialization of resource linkage for non compound documents# NOTE: always_include_to_many_linkage_data is not currently implementedconfig.always_include_to_one_linkage_data=falseend```## Contributing1. Fork it ( http://github.com/cerebris/jsonapi-resources/fork )2. Create your feature branch (`gitcheckout -bmy-new-feature`)3. Commit your changes (`gitcommit -am'Add some feature'`)4. Push to the branch (`gitpushoriginmy-new-feature`)5.CreateanewPullRequest## LicenseCopyright2014-2016CerebrisCorporation.MITLicense(seeLICENSEfordetails).

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages