Skip to content

Commit

Permalink
Merge pull request #32 from EmCousin/enhancement/enforce-JSONAPI-comp…
Browse files Browse the repository at this point in the history
…liant-response-format

Enhancement/enforce jsonapi compliant response format
  • Loading branch information
EmCousin committed Jan 25, 2022
2 parents bace927 + 500d52b commit fa81e85
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 61 deletions.
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
## Changelog

### v1.0.1 (next)
### v1.0.2 (next)

* Your contribution here.

### v1.0.1 (January 25, 2022)

[#32](https://github.com/EmCousin/grape-jsonapi/pull/32) - [@EmCousin](https://github.com/EmCousin)

* The gem now forces API response to have a JSONAPI compliant format, even for objects that are not being serialized via a `JSONAPI::Serializer`
* You can now customize the `meta` and `links` properties of your response at rendering time, without having to rely on your serializers (check README.md for more information)
* Changed the response's data structure when the object is a heterogeneous collection (a list of objects of different classes), to make it JSONAPI compliant.
* Fixed a defect that was causing empty hashes to be rendered as empty arrays

### v1.0.0 (November 21, 2020)

[#14](https://github.com/EmCousin/grape_fast_jsonapi/pull/14) - [@EmCousin](https://github.com/EmCousin)
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ get "/" do
end
```

### Override `meta`and `links` properties

`meta` and `links` properties are usually defined per resource within your serializer ([here](https://github.com/jsonapi-serializer/jsonapi-serializer#meta-per-resource) and [here](https://github.com/jsonapi-serializer/jsonapi-serializer#links-per-object))

However, if you need to override those properties, you can pass them as options when rendering your response:
```ruby
user = User.find("123")
render user, meta: { pagination: { page: 1, total: 42 } }, links: { self: 'https://my-awesome.app.com/users/1' }
```

### Model parser for response documentation

When using Grape with Swagger via [grape-swagger](https://github.com/ruby-grape/grape-swagger), you can generate response documentation automatically via the provided following model parser:
Expand Down
40 changes: 24 additions & 16 deletions lib/grape_jsonapi/formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ module Formatter
module Jsonapi
class << self
def call(object, env)
return object if object.is_a?(String)
return ::Grape::Json.dump(serialize(object, env)) if serializable?(object)
return object.to_json if object.respond_to?(:to_json)

::Grape::Json.dump(object)
response = serializable?(object) ? serialize(object, env) : { data: object }
::Grape::Json.dump(
response.merge(env.slice('meta', 'links'))
)
end

private
Expand All @@ -25,17 +24,17 @@ def serializable?(object)
def serialize(object, env)
if object.respond_to?(:serializable_hash)
serializable_object(object, jsonapi_options(env)).serializable_hash
elsif serializable_collection?(object)
serializable_collection(object, jsonapi_options(env))
elsif object.is_a?(Hash)
serialize_each_pair(object, env)
elsif serializable_collection?(object)
serializable_collection(object, env)
else
object
end
end

def serializable_collection?(object)
object.respond_to?(:to_a) && object.all? do |o|
!object.nil? && object.respond_to?(:to_a) && object.any? && object.all? do |o|
o.respond_to?(:serializable_hash)
end
end
Expand All @@ -48,22 +47,24 @@ def jsonapi_serializable(object, options)
serializable_class(object, options)&.new(object, options)
end

def serializable_collection(collection, options)
def serializable_collection(collection, env)
if heterogeneous_collection?(collection)
collection.map do |o|
serialize_resource(o, options)
collection.each_with_object({ data: [] }) do |o, hash|
hash[:data].push(serialize_resource(o, env)[:data])
end
else
serialize_resource(collection, options)
serialize_resource(collection, env)
end
end

def heterogeneous_collection?(collection)
collection.map { |item| item.class.name }.uniq.many?
end

def serialize_resource(resource, options)
jsonapi_serializable(resource, options)&.serializable_hash || resource.map(&:serializable_hash)
def serialize_resource(resource, env)
jsonapi_serializable(resource, jsonapi_options(env))&.serializable_hash || resource.map do |item|
serialize(item, env)
end
end

def serializable_class(object, options)
Expand All @@ -78,8 +79,15 @@ def serializable_class(object, options)
end

def serialize_each_pair(object, env)
h = {}
object.each_pair { |k, v| h[k] = serialize(v, env) }
h = { data: {} }
object.each_pair do |k, v|
serialized_value = serialize(v, env)
h[:data][k] = if serialized_value.is_a?(Hash) && serialized_value[:data]
serialized_value[:data]
else
serialized_value
end
end
h
end

Expand Down
2 changes: 1 addition & 1 deletion lib/grape_jsonapi/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

module Grape
module Jsonapi
VERSION = '1.0.0'
VERSION = '1.0.1'
end
end
113 changes: 71 additions & 42 deletions spec/lib/grape_jsonapi/formatter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@
describe '.call' do
subject { described_class.call(object, env) }
let(:jsonapi_serializer_options) { nil }
let(:env) { { 'jsonapi_serializer_options' => jsonapi_serializer_options } }
let(:meta) { { pagination: { page: 1, total: 2 } } }
let(:links) { { self: 'https://example/org' } }
let(:env) { { 'jsonapi_serializer_options' => jsonapi_serializer_options, 'meta' => meta, 'links' => links } }

context 'when the object is a string' do
let(:object) { 'I am a string' }
let(:response) { ::Grape::Json.dump({ data: object, meta: meta, links: links }) }

it { is_expected.to eq object }
it { is_expected.to eq response }
end

context 'when the object is serializable' do
Expand All @@ -33,76 +36,102 @@

context 'when the object has a model_name defined' do
let(:object) { admin }
it { is_expected.to eq ::Grape::Json.dump(user_serializer.serializable_hash) }
let(:response) { ::Grape::Json.dump(user_serializer.serializable_hash.merge(meta: meta, links: links)) }

it { is_expected.to eq response }
end

context 'when the object is a active serializable model instance' do
let(:object) { user }
let(:response) { ::Grape::Json.dump(user_serializer.serializable_hash.merge(meta: meta, links: links)) }

it { is_expected.to eq ::Grape::Json.dump(user_serializer.serializable_hash) }
it { is_expected.to eq response }
end

context 'when the object is an array of active serializable model instances' do
let(:object) { [user, another_user] }

it { is_expected.to eq ::Grape::Json.dump(user_serializer.serializable_hash) }
end
context 'when the object is an array' do
context 'when the object is an array of active serializable model instances' do
let(:object) { [user, another_user] }
let(:response) { ::Grape::Json.dump(user_serializer.serializable_hash.merge(meta: meta, links: links)) }

context 'when the array contains instances of different models' do
let(:object) { [user, blog_post] }
it { is_expected.to eq response }
end

it 'returns an array of jsonapi serialialized objects' do
expect(subject).to eq(::Grape::Json.dump([
UserSerializer.new(user, {}).serializable_hash,
BlogPostSerializer.new(blog_post, {}).serializable_hash
]))
context 'when the array contains instances of different models' do
let(:object) { [user, blog_post] }
let(:response) do
::Grape::Json.dump({
data: [
UserSerializer.new(user, {}).serializable_hash[:data],
BlogPostSerializer.new(blog_post, {}).serializable_hash[:data]
],
meta: meta,
links: links
})
end

it 'returns an array of jsonapi serialialized objects' do
expect(subject).to eq response
end
end
end

context 'when the object is an empty array ' do
let(:object) { [] }
context 'when the object is an empty array' do
let(:object) { [] }

it { is_expected.to eq ::Grape::Json.dump(object) }
end
it { is_expected.to eq({ data: [], meta: meta, links: links }.to_json) }
end

context 'when the object is an array of null objects ' do
let(:object) { [nil, nil] }
context 'when the object is an array of null objects' do
let(:object) { [nil, nil] }

it { is_expected.to eq ::Grape::Json.dump(object) }
it { is_expected.to eq({ data: [nil, nil], meta: meta, links: links }.to_json) }
end
end

context 'when the object is a Hash of plain values' do
let(:object) { user.as_json }
context 'when the object is a hash' do
context 'when the object is an empty hash' do
let(:object) { {} }

it { is_expected.to eq ::Grape::Json.dump(object) }
end
it { is_expected.to eq({ data: {}, meta: meta, links: links }.to_json) }
end

context 'when the object is a Hash with serializable object values' do
let(:object) do
{
user: user,
blog_post: blog_post
}
context 'when the object is a Hash of plain values' do
let(:object) { user.as_json }

it { is_expected.to eq ::Grape::Json.dump({ data: user.as_json, meta: meta, links: links }) }
end

it 'returns an hash of with jsonapi serialialized objects values' do
expect(subject).to eq(::Grape::Json.dump({
user: UserSerializer.new(user, {}).serializable_hash,
blog_post: BlogPostSerializer.new(blog_post, {}).serializable_hash
}))
context 'when the object is a Hash with serializable object values' do
let(:object) do
{ user: user, blog_post: blog_post }
end

let(:response) do
::Grape::Json.dump({
data: {
user: UserSerializer.new(user, {}).serializable_hash[:data],
blog_post: BlogPostSerializer.new(blog_post, {}).serializable_hash[:data]
},
meta: meta,
links: links
})
end

it 'returns an hash of with jsonapi serialialized objects values' do
expect(subject).to eq response
end
end
end

context 'when the object is nil' do
let(:object) { nil }

it { is_expected.to eq 'null' }
it { is_expected.to eq({ data: nil, meta: meta, links: links }.to_json) }
end

context 'when the object is a number' do
let(:object) { 42 }

it { is_expected.to eq '42' }
it { is_expected.to eq({ data: 42, meta: meta, links: links }.to_json) }
end

context 'when a custom serializer is passed as an option' do
Expand All @@ -113,7 +142,7 @@
}
end

it { is_expected.to eq ::Grape::Json.dump(another_user_serializer.serializable_hash) }
it { is_expected.to eq ::Grape::Json.dump(another_user_serializer.serializable_hash.merge(meta: meta, links: links)) }
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/lib/grape_jsonapi/version_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

describe Grape::Jsonapi::VERSION do
it { is_expected.to eq '1.0.0'.freeze }
it { is_expected.to eq '1.0.1'.freeze }
end

0 comments on commit fa81e85

Please sign in to comment.