jsonapi-serializers 0.15.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8b8e2b1a0281f387fc8e6e3ee0c24747ada99067
4
- data.tar.gz: 2ce4804f76c29485607f6738bee21dd00dac9db0
3
+ metadata.gz: 0a63fa6a767f6236335d147e099ca81b13e24d72
4
+ data.tar.gz: e8fe7e954d1b5a14d2903ff0d72923eb24d0abb8
5
5
  SHA512:
6
- metadata.gz: d7d32a4701376557f93e848ee32c7a77f80ed1214a4de326a0c8dc85530f6211ed75476b060a8716162391928af36cb7ac6e551fbf29f99c40e3d5a305cf87f3
7
- data.tar.gz: bcb13f91e195f15b052d055957e904bd4e4704365292bf36e3c9c76d88bcab74903752b9670fde309169e525ff64389a444e205fed80841fc270f9518c9e8aa7
6
+ metadata.gz: 64417e501b68bb8e686eb362e8ea53c65afff161d5c9f0f881e08d15436c8f4b27fd06491ee5d479d74c99ce7ed5c78e72e001a82a18a019d56cbd7b99ef28e9
7
+ data.tar.gz: da8f7e8f9c78c18f5d6f6a2ae7b1399116a7876f40c0c71b4d59d36615be5ba88e7b5c978b3f536dbd61e071ea6fd103940044a3b3a3d89d08a66649f113ddf1
@@ -5,7 +5,7 @@ rvm:
5
5
  - 1.9.3
6
6
  - 2.1.1
7
7
  - 2.2.2
8
- - ruby-head
8
+ - 2.3.1
9
9
  before_install:
10
10
  - gem install bundler
11
11
  script: bundle exec rspec
