Skip to content

Commit fa81e85

Browse files
authored
Merge pull request #32 from EmCousin/enhancement/enforce-JSONAPI-compliant-response-format
Enhancement/enforce jsonapi compliant response format
2 parents bace927 + 500d52b commit fa81e85

File tree

6 files changed

+117
-61
lines changed

6 files changed

+117
-61
lines changed

CHANGELOG.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
## Changelog
22

3-
### v1.0.1 (next)
3+
### v1.0.2 (next)
44

55
* Your contribution here.
66

7+
### v1.0.1 (January 25, 2022)
8+
9+
[#32](https://github.com/EmCousin/grape-jsonapi/pull/32) - [@EmCousin](https://github.com/EmCousin)
10+
11+
* The gem now forces API response to have a JSONAPI compliant format, even for objects that are not being serialized via a `JSONAPI::Serializer`
12+
* 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)
13+
* 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.
14+
* Fixed a defect that was causing empty hashes to be rendered as empty arrays
15+
716
### v1.0.0 (November 21, 2020)
817

918
[#14](https://github.com/EmCousin/grape_fast_jsonapi/pull/14) - [@EmCousin](https://github.com/EmCousin)

README.md

+10
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ get "/" do
5151
end
5252
```
5353

54+
### Override `meta`and `links` properties
55+
56+
`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))
57+
58+
However, if you need to override those properties, you can pass them as options when rendering your response:
59+
```ruby
60+
user = User.find("123")
61+
render user, meta: { pagination: { page: 1, total: 42 } }, links: { self: 'https://my-awesome.app.com/users/1' }
62+
```
63+
5464
### Model parser for response documentation
5565

5666
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:

lib/grape_jsonapi/formatter.rb

+24-16
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@ module Formatter
55
module Jsonapi
66
class << self
77
def call(object, env)
8-
return object if object.is_a?(String)
9-
return ::Grape::Json.dump(serialize(object, env)) if serializable?(object)
10-
return object.to_json if object.respond_to?(:to_json)
11-
12-
::Grape::Json.dump(object)
8+
response = serializable?(object) ? serialize(object, env) : { data: object }
9+
::Grape::Json.dump(
10+
response.merge(env.slice('meta', 'links'))
11+
)
1312
end
1413

1514
private
@@ -25,17 +24,17 @@ def serializable?(object)
2524
def serialize(object, env)
2625
if object.respond_to?(:serializable_hash)
2726
serializable_object(object, jsonapi_options(env)).serializable_hash
28-
elsif serializable_collection?(object)
29-
serializable_collection(object, jsonapi_options(env))
3027
elsif object.is_a?(Hash)
3128
serialize_each_pair(object, env)
29+
elsif serializable_collection?(object)
30+
serializable_collection(object, env)
3231
else
3332
object
3433
end
3534
end
3635

3736
def serializable_collection?(object)
38-
object.respond_to?(:to_a) && object.all? do |o|
37+
!object.nil? && object.respond_to?(:to_a) && object.any? && object.all? do |o|
3938
o.respond_to?(:serializable_hash)
4039
end
4140
end
@@ -48,22 +47,24 @@ def jsonapi_serializable(object, options)
4847
serializable_class(object, options)&.new(object, options)
4948
end
5049

51-
def serializable_collection(collection, options)
50+
def serializable_collection(collection, env)
5251
if heterogeneous_collection?(collection)
53-
collection.map do |o|
54-
serialize_resource(o, options)
52+
collection.each_with_object({ data: [] }) do |o, hash|
53+
hash[:data].push(serialize_resource(o, env)[:data])
5554
end
5655
else
57-
serialize_resource(collection, options)
56+
serialize_resource(collection, env)
5857
end
5958
end
6059

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

65-
def serialize_resource(resource, options)
66-
jsonapi_serializable(resource, options)&.serializable_hash || resource.map(&:serializable_hash)
64+
def serialize_resource(resource, env)
65+
jsonapi_serializable(resource, jsonapi_options(env))&.serializable_hash || resource.map do |item|
66+
serialize(item, env)
67+
end
6768
end
6869

6970
def serializable_class(object, options)
@@ -78,8 +79,15 @@ def serializable_class(object, options)
7879
end
7980

8081
def serialize_each_pair(object, env)
81-
h = {}
82-
object.each_pair { |k, v| h[k] = serialize(v, env) }
82+
h = { data: {} }
83+
object.each_pair do |k, v|
84+
serialized_value = serialize(v, env)
85+
h[:data][k] = if serialized_value.is_a?(Hash) && serialized_value[:data]
86+
serialized_value[:data]
87+
else
88+
serialized_value
89+
end
90+
end
8391
h
8492
end
8593

lib/grape_jsonapi/version.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22

33
module Grape
44
module Jsonapi
5-
VERSION = '1.0.0'
5+
VERSION = '1.0.1'
66
end
77
end

spec/lib/grape_jsonapi/formatter_spec.rb

+71-42
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@
1818
describe '.call' do
1919
subject { described_class.call(object, env) }
2020
let(:jsonapi_serializer_options) { nil }
21-
let(:env) { { 'jsonapi_serializer_options' => jsonapi_serializer_options } }
21+
let(:meta) { { pagination: { page: 1, total: 2 } } }
22+
let(:links) { { self: 'https://example/org' } }
23+
let(:env) { { 'jsonapi_serializer_options' => jsonapi_serializer_options, 'meta' => meta, 'links' => links } }
2224

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

