From 89f007d069304a2ba1c5e1d239fcf292998289ae Mon Sep 17 00:00:00 2001 From: nikz Date: Sun, 5 Aug 2018 20:37:44 +0100 Subject: [PATCH 01/18] 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 7f740c8e..e6623442 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -194,7 +194,7 @@ def add_relationship(relationship) 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 @@ def create_relationship(base_key, relationship_type, options, block) 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 0b3a1019..d6652b13 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 @@ def initialize( relationship_type:, cached: false, polymorphic:, - conditional_proc: + conditional_proc:, + links: ) @key = key @name = name @@ -26,14 +27,16 @@ def initialize( @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 @@ def fetch_id(record, params) 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 00000000..bb1ec16f --- /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 90612260..30c4d9ff 100644 --- a/spec/shared/contexts/movie_context.rb +++ b/spec/shared/contexts/movie_context.rb @@ -61,6 +61,10 @@ def local_name(locale = :english) 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 02/18] Adds README documentation for relationship links --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index a03e1a8c..562792c4 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 03/18] 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 bb1ec16f..6da5052d 100644 --- a/spec/lib/object_serializer_relationship_links_spec.rb +++ b/spec/lib/object_serializer_relationship_links_spec.rb @@ -25,7 +25,7 @@ class MovieSerializer 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 6dc34cd4d4d75ab1805fe322693dffff7a19db36 Mon Sep 17 00:00:00 2001 From: Kenji Sakurai Date: Sat, 8 Sep 2018 23:48:44 +0900 Subject: [PATCH 04/18] Fix set_key_transform's set_type to give priority to pre-set value --- lib/fast_jsonapi/object_serializer.rb | 6 +++++- .../object_serializer_class_methods_spec.rb | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index 183d622c..de010613 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -143,7 +143,11 @@ def set_key_transform(transform_name) self.transform_method = mapping[transform_name.to_sym] # ensure that the record type is correctly transformed - set_type(reflected_record_type) if reflected_record_type + if record_type + set_type(record_type) + elsif reflected_record_type + set_type(reflected_record_type) + end end def run_key_transform(input) diff --git a/spec/lib/object_serializer_class_methods_spec.rb b/spec/lib/object_serializer_class_methods_spec.rb index 397ace5a..c1edc0a8 100644 --- a/spec/lib/object_serializer_class_methods_spec.rb +++ b/spec/lib/object_serializer_class_methods_spec.rb @@ -411,4 +411,22 @@ def year_since_release_calculator(release_year) it_behaves_like 'returning key transformed hash', :movie_type, :underscore_movie_type, :release_year end end + + describe '#set_key_transform after #set_type' do + subject(:serializable_hash) { MovieSerializer.new(movie).serializable_hash } + + before do + MovieSerializer.set_type :foo_bar + MovieSerializer.set_key_transform :camel + end + + after do + MovieSerializer.set_type :movie + MovieSerializer.transform_method = nil + end + + it 'returns correct hash which type equals transformed set_type value' do + expect(serializable_hash[:data][:type]).to eq :FooBar + end + end end From 57f09c7d71ebf9af1536623b0901e78a3af1ff1b Mon Sep 17 00:00:00 2001 From: Kenji Sakurai Date: Mon, 10 Sep 2018 10:32:53 +0900 Subject: [PATCH 05/18] Fix method order in spec after --- spec/lib/object_serializer_class_methods_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/object_serializer_class_methods_spec.rb b/spec/lib/object_serializer_class_methods_spec.rb index c1edc0a8..233608e0 100644 --- a/spec/lib/object_serializer_class_methods_spec.rb +++ b/spec/lib/object_serializer_class_methods_spec.rb @@ -421,8 +421,8 @@ def year_since_release_calculator(release_year) end after do - MovieSerializer.set_type :movie MovieSerializer.transform_method = nil + MovieSerializer.set_type :movie end it 'returns correct hash which type equals transformed set_type value' do From 64f7b6c50d6104dee7b7957dfd518a01bf3f2277 Mon Sep 17 00:00:00 2001 From: Kenji Sakurai Date: Sun, 16 Sep 2018 14:18:10 +0900 Subject: [PATCH 06/18] Add spec for singular and plural, so remove same checking example. --- .../object_serializer_class_methods_spec.rb | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/spec/lib/object_serializer_class_methods_spec.rb b/spec/lib/object_serializer_class_methods_spec.rb index 233608e0..347b1883 100644 --- a/spec/lib/object_serializer_class_methods_spec.rb +++ b/spec/lib/object_serializer_class_methods_spec.rb @@ -416,7 +416,7 @@ def year_since_release_calculator(release_year) subject(:serializable_hash) { MovieSerializer.new(movie).serializable_hash } before do - MovieSerializer.set_type :foo_bar + MovieSerializer.set_type type_name MovieSerializer.set_key_transform :camel end @@ -425,8 +425,20 @@ def year_since_release_calculator(release_year) MovieSerializer.set_type :movie end - it 'returns correct hash which type equals transformed set_type value' do - expect(serializable_hash[:data][:type]).to eq :FooBar + context 'when sets singular type name' do + let(:type_name) { :film } + + it 'returns correct hash which type equals transformed set_type value' do + expect(serializable_hash[:data][:type]).to eq :Film + end + end + + context 'when sets plural type name' do + let(:type_name) { :films } + + it 'returns correct hash which type equals transformed set_type value' do + expect(serializable_hash[:data][:type]).to eq :Films + end end end end From 9bff4548065ed6d490af93c78473bf68eab1043f Mon Sep 17 00:00:00 2001 From: Gerrit Riessen Date: Wed, 26 Sep 2018 12:03:29 +0200 Subject: [PATCH 07/18] added spec for has_one-through relationship --- spec/lib/extensions/active_record_spec.rb | 82 +++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/spec/lib/extensions/active_record_spec.rb b/spec/lib/extensions/active_record_spec.rb index e31ffe78..cbe7c1a5 100644 --- a/spec/lib/extensions/active_record_spec.rb +++ b/spec/lib/extensions/active_record_spec.rb @@ -71,3 +71,85 @@ class Account < ActiveRecord::Base File.delete(@db_file) if File.exist?(@db_file) end end + +describe 'active record has_one through' do + # Setup DB + before(:all) do + @db_file = "test_two.db" + + # Open a database + db = SQLite3::Database.new @db_file + + # Create tables + db.execute_batch <<-SQL + create table forests ( + id int primary key, + name varchar(30) + ); + + create table trees ( + id int primary key, + forest_id int, + name varchar(30), + + FOREIGN KEY (forest_id) REFERENCES forests(id) + ); + + create table fruits ( + id int primary key, + tree_id int, + name varchar(30), + + FOREIGN KEY (tree_id) REFERENCES trees(id) + ); + SQL + + # Insert records + db.execute_batch <<-SQL + insert into forests values (1, 'sherwood'); + insert into trees values (2, 1,'pine'); + insert into fruits values (3, 2, 'pine nut'); + + insert into fruits(id,name) values (4,'apple'); + SQL + end + + # Setup Active Record + before(:all) do + class Forest < ActiveRecord::Base + has_many :trees + end + + class Tree < ActiveRecord::Base + belongs_to :forest + end + + class Fruit < ActiveRecord::Base + belongs_to :tree + has_one :forest, through: :tree + end + + ActiveRecord::Base.establish_connection( + :adapter => 'sqlite3', + :database => @db_file + ) + end + + context 'revenue' do + it 'has an forest_id' do + expect(Fruit.find(3).respond_to?(:forest_id)).to be true + expect(Fruit.find(3).forest_id).to eq 1 + expect(Fruit.find(3).forest.name).to eq "sherwood" + end + + it 'has nil if tree id not available' do + expect(Fruit.find(4).respond_to?(:tree_id)).to be true + expect(Fruit.find(4).forest_id).to eq nil + end + end + + # Clean up DB + after(:all) do + File.delete(@db_file) if File.exist?(@db_file) + end +end From 467024f8fd315a15e4f4ada98ec1fd920b30c9e7 Mon Sep 17 00:00:00 2001 From: zino Date: Tue, 2 Oct 2018 11:36:21 -0400 Subject: [PATCH 08/18] Improve readme with id_method_name --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0038b67c..1d4afbc8 100644 --- a/README.md +++ b/README.md @@ -429,7 +429,7 @@ set_type | Type name of Object | ```set_type :movie ``` key | Key of Object | ```belongs_to :owner, key: :user ``` set_id | ID of Object | ```set_id :owner_id ``` cache_options | Hash to enable caching and set cache length | ```cache_options enabled: true, cache_length: 12.hours, race_condition_ttl: 10.seconds``` -id_method_name | Set custom method name to get ID of an object | ```has_many :locations, id_method_name: :place_ids ``` +id_method_name | Set custom method name to get ID of an object (If block is provided for the relationship, `id_method_name` is invoked on the return value of the block instead of the resource object) | ```has_many :locations, id_method_name: :place_ids ``` object_method_name | Set custom method name to get related objects | ```has_many :locations, object_method_name: :places ``` record_type | Set custom Object Type for a relationship | ```belongs_to :owner, record_type: :user``` serializer | Set custom Serializer for a relationship | ```has_many :actors, serializer: :custom_actor``` or ```has_many :actors, serializer: MyApp::Api::V1::ActorSerializer``` From 1efdd3372d664c41d7c044f0188349ef4d360d32 Mon Sep 17 00:00:00 2001 From: nikz Date: Tue, 2 Oct 2018 22:07:38 +0100 Subject: [PATCH 09/18] 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 50caf310..889bfaba 100644 --- a/lib/fast_jsonapi/relationship.rb +++ b/lib/fast_jsonapi/relationship.rb @@ -30,11 +30,11 @@ def initialize( @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 1ab5cd387a38d9bf390bd6e3c1107117183a3534 Mon Sep 17 00:00:00 2001 From: Austin Matzko Date: Wed, 3 Oct 2018 17:44:43 -0400 Subject: [PATCH 10/18] Don't share data_links among inherited serializers. --- lib/fast_jsonapi/object_serializer.rb | 2 +- spec/lib/object_serializer_class_methods_spec.rb | 16 ++++++++++++++++ spec/shared/contexts/movie_context.rb | 15 +++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index 76caa7d1..18950710 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -117,7 +117,7 @@ def inherited(subclass) subclass.transform_method = transform_method subclass.cache_length = cache_length subclass.race_condition_ttl = race_condition_ttl - subclass.data_links = data_links + subclass.data_links = data_links.dup subclass.cached = cached subclass.set_type(subclass.reflected_record_type) if subclass.reflected_record_type subclass.meta_to_serialize = meta_to_serialize diff --git a/spec/lib/object_serializer_class_methods_spec.rb b/spec/lib/object_serializer_class_methods_spec.rb index 347b1883..c72a8f40 100644 --- a/spec/lib/object_serializer_class_methods_spec.rb +++ b/spec/lib/object_serializer_class_methods_spec.rb @@ -355,6 +355,22 @@ def year_since_release_calculator(release_year) expect(serializable_hash[:data][:links][:url]).to eq movie.url end end + + context 'when inheriting from a parent serializer' do + before do + MovieSerializer.link(:url) do |movie_object| + "http://movies.com/#{movie_object.id}" + end + end + subject(:action_serializable_hash) { ActionMovieSerializer.new(movie).serializable_hash } + subject(:horror_serializable_hash) { HorrorMovieSerializer.new(movie).serializable_hash } + + let(:url) { "http://movies.com/#{movie.id}" } + + it 'returns the link for the correct sub-class' do + expect(action_serializable_hash[:data][:links][:url]).to eq "/action-movie/#{movie.id}" + end + end end describe '#key_transform' do diff --git a/spec/shared/contexts/movie_context.rb b/spec/shared/contexts/movie_context.rb index f2c7697a..3f144353 100644 --- a/spec/shared/contexts/movie_context.rb +++ b/spec/shared/contexts/movie_context.rb @@ -181,6 +181,18 @@ class MovieSerializer has_one :advertising_campaign end + class GenreMovieSerializer < MovieSerializer + link(:something) { '/something/' } + end + + class ActionMovieSerializer < GenreMovieSerializer + link(:url) { |object| "/action-movie/#{object.id}" } + end + + class HorrorMovieSerializer < GenreMovieSerializer + link(:url) { |object| "/horror-movie/#{object.id}" } + end + class MovieWithoutIdStructSerializer include FastJsonapi::ObjectSerializer attributes :name, :release_year @@ -354,6 +366,9 @@ class MovieSerializer after(:context) do classes_to_remove = %i[ + ActionMovieSerializer + GenreMovieSerializer + HorrorMovieSerializer Movie MovieSerializer Actor From be701f3e06ee1fec0399f4cead255f23a56a6458 Mon Sep 17 00:00:00 2001 From: Austin Matzko Date: Wed, 3 Oct 2018 17:59:58 -0400 Subject: [PATCH 11/18] Don't attempt to dup a nil --- lib/fast_jsonapi/object_serializer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index 18950710..8d3ef03d 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -117,7 +117,7 @@ def inherited(subclass) subclass.transform_method = transform_method subclass.cache_length = cache_length subclass.race_condition_ttl = race_condition_ttl - subclass.data_links = data_links.dup + subclass.data_links = data_links.dup if data_links.present? subclass.cached = cached subclass.set_type(subclass.reflected_record_type) if subclass.reflected_record_type subclass.meta_to_serialize = meta_to_serialize From 85b41c45d4c1f1328908811d353054cada306f2b Mon Sep 17 00:00:00 2001 From: nikz Date: Sun, 7 Oct 2018 21:23:36 +0100 Subject: [PATCH 12/18] 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 3af3e488..057c8bde 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 3b84b1d2..71f64bc4 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -250,7 +250,8 @@ def create_relationship(base_key, relationship_type, options, block) 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 889bfaba..e8163a68 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 @@ def initialize( cached: false, polymorphic:, conditional_proc:, - links: + links:, + lazy_load_data: false ) @key = key @name = name @@ -28,14 +29,17 @@ def initialize( @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 6da5052d..8c2f2725 100644 --- a/spec/lib/object_serializer_relationship_links_spec.rb +++ b/spec/lib/object_serializer_relationship_links_spec.rb @@ -6,23 +6,23 @@ 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 @@ class MovieSerializer 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 From 05ad93084b28d89d9621e784c9eadec6664ed4f4 Mon Sep 17 00:00:00 2001 From: Les Fletcher Date: Tue, 9 Oct 2018 14:53:57 -0700 Subject: [PATCH 13/18] =?UTF-8?q?`require=20'active=5Fsupport/core=5Fext/n?= =?UTF-8?q?umeric/time=E2=80=99`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `5.minutes` was failing in the performance spec --- lib/fast_jsonapi/object_serializer.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index 76caa7d1..c515b181 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -3,6 +3,7 @@ require 'active_support/json' require 'active_support/concern' require 'active_support/inflector' +require 'active_support/core_ext/numeric/time' require 'fast_jsonapi/attribute' require 'fast_jsonapi/relationship' require 'fast_jsonapi/link' From 9fa26fa58800463ea4919eb2b4ab8fd8be2805c1 Mon Sep 17 00:00:00 2001 From: Larissa Reis Date: Thu, 4 Oct 2018 22:14:02 -0600 Subject: [PATCH 14/18] Allow block for id customization Allow an ID of object to be customized directly on the serializer by passing a block to `set_id` as opposed to only through a model property. We already allow for attributes that do not have a model property of the same name to be customized directly on the serializer using a block. This customization can be useful in situation in which you have different classes being serialized using the same serializer. For example, if we have `HorrorMovie`, `ComedyMovie` and `DramaMovie` using the same `MovieSerializer`, we can unify their IDs using ``` class MovieSerializer include FastJsonapi::ObjectSerializer attributes :name, :year set_id do |record| "#{record.name.downcase}-#{record.id}" end ``` which is preferable to creating a `#serialized_id` method in every model that will use `MovieSerializer` to encapsulate the customization. Closes #315 --- lib/fast_jsonapi/object_serializer.rb | 4 +- lib/fast_jsonapi/serialization_core.rb | 7 ++- .../object_serializer_class_methods_spec.rb | 59 ++++++++++++++----- 3 files changed, 50 insertions(+), 20 deletions(-) diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index 76caa7d1..355c8ba2 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -167,8 +167,8 @@ def set_type(type_name) self.record_type = run_key_transform(type_name) end - def set_id(id_name) - self.record_id = id_name + def set_id(id_name = nil, &block) + self.record_id = block || id_name end def cache_options(cache_options) diff --git a/lib/fast_jsonapi/serialization_core.rb b/lib/fast_jsonapi/serialization_core.rb index a456b21e..200af9b3 100644 --- a/lib/fast_jsonapi/serialization_core.rb +++ b/lib/fast_jsonapi/serialization_core.rb @@ -86,9 +86,10 @@ def record_hash(record, fieldset, params = {}) end def id_from_record(record) - return record.send(record_id) if record_id - raise MandatoryField, 'id is a mandatory field in the jsonapi spec' unless record.respond_to?(:id) - record.id + return record_id.call(record) if record_id.is_a?(Proc) + return record.send(record_id) if record_id + raise MandatoryField, 'id is a mandatory field in the jsonapi spec' unless record.respond_to?(:id) + record.id end # Override #to_json for alternative implementation diff --git a/spec/lib/object_serializer_class_methods_spec.rb b/spec/lib/object_serializer_class_methods_spec.rb index 347b1883..2c2ed6b5 100644 --- a/spec/lib/object_serializer_class_methods_spec.rb +++ b/spec/lib/object_serializer_class_methods_spec.rb @@ -195,28 +195,57 @@ describe '#set_id' do subject(:serializable_hash) { MovieSerializer.new(resource).serializable_hash } - before do - MovieSerializer.set_id :owner_id - end + context 'method name' do + before do + MovieSerializer.set_id :owner_id + end - after do - MovieSerializer.set_id nil - end + after do + MovieSerializer.set_id nil + end + + context 'when one record is given' do + let(:resource) { movie } + + it 'returns correct hash which id equals owner_id' do + expect(serializable_hash[:data][:id].to_i).to eq movie.owner_id + end + end - context 'when one record is given' do - let(:resource) { movie } + context 'when an array of records is given' do + let(:resource) { [movie, movie] } - it 'returns correct hash which id equals owner_id' do - expect(serializable_hash[:data][:id].to_i).to eq movie.owner_id + it 'returns correct hash which id equals owner_id' do + expect(serializable_hash[:data][0][:id].to_i).to eq movie.owner_id + expect(serializable_hash[:data][1][:id].to_i).to eq movie.owner_id + end end end - context 'when an array of records is given' do - let(:resource) { [movie, movie] } + context 'with block' do + before do + MovieSerializer.set_id { |record| "movie-#{record.owner_id}" } + end + + after do + MovieSerializer.set_id nil + end - it 'returns correct hash which id equals owner_id' do - expect(serializable_hash[:data][0][:id].to_i).to eq movie.owner_id - expect(serializable_hash[:data][1][:id].to_i).to eq movie.owner_id + context 'when one record is given' do + let(:resource) { movie } + + it 'returns correct hash which id equals movie-id' do + expect(serializable_hash[:data][:id]).to eq "movie-#{movie.owner_id}" + end + end + + context 'when an array of records is given' do + let(:resource) { [movie, movie] } + + it 'returns correct hash which id equals movie-id' do + expect(serializable_hash[:data][0][:id]).to eq "movie-#{movie.owner_id}" + expect(serializable_hash[:data][1][:id]).to eq "movie-#{movie.owner_id}" + end end end end From dbda6b615315e72e1e7e5a1781bf906a955e5c24 Mon Sep 17 00:00:00 2001 From: Keith Walsh Date: Wed, 17 Oct 2018 09:29:49 -0400 Subject: [PATCH 15/18] Update README.md Resolve minor typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0038b67c..7b9671fb 100644 --- a/README.md +++ b/README.md @@ -221,7 +221,7 @@ end ``` ### Links Per Object -Links are defined in FastJsonapi using the `link` method. By default, link are read directly from the model property of the same name. In this example, `public_url` is expected to be a property of the object being serialized. +Links are defined in FastJsonapi using the `link` method. By default, links are read directly from the model property of the same name. In this example, `public_url` is expected to be a property of the object being serialized. You can configure the method to use on the object for example a link with key `self` will get set to the value returned by a method called `url` on the movie object. From d5ea95370f6c560d33fcad5acf8cba99bf5ffdef Mon Sep 17 00:00:00 2001 From: Maros Hluska Date: Thu, 18 Oct 2018 22:59:32 +0700 Subject: [PATCH 16/18] Fix params not being hash by default --- lib/fast_jsonapi/object_serializer.rb | 2 ++ .../object_serializer_attribute_param_spec.rb | 2 +- spec/lib/object_serializer_spec.rb | 6 ++++++ spec/shared/contexts/movie_context.rb | 20 +++++++++++++++---- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index 8d3ef03d..6c56b2c0 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'active_support/time' require 'active_support/json' require 'active_support/concern' require 'active_support/inflector' @@ -72,6 +73,7 @@ def serialized_json def process_options(options) @fieldsets = deep_symbolize(options[:fields].presence || {}) + @params = {} return if options.blank? diff --git a/spec/lib/object_serializer_attribute_param_spec.rb b/spec/lib/object_serializer_attribute_param_spec.rb index f92b9c76..3765a8cf 100644 --- a/spec/lib/object_serializer_attribute_param_spec.rb +++ b/spec/lib/object_serializer_attribute_param_spec.rb @@ -15,7 +15,7 @@ def viewed?(user) class MovieSerializer attribute :viewed do |movie, params| - params ? movie.viewed?(params[:user]) : false + params[:user] ? movie.viewed?(params[:user]) : false end attribute :no_param_attribute do |movie| diff --git a/spec/lib/object_serializer_spec.rb b/spec/lib/object_serializer_spec.rb index 85f4a79d..ef755ccc 100644 --- a/spec/lib/object_serializer_spec.rb +++ b/spec/lib/object_serializer_spec.rb @@ -504,4 +504,10 @@ class BlahSerializer end end end + + context 'when attribute contents are determined by params data' do + it 'does not throw an error with no params are passed' do + expect { MovieOptionalAttributeContentsWithParamsSerializer.new(movie).serialized_json }.not_to raise_error + end + end end diff --git a/spec/shared/contexts/movie_context.rb b/spec/shared/contexts/movie_context.rb index 3f144353..4d711194 100644 --- a/spec/shared/contexts/movie_context.rb +++ b/spec/shared/contexts/movie_context.rb @@ -240,8 +240,8 @@ class AwardSerializer include FastJsonapi::ObjectSerializer attributes :id, :title attribute :year, if: Proc.new { |record, params| - params[:include_award_year].present? ? - params[:include_award_year] : + params[:include_award_year].present? ? + params[:include_award_year] : false } belongs_to :actor @@ -313,7 +313,7 @@ class MovieOptionalParamsDataSerializer include FastJsonapi::ObjectSerializer set_type :movie attributes :name - attribute :director, if: Proc.new { |record, params| params && params[:admin] == true } + attribute :director, if: Proc.new { |record, params| params[:admin] == true } end class MovieOptionalRelationshipSerializer @@ -327,7 +327,19 @@ class MovieOptionalRelationshipWithParamsSerializer include FastJsonapi::ObjectSerializer set_type :movie attributes :name - belongs_to :owner, record_type: :user, if: Proc.new { |record, params| params && params[:admin] == true } + belongs_to :owner, record_type: :user, if: Proc.new { |record, params| params[:admin] == true } + end + + class MovieOptionalAttributeContentsWithParamsSerializer + include FastJsonapi::ObjectSerializer + set_type :movie + attributes :name + attribute :director do |record, params| + data = {} + data[:first_name] = 'steven' + data[:last_name] = 'spielberg' if params[:admin] + data + end end end From 0ba5f231fec2595593e51b02ed797efd7f87af87 Mon Sep 17 00:00:00 2001 From: Larissa Reis Date: Thu, 18 Oct 2018 22:04:52 -0600 Subject: [PATCH 17/18] Add set_id block syntax example to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0038b67c..0df2ba0c 100644 --- a/README.md +++ b/README.md @@ -427,7 +427,7 @@ Option | Purpose | Example ------------ | ------------- | ------------- set_type | Type name of Object | ```set_type :movie ``` key | Key of Object | ```belongs_to :owner, key: :user ``` -set_id | ID of Object | ```set_id :owner_id ``` +set_id | ID of Object | ```set_id :owner_id ``` or ```set_id { |record| "#{record.name.downcase}-#{record.id}" }``` cache_options | Hash to enable caching and set cache length | ```cache_options enabled: true, cache_length: 12.hours, race_condition_ttl: 10.seconds``` id_method_name | Set custom method name to get ID of an object | ```has_many :locations, id_method_name: :place_ids ``` object_method_name | Set custom method name to get related objects | ```has_many :locations, object_method_name: :places ``` From ee76e0c69b31528a223c57830ed178969b29b80b Mon Sep 17 00:00:00 2001 From: Shishir Kakaraddi Date: Sat, 3 Nov 2018 12:11:56 -0700 Subject: [PATCH 18/18] bump up version to 1.5 --- lib/fast_jsonapi/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fast_jsonapi/version.rb b/lib/fast_jsonapi/version.rb index c9c4c7ab..12442c14 100644 --- a/lib/fast_jsonapi/version.rb +++ b/lib/fast_jsonapi/version.rb @@ -1,3 +1,3 @@ module FastJsonapi - VERSION = "1.4" + VERSION = "1.5" end