module Shamu::JsonApi::Rails::Controller

Add support for writing resources as well-formed JSON API.

Constants

ID_PATTERN

Pattern to identify request params that hold 'ids'

JSON_CONTEXT_KEYWORDS

Private Instance Methods

annotate_json_error( error, builder ) click to toggle source

@!visibility public

Annotate an exception that is being rendered to the browser - for example to add current user or security information if available.

# File lib/shamu/json_api/rails/controller.rb, line 200
def annotate_json_error( error, builder )
  if ::Rails.env.development?
    builder.meta :type, error.class.to_s
    builder.meta :backtrace, error.backtrace
  end
end
build_json_response( context ) click to toggle source
# File lib/shamu/json_api/rails/controller.rb, line 342
def build_json_response( context )
  Shamu::JsonApi::Response.new( json_context( **context.slice( *JSON_CONTEXT_KEYWORDS ) ) )
end
json_collection( resources, presenter = nil, pagination: :auto, **context ) { |response| ... } click to toggle source

Builds a well-formed JSON API response for a collection of resources.

@param [Enumerable<Object>] resources to present as a JSON array. @param [Class] presenter {Presenter} class to use when building the

response for each of the resources. If not given, attempts to find
a presenter by calling {Context#find_presenter}

@param (see json_context) @yield (response) write additional top-level links and meta

information.

@yieldparam [JsonApi::Response] response @return [JsonApi::Response] the presented JSON response.

# File lib/shamu/json_api/rails/controller.rb, line 101
def json_collection( resources, presenter = nil, pagination: :auto, **context, &block )
  response = build_json_response( context )
  response.collection resources, presenter
  json_paginate_resources response, resources, pagination
  yield response if block_given?
  response.as_json
end
json_context( fields: :not_set, namespaces: :not_set, presenters: :not_set ) click to toggle source

@!visibility public

Build a {JsonApi::Context} for the current request and controller.

@param [Hash<Symbol,Array>] fields to include in the response. If not

provided looks for a `fields` request argument and parses that.
See {JsonApi::Context#initialize}.

@param [Array<String>] namespaces to look for {Presenter presenters}.

If not provided automatically adds the controller name and it's
namespace.

For example in the `Users::AccountController` it will add the
`Users::Accounts` and `Users` namespaces.

See {JsonApi::Context#find_presenter}.

@param [Hash<Class,Class>] presenters a hash that maps resource classes

to the presenter class to use when building responses. See
{JsonApi::Context#find_presenter}.

@return [JsonApi::Context] the builder context honoring any filter

parameters sent by the client.
# File lib/shamu/json_api/rails/controller.rb, line 230
def json_context( fields: :not_set, namespaces: :not_set, presenters: :not_set )
  Shamu::JsonApi::Context.new \
    fields: fields == :not_set ? json_context_fields : fields,
    namespaces: namespaces == :not_set ? json_context_namespaces : namespaces,
    presenters: presenters == :not_set ? json_context_presenters : presenters
end
json_context_fields() click to toggle source
# File lib/shamu/json_api/rails/controller.rb, line 293
def json_context_fields
  params[:fields]
end
json_context_namespaces() click to toggle source
# File lib/shamu/json_api/rails/controller.rb, line 297
def json_context_namespaces
  name = self.class.name.sub( /Controller$/, "" )
  namespaces = [ name.pluralize ]
  loop do
    name = name.deconstantize
    break if name.blank?

    namespaces << name
  end

  namespaces
end
json_context_presenters() click to toggle source
# File lib/shamu/json_api/rails/controller.rb, line 310
def json_context_presenters
end
json_error( error = nil, **context ) { |builder| ... } click to toggle source

@!visibility public

Write an error response. See {Shamu::JsonApi::Response#error} for details.

@param (see Shamu::JsonApi::Response#error) @yield (builder) @yieldparam [Shamu::JsonApi::ErrorBuilder] builder to customize the

error response.

@return [JsonApi::Response] the presented JSON response.

# File lib/shamu/json_api/rails/controller.rb, line 180
def json_error( error = nil, **context, &block )
  response = build_json_response( context )

  response.error error do |builder|
    builder.http_status json_http_status_code_from_error( error )
    annotate_json_error( error, builder )
    yield builder if block_given?
  end

  response.to_json
end
json_http_status_code_from_error( error ) click to toggle source
# File lib/shamu/json_api/rails/controller.rb, line 320
def json_http_status_code_from_error( error )
  case error
  when ActiveRecord::RecordNotFound then :not_found
  when ActiveRecord::RecordInvalid  then :unprocessable_entity
  when /AccessDenied/               then :forbidden
  else
    if error.is_a?( Exception )
      ActionDispatch::ExceptionWrapper.status_code_for_exception( error )
    else
      :bad_request
    end
  end
end
json_http_status_code_from_request() click to toggle source
# File lib/shamu/json_api/rails/controller.rb, line 334
def json_http_status_code_from_request
  case request.method
  when "POST"  then :created
  when "HEAD"  then :no_content
  else              :ok
  end
end
json_page_parameter( page_param_name, param, value ) click to toggle source
# File lib/shamu/json_api/rails/controller.rb, line 148
def json_page_parameter( page_param_name, param, value )
  params = self.params
  params = params.to_unsafe_hash if params.respond_to?( :to_unsafe_hash )

  page_params = params.reverse_merge page_param_name => {}
  page_params[page_param_name][param] = value

  page_params
end
json_paginate( resources, builder, param: :page ) click to toggle source

@!visibility public

Add page-based pagination links for the resources to the builder.

@param [#current_page,#next_page,#previous_page] resources a collection that responds to `#current_page` @param [JsonApi::BaseBuilder] builder to add links to. @param [String] param the name of the key page parameter to adjust @return [void]

# File lib/shamu/json_api/rails/controller.rb, line 136
def json_paginate( resources, builder, param: :page )
  page = resources.current_page

  if resources.respond_to?( :next_page ) ? resources.next_page : true
    builder.link :next, url_for( json_page_parameter( param, :number, page + 1 ) )
  end

  if resources.respond_to?( :prev_page ) ? resources.prev_page : page > 1
    builder.link :prev, url_for( json_page_parameter( param, :number, page - 1 ) )
  end
end
json_paginate_resources( response, resources, pagination ) click to toggle source
# File lib/shamu/json_api/rails/controller.rb, line 313
def json_paginate_resources( response, resources, pagination )
  pagination = resources.respond_to?( :current_page ) if pagination == :auto
  return unless pagination

  json_paginate resources, response
end
json_pagination( param: :page ) click to toggle source

@!visibility public

Get the pagination request parameters.

@param [Symbol] param the request parameter to read pagination

options from.

@return [Pagination] the pagination state

# File lib/shamu/json_api/rails/controller.rb, line 165
def json_pagination( param: :page )
  page_params = params[ param ] || {}

  Pagination.new( page_params.merge( param: param ) )
end
json_request_payload() click to toggle source

@!visibility public

Map a JSON body to a hash. @return [Hash] the parsed JSON payload.

# File lib/shamu/json_api/rails/controller.rb, line 279
def json_request_payload
  @json_request_payload ||=
    begin
      body = request.body.read || "{}"
      json = JSON.parse( body, symbolize_names: true )

      unless json.blank?
        fail NoJsonBodyError unless json[ :data ]
      end

      json ? json[ :data ] : {}
    end
end
json_resource( resource, presenter = nil, **context ) { |response| ... } click to toggle source

@!visibility public

Builds a well-formed JSON API response for a single resource.

@param [Object] resource to present as JSON. @param [Class] presenter {Presenter} class to use when building the

response for the given resource. If not given, attempts to find a
presenter by calling {Context#find_presenter}.

@param (see json_context) @yield (response) write additional top-level links and meta

information.

@yieldparam [JsonApi::Response] response @return [JsonApi::Response] the presented JSON response.

# File lib/shamu/json_api/rails/controller.rb, line 38
def json_resource( resource, presenter = nil, **context, &block )
  response = build_json_response( context )
  response.resource resource, presenter
  yield response if block_given?
  response.as_json
end
json_validation_errors( errors, **context, &block ) click to toggle source

Write all the validation errors from a record to the response.

@param (see Shamu::JsonApi::Response#validation_errors) @yield (builder, attr, message) @yieldparam (see Shamu::JsonApi::Response#validation_errors) @return [JsonApi::Response] the presented JSON response.

# File lib/shamu/json_api/rails/controller.rb, line 121
def json_validation_errors( errors, **context, &block )
  response = build_json_response( context )
  response.validation_errors errors, &block

  response.as_json
end
map_json_resource_payload( resource ) click to toggle source
# File lib/shamu/json_api/rails/controller.rb, line 252
def map_json_resource_payload( resource ) # rubocop:disable Metrics/AbcSize
  payload = resource[ :attributes ] ? resource[ :attributes ].dup : {}
  payload[ :id ] = resource[ :id ] if resource.key?( :id )

  if relationships = resource[ :relationships ]
    relationships.each do |key, value|
      attr_key = "#{ key.to_s.singularize }_id"

      if value[ :data ].is_a?( Array )
        attr_key += "s" if value[ :data ].is_a?( Array )

        payload[ attr_key.to_sym ] = value[ :data ].map { |d| d[ :id ] }
        payload[ key ] = value[ :data ].map { |d| map_json_resource_payload( d ) }
      else
        payload[ attr_key.to_sym ] = value[ :data ][ :id ]
        payload[ key ] = map_json_resource_payload( value[ :data ] )
      end
    end
  end

  payload
end
render_collection( resources, presenter: nil, pagination: :auto, **context, &block ) click to toggle source

Present the resources as json and render it adding appropriate HTTP response codes and headers.

# File lib/shamu/json_api/rails/controller.rb, line 111
def render_collection( resources, presenter: nil, pagination: :auto, **context, &block )
  render json: json_collection( resources, presenter, pagination: pagination, **context, &block )
end
render_resource( resource, presenter: nil, status: nil, location: nil, **context, &block ) click to toggle source

@!visibility public

Present the `resource` as json and render it adding appropriate HTTP response codes and headers for standard JSON API actions.

@param [Symbol,Number] status the HTTP status code. @param (see json_resource)

# File lib/shamu/json_api/rails/controller.rb, line 52
def render_resource( resource, presenter: nil, status: nil, location: nil, **context, &block )
  json = json_resource( resource, presenter, **context, &block )

  # Include canonical url to resource if present
  if data = json[ "data" ]
    if links = data[ "links" ]
      location ||= links[ "self" ] if links[ "self" ]
    end
  end

  render json: json, status: status, location: location
end
render_result( result, presenter: nil, status: nil, **context, &block ) click to toggle source

@!visibility public

Renders a {Shamu::Services::Result} presenting either the validation errors or the entity.

@param [Shamu::Services::Result] result of a service call @param (see json_resource)

# File lib/shamu/json_api/rails/controller.rb, line 72
def render_result( result, presenter: nil, status: nil, **context, &block )
  if result.valid?
    if result.entity
      status ||= case request.method
                 when "POST"   then :created
                 when "DELETE" then :no_content
                 else               :ok
                 end

      render_resource result.entity, presenter: presenter, status: status, **context, &block
    else
      head status || :no_content
    end
  else
    render json: json_validation_errors( result.errors, **context ), status: :unprocessable_entity
  end
end
render_unhandled_exception( exception ) click to toggle source
# File lib/shamu/json_api/rails/controller.rb, line 192
def render_unhandled_exception( exception )
  render json: json_error( exception ), status: :internal_server_error
end
request_params( param_key ) click to toggle source

See (Shamu::Rails::Entity#request_params)

# File lib/shamu/json_api/rails/controller.rb, line 238
def request_params( param_key )
  if relationships = json_request_payload[ :relationships ]
    return map_json_resource_payload( relationships[ param_key ][ :data ] ) if relationships.key?( param_key )
  end

  payload = map_json_resource_payload( json_request_payload )

  request.params.each do |key, value|
    payload[ key.to_sym ] ||= value if ID_PATTERN =~ key
  end

  payload
end