diff --git a/README.md b/README.md index 4e7eaa4..f4333d1 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ Fast JSON API serialized 250 records in 3.01 ms * [Collection Serialization](#collection-serialization) * [Caching](#caching) * [Params](#params) + * [Conditional Attributes](#conditional-attributes) + * [Conditional Relationships](#conditional-relationships) * [Contributing](#contributing) @@ -259,6 +261,26 @@ hash = MovieSerializer.new([movie, movie], options).serializable_hash json_string = MovieSerializer.new([movie, movie], options).serialized_json ``` +#### Control Over Collection Serialization + +You can use `is_collection` option to have better control over collection serialization. + +If this option is not provided or `nil` autedetect logic is used to try understand +if provided resource is a single object or collection. + +Autodetect logic is compatible with most DB toolkits (ActiveRecord, Sequel, etc.) but +**cannot** guarantee that single vs collection will be always detected properly. + +```ruby +options[:is_collection] +``` + +was introduced to be able to have precise control this behavior + +- `nil` or not provided: will try to autodetect single vs collection (please, see notes above) +- `true` will always treat input resource as *collection* +- `false` will always treat input resource as *single object* + ### Caching Requires a `cache_key` method be defined on model: @@ -307,6 +329,53 @@ serializer.serializable_hash Custom attributes and relationships that only receive the resource are still possible by defining the block to only receive one argument. +### Conditional Attributes + +Conditional attributes can be defined by passing a Proc to the `if` key on the `attribute` method. Return `true` if the attribute should be serialized, and `false` if not. The record and any params passed to the serializer are available inside the Proc as the first and second parameters, respectively. + +```ruby +class MovieSerializer + include FastJsonapi::ObjectSerializer + + attributes :name, :year + attribute :release_year, if: Proc.new do |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| + # The director will be serialized only if the :admin key of params is true + params && params[:admin] == true + end +end + +# ... +current_user = User.find(cookies[:current_user_id]) +serializer = MovieSerializer.new(movie, { params: { admin: current_user.admin? }}) +serializer.serializable_hash +``` + +### Conditional Relationships + +Conditional relationships can be defined by passing a Proc to the `if` key. Return `true` if the relationship should be serialized, and `false` if not. The record and any params passed to the serializer are available inside the Proc as the first and second parameters, respectively. + +```ruby +class MovieSerializer + include FastJsonapi::ObjectSerializer + + # Actors will only be serialized if the record has any associated actors + has_many :actors, if: Proc.new { |record| record.actors.any? } + + # Owner will only be serialized if the :admin key of params is true + belongs_to :owner, if: Proc.new { |record, params| params && params[:admin] == true } +end + +# ... +current_user = User.find(cookies[:current_user_id]) +serializer = MovieSerializer.new(movie, { params: { admin: current_user.admin? }}) +serializer.serializable_hash +``` + ### Customizable Options Option | Purpose | Example diff --git a/lib/extensions/has_one.rb b/lib/extensions/has_one.rb index 930ca57..1588359 100644 --- a/lib/extensions/has_one.rb +++ b/lib/extensions/has_one.rb @@ -1,20 +1,18 @@ # frozen_string_literal: true -if defined?(::ActiveRecord) - ::ActiveRecord::Associations::Builder::HasOne.class_eval do - # Based on - # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/builder/collection_association.rb#L50 - # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/builder/singular_association.rb#L11 - def self.define_accessors(mixin, reflection) - super - name = reflection.name - mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}_id - # if an attribute is already defined with this methods name we should just use it - return read_attribute(__method__) if has_attribute?(__method__) - association(:#{name}).reader.try(:id) - end - CODE - end +::ActiveRecord::Associations::Builder::HasOne.class_eval do + # Based on + # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/builder/collection_association.rb#L50 + # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/builder/singular_association.rb#L11 + def self.define_accessors(mixin, reflection) + super + name = reflection.name + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}_id + # if an attribute is already defined with this methods name we should just use it + return read_attribute(__method__) if has_attribute?(__method__) + association(:#{name}).reader.try(:id) + end + CODE end end diff --git a/lib/fast_jsonapi.rb b/lib/fast_jsonapi.rb index d257b6e..cb4915b 100644 --- a/lib/fast_jsonapi.rb +++ b/lib/fast_jsonapi.rb @@ -2,5 +2,9 @@ module FastJsonapi require 'fast_jsonapi/object_serializer' - require 'extensions/has_one' + if defined?(::Rails) + require 'fast_jsonapi/railtie' + elsif defined?(::ActiveRecord) + require 'extensions/has_one' + end end diff --git a/lib/fast_jsonapi/attribute.rb b/lib/fast_jsonapi/attribute.rb new file mode 100644 index 0000000..c26bf19 --- /dev/null +++ b/lib/fast_jsonapi/attribute.rb @@ -0,0 +1,29 @@ +module FastJsonapi + class Attribute + attr_reader :key, :method, :conditional_proc + + def initialize(key:, method:, options: {}) + @key = key + @method = method + @conditional_proc = options[:if] + end + + 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) + else + record.public_send(method) + end + end + end + + def include_attribute?(record, serialization_params) + if conditional_proc.present? + conditional_proc.call(record, serialization_params) + else + true + end + end + end +end \ No newline at end of file diff --git a/lib/fast_jsonapi/link.rb b/lib/fast_jsonapi/link.rb new file mode 100644 index 0000000..41f84c2 --- /dev/null +++ b/lib/fast_jsonapi/link.rb @@ -0,0 +1,18 @@ +module FastJsonapi + class Link + attr_reader :key, :method + + def initialize(key:, method:) + @key = key + @method = method + end + + def serialize(record, serialization_params, output_hash) + output_hash[key] = if method.is_a?(Proc) + method.arity == 1 ? method.call(record) : method.call(record, serialization_params) + else + record.public_send(method) + end + end + end +end diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index 4dea074..90771b4 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -3,6 +3,9 @@ require 'active_support/core_ext/object' require 'active_support/concern' require 'active_support/inflector' +require 'fast_jsonapi/attribute' +require 'fast_jsonapi/relationship' +require 'fast_jsonapi/link' require 'fast_jsonapi/serialization_core' module FastJsonapi @@ -25,7 +28,7 @@ module FastJsonapi end def serializable_hash - return hash_for_collection if is_collection?(@resource) + return hash_for_collection if is_collection?(@resource, @is_collection) hash_for_one_record end @@ -72,6 +75,7 @@ module FastJsonapi @known_included_objects = {} @meta = options[:meta] @links = options[:links] + @is_collection = options[:is_collection] @params = options[:params] || {} raise ArgumentError.new("`params` option passed to serializer must be a hash") unless @params.is_a?(Hash) @@ -81,8 +85,10 @@ module FastJsonapi end end - def is_collection?(resource) - resource.respond_to?(:each) && !resource.respond_to?(:each_pair) + def is_collection?(resource, force_is_collection = nil) + return force_is_collection unless force_is_collection.nil? + + resource.respond_to?(:size) && !resource.respond_to?(:each_pair) end class_methods do @@ -118,6 +124,9 @@ module FastJsonapi underscore: :underscore } 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 end def run_key_transform(input) @@ -149,48 +158,51 @@ module FastJsonapi def attributes(*attributes_list, &block) attributes_list = attributes_list.first if attributes_list.first.class.is_a?(Array) + options = attributes_list.last.is_a?(Hash) ? attributes_list.pop : {} self.attributes_to_serialize = {} if self.attributes_to_serialize.nil? + attributes_list.each do |attr_name| method_name = attr_name key = run_key_transform(method_name) - attributes_to_serialize[key] = block || method_name + attributes_to_serialize[key] = Attribute.new( + key: key, + method: block || method_name, + options: options + ) end end alias_method :attribute, :attributes - def add_relationship(name, relationship) + 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[name] = relationship + + if !relationship.cached + self.uncachable_relationships_to_serialize[relationship.name] = relationship else - self.cachable_relationships_to_serialize[name] = relationship + self.cachable_relationships_to_serialize[relationship.name] = relationship end - self.relationships_to_serialize[name] = relationship - end + self.relationships_to_serialize[relationship.name] = relationship + end def has_many(relationship_name, options = {}, &block) - name = relationship_name.to_sym - hash = create_relationship_hash(relationship_name, :has_many, options, block) - add_relationship(name, hash) + relationship = create_relationship(relationship_name, :has_many, options, block) + add_relationship(relationship) end def has_one(relationship_name, options = {}, &block) - name = relationship_name.to_sym - hash = create_relationship_hash(relationship_name, :has_one, options, block) - add_relationship(name, hash) + relationship = create_relationship(relationship_name, :has_one, options, block) + add_relationship(relationship) end def belongs_to(relationship_name, options = {}, &block) - name = relationship_name.to_sym - hash = create_relationship_hash(relationship_name, :belongs_to, options, block) - add_relationship(name, hash) + relationship = create_relationship(relationship_name, :belongs_to, options, block) + add_relationship(relationship) end - def create_relationship_hash(base_key, relationship_type, options, block) + def create_relationship(base_key, relationship_type, options, block) name = base_key.to_sym if relationship_type == :has_many base_serialization_key = base_key.to_s.singularize @@ -201,7 +213,7 @@ module FastJsonapi base_key_sym = name id_postfix = '_id' end - { + 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, @@ -210,9 +222,10 @@ module FastJsonapi object_block: block, serializer: compute_serializer_name(options[:serializer] || base_key_sym), relationship_type: relationship_type, - cached: options[:cached] || false, - polymorphic: fetch_polymorphic_option(options) - } + cached: options[:cached], + polymorphic: fetch_polymorphic_option(options), + conditional_proc: options[:if] + ) end def compute_serializer_name(serializer_key) @@ -233,7 +246,11 @@ module FastJsonapi self.data_links = {} if self.data_links.nil? link_method_name = link_name if link_method_name.nil? key = run_key_transform(link_name) - self.data_links[key] = block || link_method_name + + self.data_links[key] = Link.new( + key: key, + method: block || link_method_name + ) end def validate_includes!(includes) @@ -244,8 +261,8 @@ module FastJsonapi parse_include_item(include_item).each do |parsed_include| relationship_to_include = klass.relationships_to_serialize[parsed_include] raise ArgumentError, "#{parsed_include} is not specified as a relationship on #{klass.name}" unless relationship_to_include - raise NotImplementedError if relationship_to_include[:polymorphic].is_a?(Hash) - klass = relationship_to_include[:serializer].to_s.constantize + raise NotImplementedError if relationship_to_include.polymorphic.is_a?(Hash) + klass = relationship_to_include.serializer.to_s.constantize end end end diff --git a/lib/fast_jsonapi/railtie.rb b/lib/fast_jsonapi/railtie.rb new file mode 100644 index 0000000..e6a2717 --- /dev/null +++ b/lib/fast_jsonapi/railtie.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'rails/railtie' + +class Railtie < Rails::Railtie + initializer 'fast_jsonapi.active_record' do + ActiveSupport.on_load :active_record do + require 'extensions/has_one' + end + end +end diff --git a/lib/fast_jsonapi/relationship.rb b/lib/fast_jsonapi/relationship.rb new file mode 100644 index 0000000..0b3a101 --- /dev/null +++ b/lib/fast_jsonapi/relationship.rb @@ -0,0 +1,99 @@ +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 + + def initialize( + key:, + name:, + id_method_name:, + record_type:, + object_method_name:, + object_block:, + serializer:, + relationship_type:, + cached: false, + polymorphic:, + conditional_proc: + ) + @key = key + @name = name + @id_method_name = id_method_name + @record_type = record_type + @object_method_name = object_method_name + @object_block = object_block + @serializer = serializer + @relationship_type = relationship_type + @cached = cached + @polymorphic = polymorphic + @conditional_proc = conditional_proc + 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 + } + end + end + + def fetch_associated_object(record, params) + return object_block.call(record, params) unless object_block.nil? + record.send(object_method_name) + end + + def include_relationship?(record, serialization_params) + if conditional_proc.present? + conditional_proc.call(record, serialization_params) + else + true + end + end + + private + + def ids_hash_from_record_and_relationship(record, params = {}) + return ids_hash( + fetch_id(record, params) + ) unless polymorphic + + return unless associated_object = fetch_associated_object(record, params) + + return associated_object.map do |object| + id_hash_from_record object, polymorphic + end if associated_object.respond_to? :map + + id_hash_from_record associated_object, polymorphic + end + + def id_hash_from_record(record, record_types) + # memoize the record type within the record_types dictionary, then assigning to record_type: + associated_record_type = record_types[record.class] ||= record.class.name.underscore.to_sym + id_hash(record.id, associated_record_type) + end + + def ids_hash(ids) + return ids.map { |id| id_hash(id, record_type) } if ids.respond_to? :map + id_hash(ids, record_type) # ids variable is just a single id here + end + + def id_hash(id, record_type, default_return=false) + if id.present? + { id: id.to_s, type: record_type } + else + default_return ? { id: nil, type: record_type } : nil + end + end + + def fetch_id(record, params) + unless object_block.nil? + object = object_block.call(record, params) + + return object.map(&:id) if object.respond_to? :map + return object.try(:id) + end + + record.public_send(id_method_name) + end + end +end \ No newline at end of file diff --git a/lib/fast_jsonapi/serialization_core.rb b/lib/fast_jsonapi/serialization_core.rb index 668aebe..11257aa 100644 --- a/lib/fast_jsonapi/serialization_core.rb +++ b/lib/fast_jsonapi/serialization_core.rb @@ -34,51 +34,15 @@ module FastJsonapi end end - def ids_hash(ids, record_type) - return ids.map { |id| id_hash(id, record_type) } if ids.respond_to? :map - id_hash(ids, record_type) # ids variable is just a single id here - end - - def id_hash_from_record(record, record_types) - # memoize the record type within the record_types dictionary, then assigning to record_type: - record_type = record_types[record.class] ||= record.class.name.underscore.to_sym - id_hash(record.id, record_type) - end - - def ids_hash_from_record_and_relationship(record, relationship, params = {}) - polymorphic = relationship[:polymorphic] - - return ids_hash( - fetch_id(record, relationship, params), - relationship[:record_type] - ) unless polymorphic - - return unless associated_object = fetch_associated_object(record, relationship, params) - - return associated_object.map do |object| - id_hash_from_record object, polymorphic - end if associated_object.respond_to? :map - - id_hash_from_record associated_object, polymorphic - end - def links_hash(record, params = {}) - data_links.each_with_object({}) do |(key, method), link_hash| - link_hash[key] = if method.is_a?(Proc) - method.arity == 1 ? method.call(record) : method.call(record, params) - else - record.public_send(method) - end + data_links.each_with_object({}) do |(_k, link), hash| + link.serialize(record, params, hash) end end def attributes_hash(record, params = {}) - attributes_to_serialize.each_with_object({}) do |(key, method), attr_hash| - attr_hash[key] = if method.is_a?(Proc) - method.arity == 1 ? method.call(record) : method.call(record, params) - else - record.public_send(method) - end + attributes_to_serialize.each_with_object({}) do |(_k, attribute), hash| + attribute.serialize(record, params, hash) end end @@ -86,11 +50,7 @@ module FastJsonapi relationships = relationships_to_serialize if relationships.nil? relationships.each_with_object({}) do |(_k, relationship), hash| - name = relationship[:key] - empty_case = relationship[:relationship_type] == :has_many ? [] : nil - hash[name] = { - data: ids_hash_from_record_and_relationship(record, relationship, params) || empty_case - } + relationship.serialize(record, params, hash) end end @@ -147,12 +107,14 @@ module FastJsonapi items = parse_include_item(include_item) items.each do |item| next unless relationships_to_serialize && relationships_to_serialize[item] - raise NotImplementedError if @relationships_to_serialize[item][:polymorphic].is_a?(Hash) - record_type = @relationships_to_serialize[item][:record_type] - serializer = @relationships_to_serialize[item][:serializer].to_s.constantize - relationship_type = @relationships_to_serialize[item][:relationship_type] + relationship_item = relationships_to_serialize[item] + next unless relationship_item.include_relationship?(record, params) + raise NotImplementedError if relationship_item.polymorphic.is_a?(Hash) + record_type = relationship_item.record_type + serializer = relationship_item.serializer.to_s.constantize + relationship_type = relationship_item.relationship_type - included_objects = fetch_associated_object(record, @relationships_to_serialize[item], params) + included_objects = relationship_item.fetch_associated_object(record, params) next if included_objects.blank? included_objects = [included_objects] unless relationship_type == :has_many @@ -171,22 +133,6 @@ module FastJsonapi end end end - - def fetch_associated_object(record, relationship, params) - return relationship[:object_block].call(record, params) unless relationship[:object_block].nil? - record.send(relationship[:object_method_name]) - end - - def fetch_id(record, relationship, params) - unless relationship[:object_block].nil? - object = relationship[:object_block].call(record, params) - - return object.map(&:id) if object.respond_to? :map - return object.try(:id) - end - - record.public_send(relationship[:id_method_name]) - end end end end diff --git a/spec/lib/object_serializer_class_methods_spec.rb b/spec/lib/object_serializer_class_methods_spec.rb index 55e351a..85346f0 100644 --- a/spec/lib/object_serializer_class_methods_spec.rb +++ b/spec/lib/object_serializer_class_methods_spec.rb @@ -312,7 +312,6 @@ describe FastJsonapi::ObjectSerializer do movie_type_serializer_class.instance_eval do include FastJsonapi::ObjectSerializer set_key_transform key_transform - set_type :movie_type attributes :name end end @@ -321,25 +320,25 @@ describe FastJsonapi::ObjectSerializer do context 'when key_transform is dash' do let(:key_transform) { :dash } - it_behaves_like 'returning key transformed hash', :'movie-type', :'release-year' + it_behaves_like 'returning key transformed hash', :'movie-type', :'dash-movie-type', :'release-year' end context 'when key_transform is camel' do let(:key_transform) { :camel } - it_behaves_like 'returning key transformed hash', :MovieType, :ReleaseYear + it_behaves_like 'returning key transformed hash', :MovieType, :CamelMovieType, :ReleaseYear end context 'when key_transform is camel_lower' do let(:key_transform) { :camel_lower } - it_behaves_like 'returning key transformed hash', :movieType, :releaseYear + it_behaves_like 'returning key transformed hash', :movieType, :camelLowerMovieType, :releaseYear end context 'when key_transform is underscore' do let(:key_transform) { :underscore } - it_behaves_like 'returning key transformed hash', :movie_type, :release_year + it_behaves_like 'returning key transformed hash', :movie_type, :underscore_movie_type, :release_year end end end diff --git a/spec/lib/object_serializer_inheritance_spec.rb b/spec/lib/object_serializer_inheritance_spec.rb index 06dba25..8cf5b53 100644 --- a/spec/lib/object_serializer_inheritance_spec.rb +++ b/spec/lib/object_serializer_inheritance_spec.rb @@ -113,7 +113,7 @@ describe FastJsonapi::ObjectSerializer do end it 'includes child attributes' do - expect(EmployeeSerializer.attributes_to_serialize[:location]).to eq(:location) + expect(EmployeeSerializer.attributes_to_serialize[:location].method).to eq(:location) end it 'doesnt change parent class attributes' do diff --git a/spec/lib/object_serializer_spec.rb b/spec/lib/object_serializer_spec.rb index 7941f66..07cbbef 100644 --- a/spec/lib/object_serializer_spec.rb +++ b/spec/lib/object_serializer_spec.rb @@ -309,4 +309,142 @@ describe FastJsonapi::ObjectSerializer do expect(serializable_hash[:included][0][:links][:self]).to eq url end end + + context 'when is_collection option present' do + subject { MovieSerializer.new(resource, is_collection_options).serializable_hash } + + context 'autodetect' do + let(:is_collection_options) { {} } + + context 'collection if no option present' do + let(:resource) { [movie] } + it { expect(subject[:data]).to be_a(Array) } + end + + context 'single if no option present' do + let(:resource) { movie } + it { expect(subject[:data]).to be_a(Hash) } + end + end + + context 'force is_collection to true' do + let(:is_collection_options) { { is_collection: true } } + + context 'collection will pass' do + let(:resource) { [movie] } + it { expect(subject[:data]).to be_a(Array) } + end + + context 'single will raise error' do + let(:resource) { movie } + it { expect { subject }.to raise_error(NoMethodError, /method(.*)each/) } + end + end + + context 'force is_collection to false' do + let(:is_collection_options) { { is_collection: false } } + + context 'collection will fail without id' do + let(:resource) { [movie] } + it { expect { subject }.to raise_error(FastJsonapi::MandatoryField, /id is a mandatory field/) } + end + + context 'single will pass' do + let(:resource) { movie } + it { expect(subject[:data]).to be_a(Hash) } + end + end + end + + context 'when optional attributes are determined by record data' do + it 'returns optional attribute when attribute is included' do + movie.release_year = 2001 + json = MovieOptionalRecordDataSerializer.new(movie).serialized_json + serializable_hash = JSON.parse(json) + expect(serializable_hash['data']['attributes']['release_year']).to eq movie.release_year + end + + it "doesn't return optional attribute when attribute is not included" do + movie.release_year = 1970 + json = MovieOptionalRecordDataSerializer.new(movie).serialized_json + serializable_hash = JSON.parse(json) + expect(serializable_hash['data']['attributes'].has_key?('release_year')).to be_falsey + end + end + + context 'when optional attributes are determined by params data' do + it 'returns optional attribute when attribute is included' do + movie.director = 'steven spielberg' + json = MovieOptionalParamsDataSerializer.new(movie, { params: { admin: true }}).serialized_json + serializable_hash = JSON.parse(json) + expect(serializable_hash['data']['attributes']['director']).to eq 'steven spielberg' + end + + it "doesn't return optional attribute when attribute is not included" do + movie.director = 'steven spielberg' + json = MovieOptionalParamsDataSerializer.new(movie, { params: { admin: false }}).serialized_json + serializable_hash = JSON.parse(json) + expect(serializable_hash['data']['attributes'].has_key?('director')).to be_falsey + end + end + + context 'when optional relationships are determined by record data' do + it 'returns optional relationship when relationship is included' do + json = MovieOptionalRelationshipSerializer.new(movie).serialized_json + serializable_hash = JSON.parse(json) + expect(serializable_hash['data']['relationships'].has_key?('actors')).to be_truthy + end + + context "when relationship is not included" do + let(:json) { + MovieOptionalRelationshipSerializer.new(movie, options).serialized_json + } + let(:options) { + {} + } + let(:serializable_hash) { + JSON.parse(json) + } + + it "doesn't return optional relationship" do + movie.actor_ids = [] + expect(serializable_hash['data']['relationships'].has_key?('actors')).to be_falsey + end + + it "doesn't include optional relationship" do + movie.actor_ids = [] + options[:include] = [:actors] + expect(serializable_hash['included']).to be_blank + end + end + end + + context 'when optional relationships are determined by params data' do + it 'returns optional relationship when relationship is included' do + json = MovieOptionalRelationshipWithParamsSerializer.new(movie, { params: { admin: true }}).serialized_json + serializable_hash = JSON.parse(json) + expect(serializable_hash['data']['relationships'].has_key?('owner')).to be_truthy + end + + context "when relationship is not included" do + let(:json) { + MovieOptionalRelationshipWithParamsSerializer.new(movie, options).serialized_json + } + let(:options) { + { params: { admin: false }} + } + let(:serializable_hash) { + JSON.parse(json) + } + + it "doesn't return optional relationship" do + expect(serializable_hash['data']['relationships'].has_key?('owner')).to be_falsey + end + + it "doesn't include optional relationship" do + options[:include] = [:owner] + expect(serializable_hash['included']).to be_blank + end + end + end end diff --git a/spec/lib/serialization_core_spec.rb b/spec/lib/serialization_core_spec.rb index 64f25bb..d161e3f 100644 --- a/spec/lib/serialization_core_spec.rb +++ b/spec/lib/serialization_core_spec.rb @@ -17,31 +17,13 @@ describe FastJsonapi::ObjectSerializer do expect(result_hash).to be nil end - it 'returns the correct hash when ids_hash_from_record_and_relationship is called for a polymorphic association' do - relationship = { name: :groupees, relationship_type: :has_many, object_method_name: :groupees, polymorphic: {} } - results = GroupSerializer.send :ids_hash_from_record_and_relationship, group, relationship - expect(results).to include({ id: "1", type: :person }, { id: "2", type: :group }) - end - - it 'returns correct hash when ids_hash is called' do - inputs = [{ids: %w(1 2 3), record_type: :movie}, {ids: %w(x y z), record_type: 'person'}] - inputs.each do |hash| - results = MovieSerializer.send(:ids_hash, hash[:ids], hash[:record_type]) - expect(results.map{|h| h[:id]}).to eq hash[:ids] - expect(results[0][:type]).to eq hash[:record_type] - end - - result = MovieSerializer.send(:ids_hash, [], 'movie') - expect(result).to be_empty - end - it 'returns correct hash when attributes_hash is called' do attributes_hash = MovieSerializer.send(:attributes_hash, movie) attribute_names = attributes_hash.keys.sort expect(attribute_names).to eq MovieSerializer.attributes_to_serialize.keys.sort - MovieSerializer.attributes_to_serialize.each do |key, method_name| + MovieSerializer.attributes_to_serialize.each do |key, attribute| value = attributes_hash[key] - expect(value).to eq movie.send(method_name) + expect(value).to eq movie.send(attribute.method) end end @@ -57,7 +39,7 @@ describe FastJsonapi::ObjectSerializer do relationships_hash = MovieSerializer.send(:relationships_hash, movie) relationship_names = relationships_hash.keys.sort relationships_hashes = MovieSerializer.relationships_to_serialize.values - expected_names = relationships_hashes.map{|relationship| relationship[:key]}.sort + expected_names = relationships_hashes.map{|relationship| relationship.key}.sort expect(relationship_names).to eq expected_names end diff --git a/spec/shared/contexts/movie_context.rb b/spec/shared/contexts/movie_context.rb index a73e869..bbd89a9 100644 --- a/spec/shared/contexts/movie_context.rb +++ b/spec/shared/contexts/movie_context.rb @@ -278,6 +278,34 @@ RSpec.shared_context 'movie class' do set_type :account belongs_to :supplier end + + class MovieOptionalRecordDataSerializer + include FastJsonapi::ObjectSerializer + set_type :movie + attributes :name + attribute :release_year, if: Proc.new { |record| record.release_year >= 2000 } + end + + class MovieOptionalParamsDataSerializer + include FastJsonapi::ObjectSerializer + set_type :movie + attributes :name + attribute :director, if: Proc.new { |record, params| params && params[:admin] == true } + end + + class MovieOptionalRelationshipSerializer + include FastJsonapi::ObjectSerializer + set_type :movie + attributes :name + has_many :actors, if: Proc.new { |record| record.actors.any? } + end + + 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 } + end end diff --git a/spec/shared/examples/object_serializer_class_methods_examples.rb b/spec/shared/examples/object_serializer_class_methods_examples.rb index c529dcb..cffce41 100644 --- a/spec/shared/examples/object_serializer_class_methods_examples.rb +++ b/spec/shared/examples/object_serializer_class_methods_examples.rb @@ -1,18 +1,18 @@ RSpec.shared_examples 'returning correct relationship hash' do |serializer, id_method_name, record_type| it 'returns correct relationship hash' do - expect(relationship).to be_instance_of(Hash) - expect(relationship.keys).to all(be_instance_of(Symbol)) - expect(relationship[:serializer]).to be serializer - expect(relationship[:id_method_name]).to be id_method_name - expect(relationship[:record_type]).to be record_type + expect(relationship).to be_instance_of(FastJsonapi::Relationship) + # expect(relationship.keys).to all(be_instance_of(Symbol)) + expect(relationship.serializer).to be serializer + expect(relationship.id_method_name).to be id_method_name + expect(relationship.record_type).to be record_type end end -RSpec.shared_examples 'returning key transformed hash' do |movie_type, release_year| +RSpec.shared_examples 'returning key transformed hash' do |movie_type, serializer_type, release_year| it 'returns correctly transformed hash' do expect(hash[:data][0][:attributes]).to have_key(release_year) expect(hash[:data][0][:relationships]).to have_key(movie_type) expect(hash[:data][0][:relationships][movie_type][:data][:type]).to eq(movie_type) - expect(hash[:included][0][:type]).to eq(movie_type) + expect(hash[:included][0][:type]).to eq(serializer_type) end end