data/README.md CHANGED
@@ -18,12 +18,13 @@ This library is up-to-date with the finalized v1 JSON API spec.
18
18
  * [Custom attributes](#custom-attributes)
19
19
  * [More customizations](#more-customizations)
20
20
  * [Base URL](#base-url)
21
- * [Root jsonapi object](#root-jsonapi-object)
22
21
  * [Root metadata](#root-metadata)
23
22
  * [Root links](#root-links)
24
23
  * [Root errors](#root-errors)
24
+ * [Root jsonapi object](#root-jsonapi-object)
25
25
  * [Explicit serializer discovery](#explicit-serializer-discovery)
26
26
  * [Namespace serializers](#namespace-serializers)
27
+ * [Sparse fieldsets](#sparse-fieldsets)
27
28
  * [Relationships](#relationships)
28
29
  * [Compound documents and includes](#compound-documents-and-includes)
29
30
  * [Relationship path handling](#relationship-path-handling)
@@ -318,14 +319,6 @@ JSONAPI::Serializer.serialize(post, base_url: 'http://example.com')
318
319
 
319
320
  Note: if you override `self_link` in your serializer and leave out `base_url`, it will not be included.
320
321
 
321
- ### Root 'jsonapi' object
322
-
323
- You can pass a `jsonapi` argument to specify a [top-level "jsonapi" key](http://jsonapi.org/format/#document-jsonapi-object) containing the version of JSON:API in use:
324
-
325
- ```ruby
326
- JSONAPI::Serializer.serialize(post, jsonapi: {version: '1.0'})
327
- ```
328
-
329
322
  ### Root metadata
330
323
 
331
324
  You can pass a `meta` argument to specify top-level metadata:
@@ -372,6 +365,14 @@ class Api::V1::ReposController < Api::V1::BaseController
372
365
  end
373
366
  ```
374
367
 
368
+ ### Root 'jsonapi' object
369
+
370
+ You can pass a `jsonapi` argument to specify a [top-level "jsonapi" key](http://jsonapi.org/format/#document-jsonapi-object) containing the version of JSON:API in use:
371
+
372
+ ```ruby
373
+ JSONAPI::Serializer.serialize(post, jsonapi: {version: '1.0'})
374
+ ```
375
+
375
376
  ### Explicit serializer discovery
376
377
 
377
378
  By default, jsonapi-serializers assumes that the serializer class for `Namespace::User` is `Namespace::UserSerializer`. You can override this behavior on a per-object basis by implementing the `jsonapi_serializer_class_name` method.
@@ -416,6 +417,35 @@ JSONAPI::Serializer.serialize(post, namespace: Api::V2)
416
417
 
417
418
  This option overrides the `jsonapi_serializer_class_name` method.
418
419
 
420
+ ### Sparse fieldsets
421
+
422
+ The JSON:API spec allows to return only [specific fields](http://jsonapi.org/format/#fetching-sparse-fieldsets) from attributes and relationships.
423
+
424
+ For example, if you wanted to return only the `title` field and `author` relationship link for `posts`:
425
+
426
+ ```ruby
427
+ fields =
428
+ JSONAPI::Serializer.serialize(post, fields: {posts: [:title]})
429
+ ```
430
+
431
+ Sparse fieldsets also affect relationship links. In this case, only the `author` relationship link would be included:
432
+
433
+ ``` ruby
434
+ JSONAPI::Serializer.serialize(post, fields: {posts: [:title, :author]})
435
+ ```
436
+
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:
438
+
439
+ ``` ruby
440
+ JSONAPI::Serializer.serialize(
441
+ post,
442
+ fields: {posts: ['title', 'author'], users: ['name']},
443
+ include: 'author',
444
+ )
445
+ ```
446
+
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
+
419
449
  ## Relationships
420
450
 
421
451
  You can easily specify relationships with the `has_one` and `has_many` directives.
@@ -783,7 +813,6 @@ See [Releases](https://github.com/fotinakis/jsonapi-serializers/releases).
783
813
 
784
814
  ## Unfinished business
785
815
 
786
- * Support for the `fields` spec is planned, would love a PR contribution for this.
787
816
  * Support for pagination/sorting is unlikely to be supported because it would likely involve coupling to ActiveRecord, but please open an issue if you have ideas of how to support this generically.
788
817
 
789
818
  ## Contributing
@@ -33,6 +33,7 @@ module JSONAPI
33
33
  @base_url = options[:base_url]
34
34
 
35
35
  # Internal serializer options, not exposed through attr_accessor. No touchie.
36
+ @_fields = options[:fields] || {}
36
37
  @_include_linkages = options[:include_linkages] || []
37
38
  end
38
39
 
@@ -165,7 +166,7 @@ module JSONAPI
165
166
  return {} if self.class.attributes_map.nil?
166
167
  attributes = {}
167
168
  self.class.attributes_map.each do |attribute_name, attr_data|
168
- next if !should_include_attr?(attr_data[:options][:if], attr_data[:options][:unless])
169
+ next if !should_include_attr?(attribute_name, attr_data)
169
170
  value = evaluate_attr_or_block(attribute_name, attr_data[:attr_or_block])
170
171
  attributes[format_name(attribute_name)] = value
171
172
  end
@@ -176,7 +177,7 @@ module JSONAPI
176
177
  return {} if self.class.to_one_associations.nil?
177
178
  data = {}
178
179
  self.class.to_one_associations.each do |attribute_name, attr_data|
179
- next if !should_include_attr?(attr_data[:options][:if], attr_data[:options][:unless])
180
+ next if !should_include_attr?(attribute_name, attr_data)
180
181
  data[attribute_name] = attr_data
181
182
  end
182
183
  data
@@ -190,7 +191,7 @@ module JSONAPI
190
191
  return {} if self.class.to_many_associations.nil?
191
192
  data = {}
192
193
  self.class.to_many_associations.each do |attribute_name, attr_data|
193
- next if !should_include_attr?(attr_data[:options][:if], attr_data[:options][:unless])
194
+ next if !should_include_attr?(attribute_name, attr_data)
194
195
  data[attribute_name] = attr_data
195
196
  end
196
197
  data
@@ -200,11 +201,14 @@ module JSONAPI
200
201
  evaluate_attr_or_block(attribute_name, attr_data[:attr_or_block])
201
202
  end
202
203
 
203
- def should_include_attr?(if_method_name, unless_method_name)
204
+ def should_include_attr?(attribute_name, attr_data)
204
205
  # 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]
205
208
  show_attr = true
206
209
  show_attr &&= send(if_method_name) if if_method_name
207
210
  show_attr &&= !send(unless_method_name) if unless_method_name
211
+ show_attr &&= @_fields[type.to_s].include?(attribute_name) if @_fields[type.to_s]
208
212
  show_attr
209
213
  end
210
214
  protected :should_include_attr?
@@ -252,6 +256,7 @@ module JSONAPI
252
256
  options[:jsonapi] = options.delete('jsonapi') || options[:jsonapi]
253
257
  options[:meta] = options.delete('meta') || options[:meta]
254
258
  options[:links] = options.delete('links') || options[:links]
259
+ options[:fields] = options.delete('fields') || options[:fields] || {}
255
260
 
256
261
  # Deprecated: use serialize_errors method instead
257
262
  options[:errors] = options.delete('errors') || options[:errors]
@@ -260,12 +265,27 @@ module JSONAPI
260
265
  includes = options[:include]
261
266
  includes = (includes.is_a?(String) ? includes.split(',') : includes).uniq if includes
262
267
 
268
+ # Transforms input so that the comma-separated fields are separate symbols in array
269
+ # and keys are stringified
270
+ # Example:
271
+ # {posts: 'title,author,long_comments'} => {'posts' => [:title, :author, :long_comments]}
272
+ # {posts: ['title', 'author', 'long_comments'} => {'posts' => [:title, :author, :long_comments]}
273
+ #
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
281
+
263
282
  # An internal-only structure that is passed through serializers as they are created.
264
283
  passthrough_options = {
265
284
  context: options[:context],
266
285
  serializer: options[:serializer],
267
286
  namespace: options[:namespace],
268
287
  include: includes,
288
+ fields: fields,
269
289
  base_url: options[:base_url]
270
290
  }
271
291
 
@@ -328,6 +348,7 @@ module JSONAPI
328
348
  included_passthrough_options = {}
329
349
  included_passthrough_options[:base_url] = passthrough_options[:base_url]
330
350
  included_passthrough_options[:context] = passthrough_options[:context]
351
+ included_passthrough_options[:fields] = passthrough_options[:fields]
331
352
  included_passthrough_options[:serializer] = find_serializer_class(data[:object], options)
332
353
  included_passthrough_options[:namespace] = passthrough_options[:namespace]
333
354
  included_passthrough_options[:include_linkages] = data[:include_linkages]
@@ -1,5 +1,5 @@
1
1
  module JSONAPI
2
2
  module Serializer
3
- VERSION = '0.15.0'
3
+ VERSION = '0.16.0'
4
4
  end
5
5
  end
@@ -441,12 +441,12 @@ describe JSONAPI::Serializer do
441
441
  it 'can include a top level errors node' do
442
442
  errors = [
443
443
  {
444
- 'source' => { 'pointer' => '/data/attributes/first-name' },
444
+ 'source' => {'pointer' => '/data/attributes/first-name'},
445
445
  'title' => 'Invalid Attribute',
446
446
  'detail' => 'First name must contain at least three characters.'
447
447
  },
448
448
  {
449
- 'source' => { 'pointer' => '/data/attributes/first-name' },
449
+ 'source' => {'pointer' => '/data/attributes/first-name'},
450
450
  'title' => 'Invalid Attribute',
451
451
  'detail' => 'First name must contain an emoji.'
452
452
  }
@@ -477,15 +477,15 @@ describe JSONAPI::Serializer do
477
477
  user = DummyUser.new
478
478
  jsonapi_errors = [
479
479
  {
480
- 'source' => { 'pointer' => '/data/attributes/email' },
480
+ 'source' => {'pointer' => '/data/attributes/email'},
481
481
  'detail' => 'Email is invalid'
482
482
  },
483
483
  {
484
- 'source' => { 'pointer' => '/data/attributes/email' },
484
+ 'source' => {'pointer' => '/data/attributes/email'},
485
485
  'detail' => "Email can't be blank"
486
486
  },
487
487
  {
488
- 'source' => { 'pointer' => '/data/attributes/first-name' },
488
+ 'source' => {'pointer' => '/data/attributes/first-name'},
489
489
  'detail' => "First name can't be blank"
490
490
  }
491
491
  ]
@@ -854,6 +854,113 @@ describe JSONAPI::Serializer do
854
854
  })
855
855
  end
856
856
  end
857
+
858
+ context 'sparse fieldsets' do
859
+ it 'allows to limit fields(attributes) for serialized resource' do
860
+ first_user = create(:user)
861
+ second_user = create(:user)
862
+ first_comment = create(:long_comment, user: first_user)
863
+ second_comment = create(:long_comment, user: second_user)
864
+ long_comments = [first_comment, second_comment]
865
+ post = create(:post, :with_author, long_comments: long_comments)
866
+
867
+ serialized_data = JSONAPI::Serializer.serialize(post, fields: {posts: :title})
868
+ expect(serialized_data).to eq ({
869
+ 'data' => {
870
+ 'type' => 'posts',
871
+ 'id' => post.id.to_s,
872
+ 'attributes' => {
873
+ 'title' => post.title,
874
+ },
875
+ 'links' => {
876
+ 'self' => '/posts/1'
877
+ }
878
+ }
879
+ })
880
+ end
881
+ it 'allows to limit fields(relationships) for serialized resource' do
882
+ first_user = create(:user)
883
+ second_user = create(:user)
884
+ first_comment = create(:long_comment, user: first_user)
885
+ second_comment = create(:long_comment, user: second_user)
886
+ long_comments = [first_comment, second_comment]
887
+ post = create(:post, :with_author, long_comments: long_comments)
888
+
889
+ fields = {posts: 'title,author,long_comments'}
890
+ serialized_data = JSONAPI::Serializer.serialize(post, fields: fields)
891
+ expect(serialized_data['data']['relationships']).to eq ({
892
+ 'author' => {
893
+ 'links' => {
894
+ 'self' => '/posts/1/relationships/author',
895
+ 'related' => '/posts/1/author'
896
+ }
897
+ },
898
+ 'long-comments' => {
899
+ 'links' => {
900
+ 'self' => '/posts/1/relationships/long-comments',
901
+ 'related' => '/posts/1/long-comments'
902
+ }
903
+ }
904
+ })
905
+ end
906
+ it "allows also to pass specific fields as array instead of comma-separates values" do
907
+ first_user = create(:user)
908
+ second_user = create(:user)
909
+ first_comment = create(:long_comment, user: first_user)
910
+ second_comment = create(:long_comment, user: second_user)
911
+ long_comments = [first_comment, second_comment]
912
+ post = create(:post, :with_author, long_comments: long_comments)
913
+
914
+ serialized_data = JSONAPI::Serializer.serialize(post, fields: {posts: ['title', 'author']})
915
+ expect(serialized_data['data']['attributes']).to eq ({
916
+ 'title' => post.title
917
+ })
918
+ expect(serialized_data['data']['relationships']).to eq ({
919
+ 'author' => {
920
+ 'links' => {
921
+ 'self' => '/posts/1/relationships/author',
922
+ 'related' => '/posts/1/author'
923
+ }
924
+ }
925
+ })
926
+ end
927
+ it 'allows to limit fields(attributes and relationships) for included resources' do
928
+ first_user = create(:user)
929
+ second_user = create(:user)
930
+ first_comment = create(:long_comment, user: first_user)
931
+ second_comment = create(:long_comment, user: second_user)
932
+ long_comments = [first_comment, second_comment]
933
+ post = create(:post, :with_author, long_comments: long_comments)
934
+
935
+ expected_primary_data = serialize_primary(post, {
936
+ serializer: MyApp::PostSerializer,
937
+ include_linkages: ['author'],
938
+ fields: {'posts' => [:title, :author] }
939
+ })
940
+
941
+ fields = {posts: 'title,author', users: ''}
942
+ serialized_data = JSONAPI::Serializer.serialize(post, fields: fields, include: 'author')
943
+ expect(serialized_data).to eq ({
944
+ 'data' => expected_primary_data,
945
+ 'included' => [
946
+ serialize_primary(
947
+ post.author,
948
+ serializer: MyAppOtherNamespace::UserSerializer,
949
+ fields: {'users' => []},
950
+ )
951
+ ]
952
+ })
953
+
954
+ fields = {posts: 'title,author'}
955
+ serialized_data = JSONAPI::Serializer.serialize(post, fields: fields, include: 'author')
956
+ expect(serialized_data).to eq ({
957
+ 'data' => expected_primary_data,
958
+ 'included' => [
959
+ serialize_primary(post.author, serializer: MyAppOtherNamespace::UserSerializer)
960
+ ]
961
+ })
962
+ end
963
+ end
857
964
  end
858
965
 
859
966
  describe 'serialize (class method)' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jsonapi-serializers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Fotinakis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-09-01 00:00:00.000000000 Z
11
+ date: 2016-10-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport