diff --git a/README.md b/README.md index 562792c..3af3e48 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,18 @@ This will create a `self` reference for the relationship, and a `related` link f end ``` +### Meta Per Resource + +For every resource in the collection, you can include a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship. +```ruby + meta do |movie| + { + years_since_release: Date.current.year - movie.year + } + end +end +``` + ### Compound Document Support for top-level and nested included associations through ` options[:include] `. @@ -381,15 +393,15 @@ class MovieSerializer include FastJsonapi::ObjectSerializer attributes :name, :year - attribute :release_year, if: Proc.new do |record| + attribute :release_year, if: Proc.new { |record| # Release year will only be serialized if it's greater than 1990 record.release_year > 1990 - end + } - attribute :director, if: Proc.new do |record, params| + attribute :director, if: Proc.new { |record, params| # The director will be serialized only if the :admin key of params is true params && params[:admin] == true - end + } end # ... diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index e662344..3b84b1d 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -120,6 +120,7 @@ module FastJsonapi subclass.data_links = data_links subclass.cached = cached subclass.set_type(subclass.reflected_record_type) if subclass.reflected_record_type + subclass.meta_to_serialize = meta_to_serialize end def reflected_record_type @@ -218,6 +219,10 @@ module FastJsonapi add_relationship(relationship) end + def meta(&block) + self.meta_to_serialize = block + end + def create_relationship(base_key, relationship_type, options, block) name = base_key.to_sym if relationship_type == :has_many @@ -232,7 +237,11 @@ module FastJsonapi Relationship.new( key: options[:key] || run_key_transform(base_key), name: name, - id_method_name: options[:id_method_name] || "#{base_serialization_key}#{id_postfix}".to_sym, + id_method_name: compute_id_method_name( + options[:id_method_name], + "#{base_serialization_key}#{id_postfix}".to_sym, + block + ), record_type: options[:record_type] || run_key_transform(base_key_sym), object_method_name: options[:object_method_name] || name, object_block: block, @@ -245,6 +254,14 @@ module FastJsonapi ) end + def compute_id_method_name(custom_id_method_name, id_method_name_from_relationship, block) + if block.present? + custom_id_method_name || :id + else + custom_id_method_name || id_method_name_from_relationship + end + end + def compute_serializer_name(serializer_key) return serializer_key unless serializer_key.is_a? Symbol namespace = self.name.gsub(/()?\w+Serializer$/, '') diff --git a/lib/fast_jsonapi/relationship.rb b/lib/fast_jsonapi/relationship.rb index d6652b1..50caf31 100644 --- a/lib/fast_jsonapi/relationship.rb +++ b/lib/fast_jsonapi/relationship.rb @@ -89,13 +89,11 @@ module FastJsonapi end def fetch_id(record, params) - unless object_block.nil? + if object_block.present? object = object_block.call(record, params) - - return object.map(&:id) if object.respond_to? :map - return object.try(:id) + return object.map { |item| item.public_send(id_method_name) } if object.respond_to? :map + return object.try(id_method_name) end - record.public_send(id_method_name) end diff --git a/lib/fast_jsonapi/serialization_core.rb b/lib/fast_jsonapi/serialization_core.rb index 6ec069a..8598fe0 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, + :meta_to_serialize end end @@ -57,6 +58,10 @@ module FastJsonapi end end + def meta_hash(record, params = {}) + meta_to_serialize.call(record, params) + end + 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 @@ -67,13 +72,15 @@ module FastJsonapi temp_hash[:links] = links_hash(record, params) if data_links.present? temp_hash end - record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize, params)) if uncachable_relationships_to_serialize.present? + record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize, fieldset, params)) if uncachable_relationships_to_serialize.present? + record_hash[:meta] = meta_hash(record, params) if meta_to_serialize.present? record_hash else record_hash = id_hash(id_from_record(record), record_type, true) 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[:meta] = meta_hash(record, params) if meta_to_serialize.present? record_hash end end @@ -123,7 +130,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, fieldsets) + serializer_records = serializer.get_included_records(inc_obj, remaining_items(items), known_included_objects, fieldsets, params) included_records.concat(serializer_records) unless serializer_records.empty? end diff --git a/spec/lib/object_serializer_class_methods_spec.rb b/spec/lib/object_serializer_class_methods_spec.rb index 3c477c3..397ace5 100644 --- a/spec/lib/object_serializer_class_methods_spec.rb +++ b/spec/lib/object_serializer_class_methods_spec.rb @@ -87,6 +87,31 @@ describe FastJsonapi::ObjectSerializer do end end + describe '#has_many with block and id_method_name' do + before do + MovieSerializer.has_many(:awards, id_method_name: :imdb_award_id) do |movie| + movie.actors.map(&:awards).flatten + end + end + + after do + MovieSerializer.relationships_to_serialize.delete(:awards) + end + + context 'awards is not included' do + subject(:hash) { MovieSerializer.new(movie).serializable_hash } + + it 'returns correct hash where id is obtained from the method specified via `id_method_name`' do + expected_award_data = movie.actors.map(&:awards).flatten.map do |actor| + { id: actor.imdb_award_id.to_s, type: actor.class.name.downcase.to_sym } + end + serialized_award_data = hash[:data][:relationships][:awards][:data] + + expect(serialized_award_data).to eq(expected_award_data) + end + end + end + describe '#belongs_to' do subject(:relationship) { MovieSerializer.relationships_to_serialize[:area] } @@ -249,6 +274,34 @@ describe FastJsonapi::ObjectSerializer do end end + describe '#meta' do + subject(:serializable_hash) { MovieSerializer.new(movie).serializable_hash } + + before do + movie.release_year = 2008 + MovieSerializer.meta do |movie| + { + years_since_release: year_since_release_calculator(movie.release_year) + } + end + end + + after do + movie.release_year = nil + MovieSerializer.meta_to_serialize = nil + end + + it 'returns correct hash when serializable_hash is called' do + expect(serializable_hash[:data][:meta]).to eq ({ years_since_release: year_since_release_calculator(movie.release_year) }) + end + + private + + def year_since_release_calculator(release_year) + Date.current.year - release_year + end + end + describe '#link' do subject(:serializable_hash) { MovieSerializer.new(movie).serializable_hash } diff --git a/spec/lib/object_serializer_spec.rb b/spec/lib/object_serializer_spec.rb index 07cbbef..06aac37 100644 --- a/spec/lib/object_serializer_spec.rb +++ b/spec/lib/object_serializer_spec.rb @@ -310,6 +310,23 @@ describe FastJsonapi::ObjectSerializer do end end + context 'when serializing included, params should be available in any serializer' do + subject(:serializable_hash) do + options = {} + options[:include] = [:"actors.awards"] + options[:params] = { include_award_year: true } + MovieSerializer.new(movie, options).serializable_hash + end + let(:actor) { movie.actors.first } + let(:award) { actor.awards.first } + let(:year) { award.year } + + it 'passes params to deeply nested includes' do + expect(year).to_not be_blank + expect(serializable_hash[:included][0][:attributes][:year]).to eq year + end + end + context 'when is_collection option present' do subject { MovieSerializer.new(resource, is_collection_options).serializable_hash } diff --git a/spec/shared/contexts/movie_context.rb b/spec/shared/contexts/movie_context.rb index 30c4d9f..52c1242 100644 --- a/spec/shared/contexts/movie_context.rb +++ b/spec/shared/contexts/movie_context.rb @@ -84,6 +84,8 @@ RSpec.shared_context 'movie class' do a.id = i a.title = "Test Award #{i}" a.actor_id = id + a.imdb_award_id = i * 10 + a.year = 1990 + i end end end @@ -114,7 +116,7 @@ RSpec.shared_context 'movie class' do end class Award - attr_accessor :id, :title, :actor_id + attr_accessor :id, :title, :actor_id, :year, :imdb_award_id end class State @@ -229,6 +231,11 @@ RSpec.shared_context 'movie class' do class AwardSerializer include FastJsonapi::ObjectSerializer attributes :id, :title + attribute :year, if: Proc.new { |record, params| + params[:include_award_year].present? ? + params[:include_award_year] : + false + } belongs_to :actor end