26-
it { is_expected.to eq object }
29+
it { is_expected.to eq response }
2730
end
2831

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

3437
context 'when the object has a model_name defined' do
3538
let(:object) { admin }
36-
it { is_expected.to eq ::Grape::Json.dump(user_serializer.serializable_hash) }
39+
let(:response) { ::Grape::Json.dump(user_serializer.serializable_hash.merge(meta: meta, links: links)) }
40+
41+
it { is_expected.to eq response }
3742
end
3843

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

42-
it { is_expected.to eq ::Grape::Json.dump(user_serializer.serializable_hash) }
48+
it { is_expected.to eq response }
4349
end
4450

45-
context 'when the object is an array of active serializable model instances' do
46-
let(:object) { [user, another_user] }
47-
48-
it { is_expected.to eq ::Grape::Json.dump(user_serializer.serializable_hash) }
49-
end
51+
context 'when the object is an array' do
52+
context 'when the object is an array of active serializable model instances' do
53+
let(:object) { [user, another_user] }
54+
let(:response) { ::Grape::Json.dump(user_serializer.serializable_hash.merge(meta: meta, links: links)) }
5055

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

54-
it 'returns an array of jsonapi serialialized objects' do
55-
expect(subject).to eq(::Grape::Json.dump([
56-
UserSerializer.new(user, {}).serializable_hash,
57-
BlogPostSerializer.new(blog_post, {}).serializable_hash
58-
]))
59+
context 'when the array contains instances of different models' do
60+
let(:object) { [user, blog_post] }
61+
let(:response) do
62+
::Grape::Json.dump({
63+
data: [
64+
UserSerializer.new(user, {}).serializable_hash[:data],
65+
BlogPostSerializer.new(blog_post, {}).serializable_hash[:data]
66+
],
67+
meta: meta,
68+
links: links
69+
})
70+
end
71+
72+
it 'returns an array of jsonapi serialialized objects' do
73+
expect(subject).to eq response
74+
end
5975
end
60-
end
6176

62-
context 'when the object is an empty array ' do
63-
let(:object) { [] }
77+
context 'when the object is an empty array' do
78+
let(:object) { [] }
6479

65-
it { is_expected.to eq ::Grape::Json.dump(object) }
66-
end
80+
it { is_expected.to eq({ data: [], meta: meta, links: links }.to_json) }
81+
end
6782

68-
context 'when the object is an array of null objects ' do
69-
let(:object) { [nil, nil] }
83+
context 'when the object is an array of null objects' do
84+
let(:object) { [nil, nil] }
7085

71-
it { is_expected.to eq ::Grape::Json.dump(object) }
86+
it { is_expected.to eq({ data: [nil, nil], meta: meta, links: links }.to_json) }
87+
end
7288
end
7389

74-
context 'when the object is a Hash of plain values' do
75-
let(:object) { user.as_json }
90+
context 'when the object is a hash' do
91+
context 'when the object is an empty hash' do
92+
let(:object) { {} }
7693

77-
it { is_expected.to eq ::Grape::Json.dump(object) }
78-
end
94+
it { is_expected.to eq({ data: {}, meta: meta, links: links }.to_json) }
95+
end
7996

80-
context 'when the object is a Hash with serializable object values' do
81-
let(:object) do
82-
{
83-
user: user,
84-
blog_post: blog_post
85-
}
97+
context 'when the object is a Hash of plain values' do
98+
let(:object) { user.as_json }
99+
100+
it { is_expected.to eq ::Grape::Json.dump({ data: user.as_json, meta: meta, links: links }) }
86101
end
87102

88-
it 'returns an hash of with jsonapi serialialized objects values' do
89-
expect(subject).to eq(::Grape::Json.dump({
90-
user: UserSerializer.new(user, {}).serializable_hash,
91-
blog_post: BlogPostSerializer.new(blog_post, {}).serializable_hash
92-
}))
103+
context 'when the object is a Hash with serializable object values' do
104+
let(:object) do
105+
{ user: user, blog_post: blog_post }
106+
end
107+
108+
let(:response) do
109+
::Grape::Json.dump({
110+
data: {
111+
user: UserSerializer.new(user, {}).serializable_hash[:data],
112+
blog_post: BlogPostSerializer.new(blog_post, {}).serializable_hash[:data]
113+
},
114+
meta: meta,
115+
links: links
116+
})
117+
end
118+
119+
it 'returns an hash of with jsonapi serialialized objects values' do
120+
expect(subject).to eq response
121+
end
93122
end
94123
end
95124

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

99-
it { is_expected.to eq 'null' }
128+
it { is_expected.to eq({ data: nil, meta: meta, links: links }.to_json) }
100129
end
101130

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

105-
it { is_expected.to eq '42' }
134+
it { is_expected.to eq({ data: 42, meta: meta, links: links }.to_json) }
106135
end
107136

108137
context 'when a custom serializer is passed as an option' do
@@ -113,7 +142,7 @@
113142
}
114143
end
115144

116-
it { is_expected.to eq ::Grape::Json.dump(another_user_serializer.serializable_hash) }
145+
it { is_expected.to eq ::Grape::Json.dump(another_user_serializer.serializable_hash.merge(meta: meta, links: links)) }
117146
end
118147
end
119148
end
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# frozen_string_literal: true
22

33
describe Grape::Jsonapi::VERSION do
4-
it { is_expected.to eq '1.0.0'.freeze }
4+
it { is_expected.to eq '1.0.1'.freeze }
55
end

0 commit comments

Comments
 (0)