diff --git a/README.md b/README.md index 53c8bfa7..f36a22a3 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ A lightning fast [JSON:API](http://jsonapi.org/) serializer for Ruby Objects. +Note: this gem deals only with implementing the JSON:API spec. If your API +responses are not formatted according to the JSON:API spec, this library will +not work for you. + # Performance Comparison We compare serialization times with Active Model Serializer as part of RSpec performance tests included on this library. We want to ensure that with every change on this library, serialization time is at least `25 times` faster than Active Model Serializers on up to current benchmark of 1000 records. Please read the [performance document](https://github.com/Netflix/fast_jsonapi/blob/master/performance_methodology.md) for any questions related to methodology. @@ -100,6 +104,17 @@ movie.actor_ids = [1, 2, 3] movie.owner_id = 3 movie.movie_type_id = 1 movie + +movies = + 2.times.map do |i| + m = Movie.new + m.id = i + 1 + m.name = "test movie #{i}" + m.actor_ids = [1, 2, 3] + m.owner_id = 3 + m.movie_type_id = 1 + m + end ``` ### Object Serialization @@ -298,7 +313,7 @@ options[:links] = { prev: '...' } options[:include] = [:actors, :'actors.agency', :'actors.agency.state'] -MovieSerializer.new([movie, movie], options).serialized_json +MovieSerializer.new(movies, options).serialized_json ``` ### Collection Serialization @@ -310,15 +325,15 @@ options[:links] = { next: '...', prev: '...' } -hash = MovieSerializer.new([movie, movie], options).serializable_hash -json_string = MovieSerializer.new([movie, movie], options).serialized_json +hash = MovieSerializer.new(movies, options).serializable_hash +json_string = MovieSerializer.new(movies, 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 this option is not provided or `nil` autodetect 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 @@ -450,7 +465,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 ``` or ```set_id { |record| "#{record.name.downcase}-#{record.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 (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 ``` @@ -473,7 +488,7 @@ Skylight relies on `ActiveSupport::Notifications` to track these two core method require 'fast_jsonapi/instrumentation' ``` -The two instrumented notifcations are supplied by these two constants: +The two instrumented notifications are supplied by these two constants: * `FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION` * `FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION` diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index b8a24183..c161fb6b 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -86,7 +86,7 @@ def process_options(options) raise ArgumentError.new("`params` option passed to serializer must be a hash") unless @params.is_a?(Hash) if options[:include].present? - @includes = options[:include].delete_if(&:blank?).map(&:to_sym) + @includes = options[:include].reject(&:blank?).map(&:to_sym) self.class.validate_includes!(@includes) end end @@ -130,7 +130,7 @@ def reflected_record_type return @reflected_record_type if defined?(@reflected_record_type) @reflected_record_type ||= begin - if self.name.end_with?('Serializer') + if self.name && self.name.end_with?('Serializer') self.name.split('::').last.chomp('Serializer').underscore.to_sym end end @@ -299,7 +299,7 @@ def link(link_name, link_method_name = nil, &block) def validate_includes!(includes) return if includes.blank? - includes.detect do |include_item| + includes.each do |include_item| klass = self parse_include_item(include_item).each do |parsed_include| relationships_to_serialize = klass.relationships_to_serialize || {} diff --git a/lib/fast_jsonapi/serialization_core.rb b/lib/fast_jsonapi/serialization_core.rb index 200af9b3..845aee75 100644 --- a/lib/fast_jsonapi/serialization_core.rb +++ b/lib/fast_jsonapi/serialization_core.rb @@ -44,6 +44,8 @@ def links_hash(record, params = {}) def attributes_hash(record, fieldset = nil, params = {}) attributes = attributes_to_serialize attributes = attributes.slice(*fieldset) if fieldset.present? + attributes = {} if fieldset == [] + attributes.each_with_object({}) do |(_k, attribute), hash| attribute.serialize(record, params, hash) end @@ -52,6 +54,7 @@ def attributes_hash(record, fieldset = 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 = {} if fieldset == [] relationships.each_with_object({}) do |(_k, relationship), hash| relationship.serialize(record, params, hash) 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 diff --git a/spec/lib/instrumentation/as_notifications_negative_spec.rb b/spec/lib/instrumentation/as_notifications_negative_spec.rb index 6912ed49..de144dea 100644 --- a/spec/lib/instrumentation/as_notifications_negative_spec.rb +++ b/spec/lib/instrumentation/as_notifications_negative_spec.rb @@ -10,7 +10,8 @@ options[:meta] = { total: 2 } options[:include] = [:actors] - @serializer = MovieSerializer.new([movie, movie], options) + movies = build_movies(2) + @serializer = MovieSerializer.new(movies, options) end context 'serializable_hash' do diff --git a/spec/lib/instrumentation/as_notifications_spec.rb b/spec/lib/instrumentation/as_notifications_spec.rb index f7769ffe..e4993bb6 100644 --- a/spec/lib/instrumentation/as_notifications_spec.rb +++ b/spec/lib/instrumentation/as_notifications_spec.rb @@ -24,7 +24,8 @@ options[:meta] = { total: 2 } options[:include] = [:actors] - @serializer = MovieSerializer.new([movie, movie], options) + movies = build_movies(2) + @serializer = MovieSerializer.new(movies, options) end context 'serializable_hash' do diff --git a/spec/lib/object_serializer_caching_spec.rb b/spec/lib/object_serializer_caching_spec.rb index 8e455d49..2f58cd16 100644 --- a/spec/lib/object_serializer_caching_spec.rb +++ b/spec/lib/object_serializer_caching_spec.rb @@ -16,7 +16,8 @@ options[:links] = { self: 'self' } options[:include] = [:actors] - serializable_hash = CachingMovieSerializer.new([movie, movie], options).serializable_hash + movies = build_movies(2) + serializable_hash = CachingMovieSerializer.new(movies, options).serializable_hash expect(serializable_hash[:data].length).to eq 2 expect(serializable_hash[:data][0][:relationships].length).to eq 3 diff --git a/spec/lib/object_serializer_class_methods_spec.rb b/spec/lib/object_serializer_class_methods_spec.rb index 205f6452..6fe4718b 100644 --- a/spec/lib/object_serializer_class_methods_spec.rb +++ b/spec/lib/object_serializer_class_methods_spec.rb @@ -213,7 +213,7 @@ end context 'when an array of records is given' do - let(:resource) { [movie, movie] } + let(:resource) { build_movies(2) } it 'returns correct hash which id equals owner_id' do expect(serializable_hash[:data][0][:id].to_i).to eq movie.owner_id @@ -240,7 +240,7 @@ end context 'when an array of records is given' do - let(:resource) { [movie, movie] } + let(:resource) { build_movies(2) } it 'returns correct hash which id equals movie-id' do expect(serializable_hash[:data][0][:id]).to eq "movie-#{movie.owner_id}" @@ -403,7 +403,7 @@ def year_since_release_calculator(release_year) end describe '#key_transform' do - subject(:hash) { movie_serializer_class.new([movie, movie], include: [:movie_type]).serializable_hash } + subject(:hash) { movie_serializer_class.new(build_movies(2), include: [:movie_type]).serializable_hash } let(:movie_serializer_class) { "#{key_transform}_movie_serializer".classify.constantize } diff --git a/spec/lib/object_serializer_fields_spec.rb b/spec/lib/object_serializer_fields_spec.rb index 913ba83b..697f209d 100644 --- a/spec/lib/object_serializer_fields_spec.rb +++ b/spec/lib/object_serializer_fields_spec.rb @@ -22,6 +22,18 @@ expect(hash[:data][:relationships].keys.sort).to eq %i[actors advertising_campaign] end + it 'returns no fields when none are specified' do + hash = MovieSerializer.new(movie, fields: { movie: [] }).serializable_hash + + expect(hash[:data][:attributes].keys).to eq [] + end + + it 'returns no relationships when none are specified' do + hash = MovieSerializer.new(movie, fields: { movie: [] }).serializable_hash + + expect(hash[:data][:relationships].keys).to eq [] + end + it 'only returns specified fields for included relationships' do hash = MovieSerializer.new(movie, fields: fields, include: %i[actors]).serializable_hash @@ -45,4 +57,25 @@ expect(hash[:included][3][:relationships].keys.sort).to eq %i[movie] end + + context 'with no included fields specified' do + let(:fields) do + { + movie: %i[name actors advertising_campaign], + actor: [] + } + end + + it 'returns no fields for included relationships when none are specified' do + hash = MovieSerializer.new(movie, fields: fields, include: %i[actors advertising_campaign]).serializable_hash + + expect(hash[:included][2][:attributes].keys).to eq [] + end + + it 'returns no relationships when none are specified' do + hash = MovieSerializer.new(movie, fields: fields, include: %i[actors advertising_campaign]).serializable_hash + + expect(hash[:included][2][:relationships].keys).to eq [] + end + end end diff --git a/spec/lib/object_serializer_spec.rb b/spec/lib/object_serializer_spec.rb index ef755ccc..724795c7 100644 --- a/spec/lib/object_serializer_spec.rb +++ b/spec/lib/object_serializer_spec.rb @@ -4,13 +4,15 @@ include_context 'movie class' include_context 'group class' + let(:movies) { build_movies(2) } + context 'when testing instance methods of object serializer' do it 'returns correct hash when serializable_hash is called' do options = {} options[:meta] = { total: 2 } options[:links] = { self: 'self' } options[:include] = [:actors] - serializable_hash = MovieSerializer.new([movie, movie], options).serializable_hash + serializable_hash = MovieSerializer.new(movies, options).serializable_hash expect(serializable_hash[:data].length).to eq 2 expect(serializable_hash[:data][0][:relationships].length).to eq 4 @@ -58,7 +60,7 @@ it 'returns correct number of records when serialized_json is called for an array' do options = {} options[:meta] = { total: 2 } - json = MovieSerializer.new([movie, movie], options).serialized_json + json = MovieSerializer.new(movies, options).serialized_json serializable_hash = JSON.parse(json) expect(serializable_hash['data'].length).to eq 2 expect(serializable_hash['meta']).to be_instance_of(Hash) @@ -124,7 +126,7 @@ end it 'returns multiple records' do - json_hash = MovieSerializer.new([movie, movie]).as_json + json_hash = MovieSerializer.new(movies).as_json expect(json_hash['data'].length).to eq 2 end @@ -139,6 +141,13 @@ options = {} options[:meta] = { total: 2 } options[:include] = [:blah_blah] + expect { MovieSerializer.new(movies, options).serializable_hash }.to raise_error(ArgumentError) + end + + it 'returns errors when serializing with non-existent and existent includes keys' do + options = {} + options[:meta] = { total: 2 } + options[:include] = [:actors, :blah_blah] expect { MovieSerializer.new([movie, movie], options).serializable_hash }.to raise_error(ArgumentError) end @@ -148,13 +157,19 @@ expect { MovieSerializer.new(movie, options) }.not_to raise_error end + it 'does not throw an error with non-empty string array includes keys' do + options = {} + options[:include] = ['actors', 'owner'] + expect { MovieSerializer.new(movie, options) }.not_to raise_error + end + it 'returns keys when serializing with empty string/nil array includes key' do options = {} options[:meta] = { total: 2 } options[:include] = [''] - expect(MovieSerializer.new([movie, movie], options).serializable_hash.keys).to eq [:data, :meta] + expect(MovieSerializer.new(movies, options).serializable_hash.keys).to eq [:data, :meta] options[:include] = [nil] - expect(MovieSerializer.new([movie, movie], options).serializable_hash.keys).to eq [:data, :meta] + expect(MovieSerializer.new(movies, options).serializable_hash.keys).to eq [:data, :meta] end end @@ -314,6 +329,15 @@ class BlahBlahSerializer expect(BlahBlahSerializer.record_type).to be :blah_blah end + it 'should set default_type for a namespaced serializer' do + module V1 + class BlahSerializer + include FastJsonapi::ObjectSerializer + end + end + expect(V1::BlahSerializer.record_type).to be :blah + end + it 'shouldnt set default_type for a serializer that doesnt follow convention' do class BlahBlahSerializerBuilder include FastJsonapi::ObjectSerializer @@ -321,13 +345,11 @@ class BlahBlahSerializerBuilder expect(BlahBlahSerializerBuilder.record_type).to be_nil end - it 'should set default_type for a namespaced serializer' do - module V1 - class BlahSerializer - include FastJsonapi::ObjectSerializer - end + it 'shouldnt set default_type for an anonymous serializer' do + serializer_class = Class.new do + include FastJsonapi::ObjectSerializer end - expect(V1::BlahSerializer.record_type).to be :blah + expect(serializer_class.record_type).to be_nil end end @@ -473,6 +495,16 @@ class BlahSerializer options[:include] = [:actors] expect(serializable_hash['included']).to be_blank end + + end + end + + context 'when include has frozen array' do + let(:options) { { include: [:actors].freeze }} + let(:json) { MovieOptionalRelationshipSerializer.new(movie, options).serialized_json } + + it 'does not raise and error' do + expect(json['included']).to_not be_blank end end