From 89f007d069304a2ba1c5e1d239fcf292998289ae Mon Sep 17 00:00:00 2001 From: nikz Date: Sun, 5 Aug 2018 20:37:44 +0100 Subject: [PATCH 1/5] Adds a :links option to the relationship macros This allows specifying a `:links` option to a has_many/has_one relationship, which means you can specify `self` or `related` links as per the JSON API spec (these are often useful for not loading all associated objects in a single payload) --- lib/fast_jsonapi/object_serializer.rb | 5 +- lib/fast_jsonapi/relationship.rb | 17 ++++-- ...ject_serializer_relationship_links_spec.rb | 53 +++++++++++++++++++ spec/shared/contexts/movie_context.rb | 4 ++ 4 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 spec/lib/object_serializer_relationship_links_spec.rb diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index 7f740c8..e662344 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -194,7 +194,7 @@ module FastJsonapi self.relationships_to_serialize = {} if relationships_to_serialize.nil? self.cachable_relationships_to_serialize = {} if cachable_relationships_to_serialize.nil? self.uncachable_relationships_to_serialize = {} if uncachable_relationships_to_serialize.nil? - + if !relationship.cached self.uncachable_relationships_to_serialize[relationship.name] = relationship else @@ -240,7 +240,8 @@ module FastJsonapi relationship_type: relationship_type, cached: options[:cached], polymorphic: fetch_polymorphic_option(options), - conditional_proc: options[:if] + conditional_proc: options[:if], + links: options[:links] ) end diff --git a/lib/fast_jsonapi/relationship.rb b/lib/fast_jsonapi/relationship.rb index 0b3a101..d6652b1 100644 --- a/lib/fast_jsonapi/relationship.rb +++ b/lib/fast_jsonapi/relationship.rb @@ -1,6 +1,6 @@ module FastJsonapi class Relationship - attr_reader :key, :name, :id_method_name, :record_type, :object_method_name, :object_block, :serializer, :relationship_type, :cached, :polymorphic, :conditional_proc + attr_reader :key, :name, :id_method_name, :record_type, :object_method_name, :object_block, :serializer, :relationship_type, :cached, :polymorphic, :conditional_proc, :links def initialize( key:, @@ -13,7 +13,8 @@ module FastJsonapi relationship_type:, cached: false, polymorphic:, - conditional_proc: + conditional_proc:, + links: ) @key = key @name = name @@ -26,14 +27,16 @@ module FastJsonapi @cached = cached @polymorphic = polymorphic @conditional_proc = conditional_proc + @links = links || {} end - def serialize(record, serialization_params, output_hash) + def serialize(record, serialization_params, output_hash, &block) if include_relationship?(record, serialization_params) empty_case = relationship_type == :has_many ? [] : nil output_hash[key] = { - data: ids_hash_from_record_and_relationship(record, serialization_params) || empty_case + data: ids_hash_from_record_and_relationship(record, serialization_params) || empty_case, } + add_links_hash(record, serialization_params, output_hash) if links.present? end end @@ -95,5 +98,11 @@ module FastJsonapi record.public_send(id_method_name) end + + def add_links_hash(record, params, output_hash) + output_hash[key][:links] = links.each_with_object({}) do |(key, method), hash| + Link.new(key: key, method: method).serialize(record, params, hash) + end + end end end \ No newline at end of file diff --git a/spec/lib/object_serializer_relationship_links_spec.rb b/spec/lib/object_serializer_relationship_links_spec.rb new file mode 100644 index 0000000..bb1ec16 --- /dev/null +++ b/spec/lib/object_serializer_relationship_links_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe FastJsonapi::ObjectSerializer do + include_context 'movie class' + + context "params option" do + let(:hash) { serializer.serializable_hash } + + before(:context) do + class MovieSerializer + has_many :actors, links: { + self: :actors_relationship_url, + related: -> (object, params = {}) { + "#{params.has_key?(:secure) ? "https" : "http"}://movies.com/movies/#{object.name.parameterize}/actors/" + } + } + end + end + + context "generating links for a serializer relationship" do + let(:params) { { } } + let(:options_with_params) { { params: params } } + let(:relationship_url) { "http://movies.com/#{movie.id}/relationships/actors" } + let(:related_url) { "http://movies.com/movies/#{movie.name.parameterize}/actors/" } + + context "with a single record" do + let(:serializer) { MovieSerializer.new(movie, options_with_params) } + let(:links) { hash.dig(:data, :relationships, :actors, :links) } + + it "handles relationship links that call a method" do + expect(links).to be_present + expect(links[:self]).to eq(relationship_url) + end + + it "handles relationship links that call a proc" do + expect(links).to be_present + expect(links[:related]).to eq(related_url) + end + + context "with serializer params" do + let(:params) { { secure: true } } + let(:secure_related_url) { related_url.gsub("http", "https") } + + it "passes the params to the link serializer correctly" do + expect(links).to be_present + expect(links[:related]).to eq(secure_related_url) + end + end + end + + end + end +end diff --git a/spec/shared/contexts/movie_context.rb b/spec/shared/contexts/movie_context.rb index 9061226..30c4d9f 100644 --- a/spec/shared/contexts/movie_context.rb +++ b/spec/shared/contexts/movie_context.rb @@ -61,6 +61,10 @@ RSpec.shared_context 'movie class' do def url "http://movies.com/#{id}" end + + def actors_relationship_url + "#{url}/relationships/actors" + end end class Actor From 8eef7a0bb10f330d62426a52d12ab722facaeb4c Mon Sep 17 00:00:00 2001 From: nikz Date: Sun, 5 Aug 2018 20:51:56 +0100 Subject: [PATCH 2/5] Adds README documentation for relationship links --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index a03e1a8..562792c 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,36 @@ class MovieSerializer end ``` +#### Links on a Relationship + +You can specify [relationship links](http://jsonapi.org/format/#document-resource-object-relationships) by using the `links:` option on the serializer. Relationship links in JSON API are useful if you want to load a parent document and then load associated documents later due to size constraints (see [related resource links](http://jsonapi.org/format/#document-resource-object-related-resource-links)) + +```ruby +class MovieSerializer + include FastJsonapi::ObjectSerializer + + has_many :actors, links: { + self: :url, + related: -> (object) { + "https://movies.com/#{object.id}/actors" + } + } +end +``` + +This will create a `self` reference for the relationship, and a `related` link for loading the actors relationship later. NB: This will not automatically disable including the data in the relationship, you'll need to do that using the yielded block: + +```ruby + has_many :actors, links: { + self: :url, + related: -> (object) { + "https://movies.com/#{object.id}/actors" + } + } do |movie| + movie.actors.limit(5) + end +``` + ### Compound Document Support for top-level and nested included associations through ` options[:include] `. From ef04bc377e4115fa3cb99fe03c6beb168bb67ee3 Mon Sep 17 00:00:00 2001 From: nikz Date: Sun, 5 Aug 2018 21:00:21 +0100 Subject: [PATCH 3/5] Removes Hash#dig usage --- spec/lib/object_serializer_relationship_links_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/object_serializer_relationship_links_spec.rb b/spec/lib/object_serializer_relationship_links_spec.rb index bb1ec16..6da5052 100644 --- a/spec/lib/object_serializer_relationship_links_spec.rb +++ b/spec/lib/object_serializer_relationship_links_spec.rb @@ -25,7 +25,7 @@ describe FastJsonapi::ObjectSerializer do context "with a single record" do let(:serializer) { MovieSerializer.new(movie, options_with_params) } - let(:links) { hash.dig(:data, :relationships, :actors, :links) } + let(:links) { hash[:data][:relationships][:actors][:links] } it "handles relationship links that call a method" do expect(links).to be_present From 1efdd3372d664c41d7c044f0188349ef4d360d32 Mon Sep 17 00:00:00 2001 From: nikz Date: Tue, 2 Oct 2018 22:07:38 +0100 Subject: [PATCH 4/5] Fixes dangling comma and unused param --- lib/fast_jsonapi/relationship.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/fast_jsonapi/relationship.rb b/lib/fast_jsonapi/relationship.rb index 50caf31..889bfab 100644 --- a/lib/fast_jsonapi/relationship.rb +++ b/lib/fast_jsonapi/relationship.rb @@ -30,11 +30,11 @@ module FastJsonapi @links = links || {} end - def serialize(record, serialization_params, output_hash, &block) + def serialize(record, serialization_params, output_hash) if include_relationship?(record, serialization_params) empty_case = relationship_type == :has_many ? [] : nil output_hash[key] = { - data: ids_hash_from_record_and_relationship(record, serialization_params) || empty_case, + data: ids_hash_from_record_and_relationship(record, serialization_params) || empty_case } add_links_hash(record, serialization_params, output_hash) if links.present? end From 85b41c45d4c1f1328908811d353054cada306f2b Mon Sep 17 00:00:00 2001 From: nikz Date: Sun, 7 Oct 2018 21:23:36 +0100 Subject: [PATCH 5/5] Adds :lazy_load_data option If you include a default empty `data` option in your JSON API response, many frontend frameworks will ignore your `related` link that could be used to load relationship records, and will instead treat the relationship as empty. This adds a `lazy_load_data` option which will: * stop the serializer attempting to load the data and; * exclude the `data` key from the final response This allows you to lazy load a JSON API relationship. --- README.md | 8 ++-- lib/fast_jsonapi/object_serializer.rb | 3 +- lib/fast_jsonapi/relationship.rb | 14 ++++--- ...ject_serializer_relationship_links_spec.rb | 40 ++++++++++++++----- 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 3af3e48..057c8bd 100644 --- a/README.md +++ b/README.md @@ -262,17 +262,15 @@ class MovieSerializer end ``` -This will create a `self` reference for the relationship, and a `related` link for loading the actors relationship later. NB: This will not automatically disable including the data in the relationship, you'll need to do that using the yielded block: +This will create a `self` reference for the relationship, and a `related` link for loading the actors relationship later. NB: This will not automatically disable loading the data in the relationship, you'll need to do that using the `lazy_load_data` option: ```ruby - has_many :actors, links: { + has_many :actors, lazy_load_data: true, links: { self: :url, related: -> (object) { "https://movies.com/#{object.id}/actors" } - } do |movie| - movie.actors.limit(5) - end + } ``` ### Meta Per Resource diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index 3b84b1d..71f64bc 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -250,7 +250,8 @@ module FastJsonapi cached: options[:cached], polymorphic: fetch_polymorphic_option(options), conditional_proc: options[:if], - links: options[:links] + links: options[:links], + lazy_load_data: options[:lazy_load_data] ) end diff --git a/lib/fast_jsonapi/relationship.rb b/lib/fast_jsonapi/relationship.rb index 889bfab..e8163a6 100644 --- a/lib/fast_jsonapi/relationship.rb +++ b/lib/fast_jsonapi/relationship.rb @@ -1,6 +1,6 @@ module FastJsonapi class Relationship - attr_reader :key, :name, :id_method_name, :record_type, :object_method_name, :object_block, :serializer, :relationship_type, :cached, :polymorphic, :conditional_proc, :links + attr_reader :key, :name, :id_method_name, :record_type, :object_method_name, :object_block, :serializer, :relationship_type, :cached, :polymorphic, :conditional_proc, :links, :lazy_load_data def initialize( key:, @@ -14,7 +14,8 @@ module FastJsonapi cached: false, polymorphic:, conditional_proc:, - links: + links:, + lazy_load_data: false ) @key = key @name = name @@ -28,14 +29,17 @@ module FastJsonapi @polymorphic = polymorphic @conditional_proc = conditional_proc @links = links || {} + @lazy_load_data = lazy_load_data end def serialize(record, serialization_params, output_hash) if include_relationship?(record, serialization_params) empty_case = relationship_type == :has_many ? [] : nil - output_hash[key] = { - data: ids_hash_from_record_and_relationship(record, serialization_params) || empty_case - } + + output_hash[key] = {} + unless lazy_load_data + output_hash[key][:data] = ids_hash_from_record_and_relationship(record, serialization_params) || empty_case + end add_links_hash(record, serialization_params, output_hash) if links.present? end end diff --git a/spec/lib/object_serializer_relationship_links_spec.rb b/spec/lib/object_serializer_relationship_links_spec.rb index 6da5052..8c2f272 100644 --- a/spec/lib/object_serializer_relationship_links_spec.rb +++ b/spec/lib/object_serializer_relationship_links_spec.rb @@ -6,23 +6,23 @@ describe FastJsonapi::ObjectSerializer do context "params option" do let(:hash) { serializer.serializable_hash } - before(:context) do - class MovieSerializer - has_many :actors, links: { - self: :actors_relationship_url, - related: -> (object, params = {}) { - "#{params.has_key?(:secure) ? "https" : "http"}://movies.com/movies/#{object.name.parameterize}/actors/" - } - } - end - end - context "generating links for a serializer relationship" do let(:params) { { } } let(:options_with_params) { { params: params } } let(:relationship_url) { "http://movies.com/#{movie.id}/relationships/actors" } let(:related_url) { "http://movies.com/movies/#{movie.name.parameterize}/actors/" } + before(:context) do + class MovieSerializer + has_many :actors, lazy_load_data: false, links: { + self: :actors_relationship_url, + related: -> (object, params = {}) { + "#{params.has_key?(:secure) ? "https" : "http"}://movies.com/movies/#{object.name.parameterize}/actors/" + } + } + end + end + context "with a single record" do let(:serializer) { MovieSerializer.new(movie, options_with_params) } let(:links) { hash[:data][:relationships][:actors][:links] } @@ -49,5 +49,23 @@ describe FastJsonapi::ObjectSerializer do end end + + context "lazy loading relationship data" do + before(:context) do + class LazyLoadingMovieSerializer < MovieSerializer + has_many :actors, lazy_load_data: true, links: { + related: :actors_relationship_url + } + end + end + + let(:serializer) { LazyLoadingMovieSerializer.new(movie) } + let(:actor_hash) { hash[:data][:relationships][:actors] } + + it "does not include the :data key" do + expect(actor_hash).to be_present + expect(actor_hash).not_to have_key(:data) + end + end end end