Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
tiagopog committed Jul 16, 2017
2 parents 2989b0e + fb3bae6 commit 9604c35
Show file tree
Hide file tree
Showing 12 changed files with 501 additions and 207 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,3 @@
.byebug_history
test_db
Gemfile.lock

14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ gem 'jsonapi-utils', '~> 0.4.9'
For Rails 5:

```ruby
gem 'jsonapi-utils', '~> 0.6.0.beta'
gem 'jsonapi-utils', '~> 0.7.0'
```

And then execute:
Expand Down Expand Up @@ -110,8 +110,8 @@ end

Arguments:

- `json`: object to be rendered as a JSON document: ActiveRecord object, Hash or Array of Hashes;
- `status`: HTTP status code (Integer or Symbol). If ommited a status code will be automatically infered;
- `json`: object to be rendered as a JSON document: ActiveRecord object, Hash or Array<Hash>;
- `status`: HTTP status code (Integer, String or Symbol). If ommited a status code will be automatically infered;
- `options`:
- `resource`: explicitly points the resource to be used in the serialization. By default, JU will select resources by inferencing from controller's name.
- `count`: explicitly points the total count of records for the request in order to build a proper pagination. By default, JU will count the total number of records.
Expand Down Expand Up @@ -155,16 +155,16 @@ It renders a JSON API-compliant error response.

Arguments:
- Exception
- `json`: object to be rendered as a JSON document: ActiveRecord, Exception, Array of Hashes or any object which implements the `errors` method;
- `status`: HTTP status code (Integer or Symbol). If ommited a status code will be automatically infered from the error body.
- `json`: object to be rendered as a JSON document: ActiveRecord, Exception, Array<Hash> or any object which implements the `errors` method;
- `status`: HTTP status code (Integer, String or Symbol). If ommited a status code will be automatically infered from the error body.

Other examples:

```ruby
# Render errors from a custom exception:
jsonapi_render_errors Exceptions::MyCustomError.new(user)

# Render errors from an Array of Hashes:
# Render errors from an Array<Hash>:
errors = [{ id: 'validation', title: 'Something went wrong', code: '100' }]
jsonapi_render_errors json: errors, status: :unprocessable_entity
```
Expand All @@ -188,7 +188,7 @@ end
```

Arguments:
- First: ActiveRecord object, Hash or Array of Hashes;
- First: ActiveRecord object, Hash or Array<Hash>;
- Last: Hash of options (same as `JSONAPI::Utils#jsonapi_render`).

#### Paginators
Expand Down
128 changes: 2 additions & 126 deletions lib/jsonapi/utils/exceptions.rb
Original file line number Diff line number Diff line change
@@ -1,126 +1,2 @@
require 'jsonapi/utils/version'

module JSONAPI
module Utils
module Exceptions
class ActiveRecord < ::JSONAPI::Exceptions::Error
attr_reader :object, :resource, :relationships, :relationship_keys, :foreign_keys

def initialize(object, resource_klass, context)
@object = object
@resource = resource_klass.new(object, context)

# Need to reflect on resource's relationships for error reporting.
@relationships = resource_klass._relationships.values
@relationship_keys = @relationships.map(&:name).map(&:to_sym)
@foreign_keys = @relationships.map(&:foreign_key).map(&:to_sym)
end

def errors
object.errors.messages.flat_map do |key, messages|
messages.map do |message|
error_meta = error_base
.merge(title: title_member(key, message))
.merge(id: id_member(key))
.merge(source_member(key))

JSONAPI::Error.new(error_meta)
end
end
end

private

def id_member(key)
id = resource_key_for(key)
key_formatter = JSONAPI.configuration.key_formatter
key_formatter.format(id).to_sym
end

# Determine if this is a foreign key, which will need to look up its
# matching association name.
def resource_key_for(key)
if foreign_keys.include?(key)
relationships.find { |r| r.foreign_key == key }.name.to_sym
else
key
end
end

def source_member(key)
Hash.new.tap do |hash|
resource_key = resource_key_for(key)

# Pointer should only be created for whitelisted attributes.
return hash unless resource.fetchable_fields.include?(resource_key) || key == :base

id = id_member(key)

hash[:source] = {}
hash[:source][:pointer] =
# Relationship
if relationship_keys.include?(resource_key)
"/data/relationships/#{id}"
# Base
elsif key == :base
'/data'
# Attribute
else
"/data/attributes/#{id}"
end
end
end

def title_member(key, message)
if key == :base
message
else
resource_key = resource_key_for(key)
[translation_for(resource_key), message].join(' ')
end
end

def translation_for(key)
object.class.human_attribute_name(key)
end

def error_base
{
code: JSONAPI::VALIDATION_ERROR,
status: :unprocessable_entity
}
end
end

class BadRequest < ::JSONAPI::Exceptions::Error
def code
'400'
end

def errors
[JSONAPI::Error.new(
code: code,
status: :bad_request,
title: 'Bad Request',
detail: 'This request is not supported.'
)]
end
end

class InternalServerError < ::JSONAPI::Exceptions::Error
def code
'500'
end

