From 449c1bf05fb6144b39fd1523eb7aa96f9af81d0a Mon Sep 17 00:00:00 2001 From: Erol Date: Fri, 13 Jul 2018 12:35:43 +0800 Subject: [PATCH 01/10] Allow passing procs with variable arguments when declaring an attribute --- lib/fast_jsonapi/attribute.rb | 4 ++-- spec/lib/object_serializer_class_methods_spec.rb | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/fast_jsonapi/attribute.rb b/lib/fast_jsonapi/attribute.rb index c26bf19..9acc3e0 100644 --- a/lib/fast_jsonapi/attribute.rb +++ b/lib/fast_jsonapi/attribute.rb @@ -11,7 +11,7 @@ module FastJsonapi def serialize(record, serialization_params, output_hash) if include_attribute?(record, serialization_params) output_hash[key] = if method.is_a?(Proc) - method.arity == 1 ? method.call(record) : method.call(record, serialization_params) + method.arity.abs == 1 ? method.call(record) : method.call(record, serialization_params) else record.public_send(method) end @@ -26,4 +26,4 @@ module FastJsonapi end end end -end \ No newline at end of file +end diff --git a/spec/lib/object_serializer_class_methods_spec.rb b/spec/lib/object_serializer_class_methods_spec.rb index 85346f0..761900e 100644 --- a/spec/lib/object_serializer_class_methods_spec.rb +++ b/spec/lib/object_serializer_class_methods_spec.rb @@ -230,6 +230,22 @@ describe FastJsonapi::ObjectSerializer do expect(serializable_hash[:data][:attributes][:title_with_year]).to eq "#{movie.name} (#{movie.release_year})" end end + + context 'with &:proc' do + before do + movie.release_year = 2008 + MovieSerializer.attribute :released_in_year, &:release_year + end + + after do + MovieSerializer.attributes_to_serialize.delete(:released_in_year) + end + + it 'returns correct hash when serializable_hash is called' do + expect(serializable_hash[:data][:attributes][:name]).to eq movie.name + expect(serializable_hash[:data][:attributes][:released_in_year]).to eq movie.release_year + end + end end describe '#link' do From e05193fb5e6de1c901f95bfce76523fd5de4b0ba Mon Sep 17 00:00:00 2001 From: Erol Date: Sat, 14 Jul 2018 17:11:24 +0800 Subject: [PATCH 02/10] Add spec for proc methods with optional arguments --- spec/lib/object_serializer_class_methods_spec.rb | 3 ++- spec/shared/contexts/movie_context.rb | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/spec/lib/object_serializer_class_methods_spec.rb b/spec/lib/object_serializer_class_methods_spec.rb index 761900e..3c477c3 100644 --- a/spec/lib/object_serializer_class_methods_spec.rb +++ b/spec/lib/object_serializer_class_methods_spec.rb @@ -235,6 +235,7 @@ describe FastJsonapi::ObjectSerializer do before do movie.release_year = 2008 MovieSerializer.attribute :released_in_year, &:release_year + MovieSerializer.attribute :name, &:local_name end after do @@ -242,7 +243,7 @@ describe FastJsonapi::ObjectSerializer do end it 'returns correct hash when serializable_hash is called' do - expect(serializable_hash[:data][:attributes][:name]).to eq movie.name + expect(serializable_hash[:data][:attributes][:name]).to eq "english #{movie.name}" expect(serializable_hash[:data][:attributes][:released_in_year]).to eq movie.release_year end end diff --git a/spec/shared/contexts/movie_context.rb b/spec/shared/contexts/movie_context.rb index bbd89a9..9061226 100644 --- a/spec/shared/contexts/movie_context.rb +++ b/spec/shared/contexts/movie_context.rb @@ -54,6 +54,10 @@ RSpec.shared_context 'movie class' do "#{id}" end + def local_name(locale = :english) + "#{locale} #{name}" + end + def url "http://movies.com/#{id}" end From 41c1e0a106d97de9f48fb6ce427b921c4a8a5d9d Mon Sep 17 00:00:00 2001 From: Erol Date: Mon, 16 Jul 2018 06:19:54 +0800 Subject: [PATCH 03/10] Add proc shortcut use case to README --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index f4333d1..c812eac 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,18 @@ class MovieSerializer end ``` +Attributes can also use a different name by passing the original method or accessor with a proc shortcut: + +```ruby +class MovieSerializer + include FastJsonapi::ObjectSerializer + + attributes :name + + attribute :released_in_year, &:year +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. From a363c90bfbc39eee08f7d827ff27a6eea538cbff Mon Sep 17 00:00:00 2001 From: Erol Date: Mon, 16 Jul 2018 17:34:42 +0800 Subject: [PATCH 04/10] Allow the serializer to return sparse fieldsets --- lib/fast_jsonapi/fieldset.rb | 7 +++++ lib/fast_jsonapi/object_serializer.rb | 25 +++++++++++++--- lib/fast_jsonapi/serialization_core.rb | 29 ++++++++++-------- spec/lib/object_serializer_fields_spec.rb | 36 +++++++++++++++++++++++ spec/lib/serialization_core_spec.rb | 2 +- 5 files changed, 82 insertions(+), 17 deletions(-) create mode 100644 lib/fast_jsonapi/fieldset.rb create mode 100644 spec/lib/object_serializer_fields_spec.rb diff --git a/lib/fast_jsonapi/fieldset.rb b/lib/fast_jsonapi/fieldset.rb new file mode 100644 index 0000000..0788a67 --- /dev/null +++ b/lib/fast_jsonapi/fieldset.rb @@ -0,0 +1,7 @@ +module FastJsonapi + class Fieldset + def initialize(fields) + @fields = fields + end + end +end diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index 90771b4..cbadf40 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -7,6 +7,7 @@ require 'fast_jsonapi/attribute' require 'fast_jsonapi/relationship' require 'fast_jsonapi/link' require 'fast_jsonapi/serialization_core' +require 'fast_jsonapi/fieldset' module FastJsonapi module ObjectSerializer @@ -41,8 +42,8 @@ module FastJsonapi return serializable_hash unless @resource - serializable_hash[:data] = self.class.record_hash(@resource, @params) - serializable_hash[:included] = self.class.get_included_records(@resource, @includes, @known_included_objects, @params) if @includes.present? + serializable_hash[:data] = self.class.record_hash(@resource, @fieldsets[self.class.reflected_record_type.to_sym], @params) + serializable_hash[:included] = self.class.get_included_records(@resource, @includes, @known_included_objects, @fieldsets, @params) if @includes.present? serializable_hash end @@ -51,9 +52,10 @@ module FastJsonapi data = [] included = [] + fieldset = @fieldsets[self.class.reflected_record_type.to_sym] @resource.each do |record| - data << self.class.record_hash(record, @params) - included.concat self.class.get_included_records(record, @includes, @known_included_objects, @params) if @includes.present? + data << self.class.record_hash(record, fieldset, @params) + included.concat self.class.get_included_records(record, @includes, @known_included_objects, @fieldsets, @params) if @includes.present? end serializable_hash[:data] = data @@ -70,6 +72,8 @@ module FastJsonapi private def process_options(options) + @fieldsets = deep_symbolize(options[:fields].presence || {}) + return if options.blank? @known_included_objects = {} @@ -85,6 +89,18 @@ module FastJsonapi end end + def deep_symbolize(collection) + if collection.is_a? Hash + Hash[collection.map do |k, v| + [k.to_sym, deep_symbolize(v)] + end] + elsif collection.is_a? Array + collection.map { |i| deep_symbolize(i) } + else + collection.to_sym + end + end + def is_collection?(resource, force_is_collection = nil) return force_is_collection unless force_is_collection.nil? @@ -104,6 +120,7 @@ module FastJsonapi subclass.race_condition_ttl = race_condition_ttl subclass.data_links = data_links subclass.cached = cached + subclass.fieldset = fieldset.dup if fieldset.present? end def reflected_record_type diff --git a/lib/fast_jsonapi/serialization_core.rb b/lib/fast_jsonapi/serialization_core.rb index 11257aa..03df974 100644 --- a/lib/fast_jsonapi/serialization_core.rb +++ b/lib/fast_jsonapi/serialization_core.rb @@ -21,7 +21,8 @@ module FastJsonapi :cache_length, :race_condition_ttl, :cached, - :data_links + :data_links, + :fieldset end end @@ -40,27 +41,30 @@ module FastJsonapi end end - def attributes_hash(record, params = {}) - attributes_to_serialize.each_with_object({}) do |(_k, attribute), hash| + def attributes_hash(record, fieldset = nil, params = {}) + attributes = attributes_to_serialize + attributes = attributes.slice(*fieldset) if fieldset.present? + attributes.each_with_object({}) do |(_k, attribute), hash| attribute.serialize(record, params, hash) end end - def relationships_hash(record, relationships = nil, params = {}) + def relationships_hash(record, relationships = nil, fieldset = nil, params = {}) relationships = relationships_to_serialize if relationships.nil? + relationships = relationships.slice(*fieldset) if fieldset.present? relationships.each_with_object({}) do |(_k, relationship), hash| relationship.serialize(record, params, hash) end end - def record_hash(record, params = {}) + def record_hash(record, fieldset, params = {}) if cached record_hash = Rails.cache.fetch(record.cache_key, expires_in: cache_length, race_condition_ttl: race_condition_ttl) do temp_hash = id_hash(id_from_record(record), record_type, true) - temp_hash[:attributes] = attributes_hash(record, params) if attributes_to_serialize.present? + temp_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present? temp_hash[:relationships] = {} - temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize, params) if cachable_relationships_to_serialize.present? + temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize, fieldset, params) if cachable_relationships_to_serialize.present? temp_hash[:links] = links_hash(record, params) if data_links.present? temp_hash end @@ -68,8 +72,8 @@ module FastJsonapi record_hash else record_hash = id_hash(id_from_record(record), record_type, true) - record_hash[:attributes] = attributes_hash(record, params) if attributes_to_serialize.present? - record_hash[:relationships] = relationships_hash(record, nil, params) if relationships_to_serialize.present? + record_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present? + record_hash[:relationships] = relationships_hash(record, nil, fieldset, params) if relationships_to_serialize.present? record_hash[:links] = links_hash(record, params) if data_links.present? record_hash end @@ -100,7 +104,7 @@ module FastJsonapi end # includes handler - def get_included_records(record, includes_list, known_included_objects, params = {}) + def get_included_records(record, includes_list, known_included_objects, fieldsets, params = {}) return unless includes_list.present? includes_list.sort.each_with_object([]) do |include_item, included_records| @@ -120,7 +124,7 @@ module FastJsonapi included_objects.each do |inc_obj| if remaining_items(items) - serializer_records = serializer.get_included_records(inc_obj, remaining_items(items), known_included_objects) + serializer_records = serializer.get_included_records(inc_obj, remaining_items(items), known_included_objects, fieldsets) included_records.concat(serializer_records) unless serializer_records.empty? end @@ -128,7 +132,8 @@ module FastJsonapi next if known_included_objects.key?(code) known_included_objects[code] = inc_obj - included_records << serializer.record_hash(inc_obj, params) + + included_records << serializer.record_hash(inc_obj, fieldsets[serializer.reflected_record_type], params) end end end diff --git a/spec/lib/object_serializer_fields_spec.rb b/spec/lib/object_serializer_fields_spec.rb new file mode 100644 index 0000000..f44ab2e --- /dev/null +++ b/spec/lib/object_serializer_fields_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe FastJsonapi::ObjectSerializer do + include_context 'movie class' + + let(:fields) do + { + movie: %i[name actors], + actor: %i[name agency] + } + end + + it 'only returns specified fields' do + hash = MovieSerializer.new(movie, fields: fields).serializable_hash + + expect(hash[:data][:attributes].keys.sort).to eq %i[name] + end + + it 'only returns specified relationships' do + hash = MovieSerializer.new(movie, fields: fields).serializable_hash + + expect(hash[:data][:relationships].keys.sort).to eq %i[actors] + end + + it 'only returns specified fields for included relationships' do + hash = MovieSerializer.new(movie, fields: fields, include: %i[actors]).serializable_hash + + expect(hash[:included].first[:attributes].keys.sort).to eq %i[name] + end + + it 'only returns specified relationships for included relationships' do + hash = MovieSerializer.new(movie, fields: fields, include: %i[actors]).serializable_hash + + expect(hash[:included].first[:relationships].keys.sort).to eq %i[agency] + end +end diff --git a/spec/lib/serialization_core_spec.rb b/spec/lib/serialization_core_spec.rb index d161e3f..adf33df 100644 --- a/spec/lib/serialization_core_spec.rb +++ b/spec/lib/serialization_core_spec.rb @@ -64,7 +64,7 @@ describe FastJsonapi::ObjectSerializer do known_included_objects = {} included_records = [] [movie, movie].each do |record| - included_records.concat MovieSerializer.send(:get_included_records, record, includes_list, known_included_objects, nil) + included_records.concat MovieSerializer.send(:get_included_records, record, includes_list, known_included_objects, {}, nil) end expect(included_records.size).to eq 3 end From e683bbfb783b16fe3a220ea44eb876d2f1770ef9 Mon Sep 17 00:00:00 2001 From: Erol Date: Mon, 16 Jul 2018 17:59:54 +0800 Subject: [PATCH 05/10] Update spec with included documents with no explicitly given fields --- spec/lib/object_serializer_fields_spec.rb | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/spec/lib/object_serializer_fields_spec.rb b/spec/lib/object_serializer_fields_spec.rb index f44ab2e..913ba83 100644 --- a/spec/lib/object_serializer_fields_spec.rb +++ b/spec/lib/object_serializer_fields_spec.rb @@ -5,7 +5,7 @@ describe FastJsonapi::ObjectSerializer do let(:fields) do { - movie: %i[name actors], + movie: %i[name actors advertising_campaign], actor: %i[name agency] } end @@ -19,7 +19,7 @@ describe FastJsonapi::ObjectSerializer do it 'only returns specified relationships' do hash = MovieSerializer.new(movie, fields: fields).serializable_hash - expect(hash[:data][:relationships].keys.sort).to eq %i[actors] + expect(hash[:data][:relationships].keys.sort).to eq %i[actors advertising_campaign] end it 'only returns specified fields for included relationships' do @@ -29,8 +29,20 @@ describe FastJsonapi::ObjectSerializer do end it 'only returns specified relationships for included relationships' do - hash = MovieSerializer.new(movie, fields: fields, include: %i[actors]).serializable_hash + hash = MovieSerializer.new(movie, fields: fields, include: %i[actors advertising_campaign]).serializable_hash expect(hash[:included].first[:relationships].keys.sort).to eq %i[agency] end + + it 'returns all fields for included relationships when no explicit fields have been specified' do + hash = MovieSerializer.new(movie, fields: fields, include: %i[actors advertising_campaign]).serializable_hash + + expect(hash[:included][3][:attributes].keys.sort).to eq %i[id name] + end + + it 'returns all fields for included relationships when no explicit fields have been specified' do + hash = MovieSerializer.new(movie, fields: fields, include: %i[actors advertising_campaign]).serializable_hash + + expect(hash[:included][3][:relationships].keys.sort).to eq %i[movie] + end end From ab652c44005b1782837a513b7a3b63a901848f7e Mon Sep 17 00:00:00 2001 From: Erol Date: Tue, 17 Jul 2018 11:01:33 +0800 Subject: [PATCH 06/10] Remove unused code --- lib/fast_jsonapi/fieldset.rb | 7 ------- lib/fast_jsonapi/object_serializer.rb | 2 -- lib/fast_jsonapi/serialization_core.rb | 3 +-- 3 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 lib/fast_jsonapi/fieldset.rb diff --git a/lib/fast_jsonapi/fieldset.rb b/lib/fast_jsonapi/fieldset.rb deleted file mode 100644 index 0788a67..0000000 --- a/lib/fast_jsonapi/fieldset.rb +++ /dev/null @@ -1,7 +0,0 @@ -module FastJsonapi - class Fieldset - def initialize(fields) - @fields = fields - end - end -end diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index cbadf40..7d58e67 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -7,7 +7,6 @@ require 'fast_jsonapi/attribute' require 'fast_jsonapi/relationship' require 'fast_jsonapi/link' require 'fast_jsonapi/serialization_core' -require 'fast_jsonapi/fieldset' module FastJsonapi module ObjectSerializer @@ -120,7 +119,6 @@ module FastJsonapi subclass.race_condition_ttl = race_condition_ttl subclass.data_links = data_links subclass.cached = cached - subclass.fieldset = fieldset.dup if fieldset.present? end def reflected_record_type diff --git a/lib/fast_jsonapi/serialization_core.rb b/lib/fast_jsonapi/serialization_core.rb index 03df974..8b3ea11 100644 --- a/lib/fast_jsonapi/serialization_core.rb +++ b/lib/fast_jsonapi/serialization_core.rb @@ -21,8 +21,7 @@ module FastJsonapi :cache_length, :race_condition_ttl, :cached, - :data_links, - :fieldset + :data_links end end From e2bf5411a2d3bace9dafac5f1a3c9694a7735d12 Mon Sep 17 00:00:00 2001 From: Erol Date: Tue, 17 Jul 2018 11:54:48 +0800 Subject: [PATCH 07/10] Set the record type for inherited serializers --- lib/fast_jsonapi/object_serializer.rb | 1 + spec/lib/object_serializer_inheritance_spec.rb | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index 7d58e67..2cc1b35 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -119,6 +119,7 @@ module FastJsonapi subclass.race_condition_ttl = race_condition_ttl subclass.data_links = data_links subclass.cached = cached + subclass.set_type(subclass.reflected_record_type) if subclass.reflected_record_type end def reflected_record_type diff --git a/spec/lib/object_serializer_inheritance_spec.rb b/spec/lib/object_serializer_inheritance_spec.rb index 8cf5b53..beb7487 100644 --- a/spec/lib/object_serializer_inheritance_spec.rb +++ b/spec/lib/object_serializer_inheritance_spec.rb @@ -95,6 +95,11 @@ describe FastJsonapi::ObjectSerializer do has_one :account end + it 'sets the correct record type' do + expect(EmployeeSerializer.reflected_record_type).to eq :employee + expect(EmployeeSerializer.record_type).to eq :employee + end + context 'when testing inheritance of attributes' do it 'includes parent attributes' do From fa194133fa5931ae5c70c6ae31c13547ff16fc81 Mon Sep 17 00:00:00 2001 From: Erol Date: Tue, 17 Jul 2018 11:56:36 +0800 Subject: [PATCH 08/10] Use record type instead of reflected record type --- lib/fast_jsonapi/object_serializer.rb | 4 ++-- lib/fast_jsonapi/serialization_core.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index 2cc1b35..7f740c8 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -41,7 +41,7 @@ module FastJsonapi return serializable_hash unless @resource - serializable_hash[:data] = self.class.record_hash(@resource, @fieldsets[self.class.reflected_record_type.to_sym], @params) + serializable_hash[:data] = self.class.record_hash(@resource, @fieldsets[self.class.record_type.to_sym], @params) serializable_hash[:included] = self.class.get_included_records(@resource, @includes, @known_included_objects, @fieldsets, @params) if @includes.present? serializable_hash end @@ -51,7 +51,7 @@ module FastJsonapi data = [] included = [] - fieldset = @fieldsets[self.class.reflected_record_type.to_sym] + fieldset = @fieldsets[self.class.record_type.to_sym] @resource.each do |record| data << self.class.record_hash(record, fieldset, @params) included.concat self.class.get_included_records(record, @includes, @known_included_objects, @fieldsets, @params) if @includes.present? diff --git a/lib/fast_jsonapi/serialization_core.rb b/lib/fast_jsonapi/serialization_core.rb index 8b3ea11..6ec069a 100644 --- a/lib/fast_jsonapi/serialization_core.rb +++ b/lib/fast_jsonapi/serialization_core.rb @@ -132,7 +132,7 @@ module FastJsonapi known_included_objects[code] = inc_obj - included_records << serializer.record_hash(inc_obj, fieldsets[serializer.reflected_record_type], params) + included_records << serializer.record_hash(inc_obj, fieldsets[serializer.record_type], params) end end end From 9aec7c58ede732b19643309383c0e74d289efeb9 Mon Sep 17 00:00:00 2001 From: Erol Date: Tue, 17 Jul 2018 12:02:58 +0800 Subject: [PATCH 09/10] Add README instructions for using sparse fieldsets --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index c812eac..a03e1a8 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Fast JSON API serialized 250 records in 3.01 ms * [Params](#params) * [Conditional Attributes](#conditional-attributes) * [Conditional Relationships](#conditional-relationships) + * [Sparse Fieldsets](#sparse-fieldsets) * [Contributing](#contributing) @@ -388,6 +389,21 @@ serializer = MovieSerializer.new(movie, { params: { admin: current_user.admin? } serializer.serializable_hash ``` +### Sparse Fieldsets + +Attributes and relationships can be selectively returned per record type by using the `fields` option. + +```ruby +class MovieSerializer + include FastJsonapi::ObjectSerializer + + attributes :name, :year +end + +serializer = MovieSerializer.new(movie, { fields: { movie: [:name] } }) +serializer.serializable_hash +``` + ### Customizable Options Option | Purpose | Example From 115a01a7c22564625c9fbbbc181715a7239f1eb4 Mon Sep 17 00:00:00 2001 From: Shishir Kakaraddi Date: Mon, 16 Jul 2018 21:52:58 -0700 Subject: [PATCH 10/10] bump up version to 1.3 --- 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 3c96404..f17716e 100644 --- a/lib/fast_jsonapi/version.rb +++ b/lib/fast_jsonapi/version.rb @@ -1,3 +1,3 @@ module FastJsonapi - VERSION = "1.2" + VERSION = "1.3" end