Skip to content

Commit ca44156

Browse files
committed
Cleanup sparse fieldset handling and docs.
1 parent f03663e commit ca44156

File tree

3 files changed

+52
-95
lines changed

3 files changed

+52
-95
lines changed

README.md

Lines changed: 15 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -419,60 +419,33 @@ This option overrides the `jsonapi_serializer_class_name` method.
419419

420420
### Sparse fieldsets
421421

422-
The JSON:API spec allows to return only [specific fields](http://jsonapi.org/format/#fetching-sparse-fieldsets).
422+
The JSON:API spec allows to return only [specific fields](http://jsonapi.org/format/#fetching-sparse-fieldsets) from attributes and relationships.
423423

424-
For example, if you wanted to return only `title` and `author` fields for `posts` type and `name` field for `users` type, you could write the following code:
424+
For example, if you wanted to return only the `title` field and `author` relationship link for `posts`:
425425

426426
```ruby
427-
fields = {posts: 'title,author', users: 'name'}
428-
JSONAPI::Serializer.serialize(post, fields: fields, include: 'author')
427+
fields =
428+
JSONAPI::Serializer.serialize(post, fields: {posts: [:title]})
429429
```
430430

431-
Returns:
431+
Sparse fieldsets also affect relationship links. In this case, only the `author` relationship link would be included:
432432

433-
```json
434-
{
435-
"data": {
436-
"type": "posts",
437-
"id": "1",
438-
"attributes": {
439-
"title": "Title for Post 1"
440-
},
441-
"links": {
442-
"self": "/posts/1"
443-
},
444-
"relationships": {
445-
"author": {
446-
"links": {
447-
"self": "/posts/1/relationships/author",
448-
"related": "/posts/1/author"
449-
},
450-
"data": { "type": "users", "id": "3" }
451-
}
452-
}
453-
},
454-
"included": [
455-
{
456-
"type": "users",
457-
"id": "3",
458-
"attributes": {
459-
"name": "User #3"
460-
},
461-
"links": {
462-
"self": "/users/3"
463-
}
464-
}
465-
]
466-
}
433+
``` ruby
434+
JSONAPI::Serializer.serialize(post, fields: {posts: [:title, :author]})
467435
```
468436

469-
You could also pass an array of specific fields for given type instead of comma-separated values:
437+
Sparse fieldsets operate on a per-type basis, so they affect all resources in the response including in compound documents. For example, this will affect both the `posts` type in the primary data and the `users` type in the compound data:
470438

471439
``` ruby
472-
fields = {posts: ['title', 'author'], users: ['name']}
473-
JSONAPI::Serializer.serialize(post, fields: fields, include: 'author')
440+
JSONAPI::Serializer.serialize(
441+
post,
442+
fields: {posts: ['title', 'author'], users: ['name']},
443+
include: 'author',
444+
)
474445
```
475446

447+
Sparse fieldsets support comma-separated strings (`fields: {posts: 'title,author'}`, arrays of strings (`fields: {posts: ['title', 'author']}`), single symbols (`fields: {posts: :title}`), and arrays of symbols (`fields: {posts: [:title, :author]}`).
448+
476449
## Relationships
477450

478451
You can easily specify relationships with the `has_one` and `has_many` directives.

lib/jsonapi-serializers/serializer.rb

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,15 @@ module InstanceMethods
2525
attr_accessor :object
2626
attr_accessor :context
2727
attr_accessor :base_url
28-
attr_accessor :fields
2928

3029
def initialize(object, options = {})
3130
@object = object
3231
@options = options
3332
@context = options[:context] || {}
3433
@base_url = options[:base_url]
35-
@fields = options[:fields] || {}
3634

3735
# Internal serializer options, not exposed through attr_accessor. No touchie.
36+
@_fields = options[:fields] || {}
3837
@_include_linkages = options[:include_linkages] || []
3938
end
4039

@@ -166,7 +165,8 @@ def relationships
166165
def attributes
167166
return {} if self.class.attributes_map.nil?
168167
attributes = {}
169-
self.class.attributes_map.select(&should_include_attr_proc).each do |attribute_name, attr_data|
168+
self.class.attributes_map.each do |attribute_name, attr_data|
169+
next if !should_include_attr?(attribute_name, attr_data)
170170
value = evaluate_attr_or_block(attribute_name, attr_data[:attr_or_block])
171171
attributes[format_name(attribute_name)] = value
172172
end
@@ -176,7 +176,8 @@ def attributes
176176
def has_one_relationships
177177
return {} if self.class.to_one_associations.nil?
178178
data = {}
179-
self.class.to_one_associations.select(&should_include_attr_proc).each do |attribute_name, attr_data|
179+
self.class.to_one_associations.each do |attribute_name, attr_data|
180+
next if !should_include_attr?(attribute_name, attr_data)
180181
data[attribute_name] = attr_data
181182
end
182183
data
@@ -189,7 +190,8 @@ def has_one_relationship(attribute_name, attr_data)
189190
def has_many_relationships
190191
return {} if self.class.to_many_associations.nil?
191192
data = {}
192-
self.class.to_many_associations.select(&should_include_attr_proc).each do |attribute_name, attr_data|
193+
self.class.to_many_associations.each do |attribute_name, attr_data|
194+
next if !should_include_attr?(attribute_name, attr_data)
193195
data[attribute_name] = attr_data
194196
end
195197
data
@@ -199,23 +201,18 @@ def has_many_relationship(attribute_name, attr_data)
199201
evaluate_attr_or_block(attribute_name, attr_data[:attr_or_block])
200202
end
201203

202-
def should_include_attr?(attribute_name, if_method_name, unless_method_name)
204+
def should_include_attr?(attribute_name, attr_data)
203205
# Allow "if: :show_title?" and "unless: :hide_title?" attribute options.
206+
if_method_name = attr_data[:options][:if]
207+
unless_method_name = attr_data[:options][:unless]
204208
show_attr = true
205209
show_attr &&= send(if_method_name) if if_method_name
206210
show_attr &&= !send(unless_method_name) if unless_method_name
207-
show_attr &&= @fields[type].include?(attribute_name) if @fields[type]
211+
show_attr &&= @_fields[type.to_s].include?(attribute_name) if @_fields[type.to_s]
208212
show_attr
209213
end
210214
protected :should_include_attr?
211215

212-
def should_include_attr_proc
213-
lambda do |attribute_name, attr_data|
214-
should_include_attr?(attribute_name, attr_data[:options][:if], attr_data[:options][:unless])
215-
end
216-
end
217-
private :should_include_attr_proc
218-
219216
def evaluate_attr_or_block(attribute_name, attr_or_block)
220217
if attr_or_block.is_a?(Proc)
221218
# A custom block was given, call it to get the value.
@@ -259,7 +256,7 @@ def self.serialize(objects, options = {})
259256
options[:jsonapi] = options.delete('jsonapi') || options[:jsonapi]
260257
options[:meta] = options.delete('meta') || options[:meta]
261258
options[:links] = options.delete('links') || options[:links]
262-
options[:fields] = options.delete('fields') || options[:fields]
259+
options[:fields] = options.delete('fields') || options[:fields] || {}
263260

264261
# Deprecated: use serialize_errors method instead
265262
options[:errors] = options.delete('errors') || options[:errors]
@@ -268,20 +265,19 @@ def self.serialize(objects, options = {})
268265
includes = options[:include]
269266
includes = (includes.is_a?(String) ? includes.split(',') : includes).uniq if includes
270267

271-
fields = options[:fields] || {}
272268
# Transforms input so that the comma-separated fields are separate symbols in array
273269
# and keys are stringified
274270
# Example:
275271
# {posts: 'title,author,long_comments'} => {'posts' => [:title, :author, :long_comments]}
276272
# {posts: ['title', 'author', 'long_comments'} => {'posts' => [:title, :author, :long_comments]}
277273
#
278-
fields = Hash[fields.map do |type, whitelisted_fields|
279-
if whitelisted_fields.respond_to?(:to_ary)
280-
[type.to_s, whitelisted_fields.map(&:to_sym)]
281-
else
282-
[type.to_s, whitelisted_fields.split(",").map(&:to_sym)]
283-
end
284-
end]
274+
fields = {}
275+
# Normalize fields to accept a comma-separated string or an array of strings.
276+
options[:fields].map do |type, whitelisted_fields|
277+
whitelisted_fields = [whitelisted_fields] if whitelisted_fields.is_a?(Symbol)
278+
whitelisted_fields = whitelisted_fields.split(',') if whitelisted_fields.is_a?(String)
279+
fields[type.to_s] = whitelisted_fields.map(&:to_sym)
280+
end
285281

286282
# An internal-only structure that is passed through serializers as they are created.
287283
passthrough_options = {

spec/serializer_spec.rb

Lines changed: 18 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -441,12 +441,12 @@ def serialize_primary(object, options = {})
441441
it 'can include a top level errors node' do
442442
errors = [
443443
{
444-
'source' => { 'pointer' => '/data/attributes/first-name' },
444+
'source' => {'pointer' => '/data/attributes/first-name'},
445445
'title' => 'Invalid Attribute',
446446
'detail' => 'First name must contain at least three characters.'
447447
},
448448
{
449-
'source' => { 'pointer' => '/data/attributes/first-name' },
449+
'source' => {'pointer' => '/data/attributes/first-name'},
450450
'title' => 'Invalid Attribute',
451451
'detail' => 'First name must contain an emoji.'
452452
}
@@ -477,15 +477,15 @@ def read_attribute_for_validation(attr)
477477
user = DummyUser.new
478478
jsonapi_errors = [
479479
{
480-
'source' => { 'pointer' => '/data/attributes/email' },
480+
'source' => {'pointer' => '/data/attributes/email'},
481481
'detail' => 'Email is invalid'
482482
},
483483
{
484-
'source' => { 'pointer' => '/data/attributes/email' },
484+
'source' => {'pointer' => '/data/attributes/email'},
485485
'detail' => "Email can't be blank"
486486
},
487487
{
488-
'source' => { 'pointer' => '/data/attributes/first-name' },
488+
'source' => {'pointer' => '/data/attributes/first-name'},
489489
'detail' => "First name can't be blank"
490490
}
491491
]
@@ -864,7 +864,7 @@ def read_attribute_for_validation(attr)
864864
long_comments = [first_comment, second_comment]
865865
post = create(:post, :with_author, long_comments: long_comments)
866866

867-
serialized_data = JSONAPI::Serializer.serialize(post, fields: {posts: 'title'})
867+
serialized_data = JSONAPI::Serializer.serialize(post, fields: {posts: :title})
868868
expect(serialized_data).to eq ({
869869
'data' => {
870870
'type' => 'posts',
@@ -878,7 +878,6 @@ def read_attribute_for_validation(attr)
878878
}
879879
})
880880
end
881-
882881
it 'allows to limit fields(relationships) for serialized resource' do
883882
first_user = create(:user)
884883
second_user = create(:user)
@@ -887,7 +886,8 @@ def read_attribute_for_validation(attr)
887886
long_comments = [first_comment, second_comment]
888887
post = create(:post, :with_author, long_comments: long_comments)
889888

890-
serialized_data = JSONAPI::Serializer.serialize(post, fields: {posts: 'title,author,long_comments'})
889+
fields = {posts: 'title,author,long_comments'}
890+
serialized_data = JSONAPI::Serializer.serialize(post, fields: fields)
891891
expect(serialized_data['data']['relationships']).to eq ({
892892
'author' => {
893893
'links' => {
@@ -903,7 +903,6 @@ def read_attribute_for_validation(attr)
903903
}
904904
})
905905
end
906-
907906
it "allows also to pass specific fields as array instead of comma-separates values" do
908907
first_user = create(:user)
909908
second_user = create(:user)
@@ -925,7 +924,6 @@ def read_attribute_for_validation(attr)
925924
}
926925
})
927926
end
928-
929927
it 'allows to limit fields(attributes and relationships) for included resources' do
930928
first_user = create(:user)
931929
second_user = create(:user)
@@ -937,40 +935,30 @@ def read_attribute_for_validation(attr)
937935
expected_primary_data = serialize_primary(post, {
938936
serializer: MyApp::PostSerializer,
939937
include_linkages: ['author'],
940-
fields: { 'posts' => [:title, :author] }
938+
fields: {'posts' => [:title, :author] }
941939
})
942940

943-
serialized_data = JSONAPI::Serializer.serialize(post, fields: {posts: 'title,author', users: ''}, include: 'author')
941+
fields = {posts: 'title,author', users: ''}
942+
serialized_data = JSONAPI::Serializer.serialize(post, fields: fields, include: 'author')
944943
expect(serialized_data).to eq ({
945944
'data' => expected_primary_data,
946945
'included' => [
947-
serialize_primary(post.author, serializer: MyAppOtherNamespace::UserSerializer, fields: { 'users' => [] })
946+
serialize_primary(
947+
post.author,
948+
serializer: MyAppOtherNamespace::UserSerializer,
949+
fields: {'users' => []},
950+
)
948951
]
949952
})
950953

951-
serialized_data = JSONAPI::Serializer.serialize(post, fields: {posts: 'title,author'}, include: 'author')
954+
fields = {posts: 'title,author'}
955+
serialized_data = JSONAPI::Serializer.serialize(post, fields: fields, include: 'author')
952956
expect(serialized_data).to eq ({
953957
'data' => expected_primary_data,
954958
'included' => [
955959
serialize_primary(post.author, serializer: MyAppOtherNamespace::UserSerializer)
956960
]
957961
})
958-
959-
serialized_data = JSONAPI::Serializer.serialize(post, fields: {posts: 'title,author', users: 'nonexistent'}, include: 'author')
960-
expect(serialized_data).to eq ({
961-
'data' => expected_primary_data,
962-
'included' => [
963-
serialize_primary(post.author, serializer: MyAppOtherNamespace::UserSerializer, fields: { 'users' => [:nonexistent] })
964-
]
965-
})
966-
967-
serialized_data = JSONAPI::Serializer.serialize(post, fields: {posts: 'title,author', users: 'name'}, include: 'author')
968-
expect(serialized_data).to eq ({
969-
'data' => expected_primary_data,
970-
'included' => [
971-
serialize_primary(post.author, serializer: MyAppOtherNamespace::UserSerializer, fields: { 'users' => [:name] })
972-
]
973-
})
974962
end
975963
end
976964
end

0 commit comments

Comments
 (0)