def errors
[JSONAPI::Error.new(
code: code,
status: :internal_server_error,
title: 'Internal Server Error',
detail: 'An internal error ocurred while processing the request.'
)]
end
end
end
end
end
require 'jsonapi/utils/exceptions/active_record'
require 'jsonapi/utils/exceptions/internal_server_error'
197 changes: 197 additions & 0 deletions lib/jsonapi/utils/exceptions/active_record.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
module JSONAPI
module Utils
module Exceptions
class ActiveRecord < ::JSONAPI::Exceptions::Error
attr_reader :object, :resource, :relationships, :relationship_names, :foreign_keys

# Construct an error decorator over ActiveRecord objects.
#
# @param object [ActiveRecord::Base] Invalid ActiveRecord object.
# e.g.: User.new(name: nil).tap(&:save)
#
# @param resource_klass [JSONAPI::Resource] Resource class to be used for reflection.
# e.g.: UserResuource
#
# @return [JSONAPI::Utils::Exceptions::ActiveRecord]
#
# @api public
def initialize(object, resource_klass, context)
@object = object
@resource = resource_klass.new(object, context)

# Need to reflect on resource's relationships for error reporting.
@relationships = resource_klass._relationships.values
@relationship_names = @relationships.map(&:name).map(&:to_sym)
@foreign_keys = @relationships.map(&:foreign_key).map(&:to_sym)
@resource_key_for = {}
@formatted_key = {}
end

# Decorate errors for AR invalid objects.
#
# @note That's the method used by formatters to build the response's error body.
#
# @return [Array]
#
# @api public
def errors
object.errors.messages.flat_map do |field, messages|
messages.map.with_index do |message, index|
build_error(field, message, index)
end
end
end

private

# Turn AR error into JSONAPI::Error.
#
# @param field [Symbol] Name of the invalid field
# e.g.: :title
#
# @param message [String] Error message
# e.g.: "can't be blank"
#
# @param index [Integer] Index of the error detail
#
# @return [JSONAPI::Error]
#
# @api private
def build_error(field, message, index = 0)
error = error_base
.merge(
id: id_member(field, index),
title: message,
detail: detail_member(field, message)
).merge(source_member(field))
JSONAPI::Error.new(error)
end

# Build the "id" member value for the JSON API error object.
# e.g.: for :first_name, :too_short => "first-name#too-short"
#
# @note The returned value depends on the key formatter type defined
# via configuration, e.g.: config.json_key_format = :dasherized_key
#
# @param field [Symbol] Name of the invalid field
# e.g.: :first_name
#
# @param index [Integer] Index of the error detail
#
# @return [String]
#
# @api private
def id_member(field, index)
[
key_format(field),
key_format(
object.errors.details
.dig(field, index, :error)
.to_s.downcase
.split
.join('_')
)
].join('#')
end

# Bring the formatted resource key for a given field.
# e.g.: for :first_name => :"first-name"
#
# @note The returned value depends on the key formatter type defined
# via configuration, e.g.: config.json_key_format = :dasherized_key
#
# @param field [Symbol] Name of the invalid field
# e.g.: :title
#
# @return [Symbol]
#
# @api private
def key_format(field)
@formatted_key[field] ||= JSONAPI.configuration
.key_formatter
.format(resource_key_for(field))
.to_sym
end

# Build the "source" member value for the JSON API error object.
# e.g.: :title => "/data/attributes/title"
#
# @param field [Symbol] Name of the invalid field
# e.g.: :title
#
# @return [Hash]
#
# @api private
def source_member(field)
resource_key = resource_key_for(field)
return {} unless field == :base || resource.fetchable_fields.include?(resource_key)
id = key_format(field)

pointer =
if field == :base then '/data'
elsif relationship_names.include?(resource_key) then "/data/relationships/#{id}"
else "/data/attributes/#{id}"
end

{ source: { pointer: pointer } }
end

# Build the "detail" member value for the JSON API error object.
# e.g.: :first_name, "can't be blank" => "First name can't be blank"
#
# @param field [Symbol] Name of the invalid field
# e.g.: :first_name
#
# @return [String]
#
# @api private
def detail_member(field, message)
return message if field == :base
resource_key = resource_key_for(field)
[translation_for(resource_key), message].join(' ')
end

# Return the resource's attribute or relationship key name for a given field name.
# e.g.: :title => :title, :user_id => :author
#
# @param field [Symbol] Name of the invalid field
# e.g.: :title
#
# @return [Symbol]
#
# @api private
def resource_key_for(field)
@resource_key_for[field] ||= begin
return field unless foreign_keys.include?(field)
relationships.find { |r| r.foreign_key == field }.name.to_sym
end
end

# Turn the field name into human-friendly one.
# e.g.: :first_name => "First name"
#
# @param field [Symbol] Name of the invalid field
# e.g.: :first_name
#
# @return [String]
#
# @api private
def translation_for(field)
object.class.human_attribute_name(field)
end

# Return the base data used for all errors of this kind.
#
# @return [Hash]
#
# @api private
def error_base
{
code: JSONAPI::VALIDATION_ERROR,
status: :unprocessable_entity
}
end
end
end
end
end
Loading

0 comments on commit 9604c35

Please sign in to comment.