From 5eeaf05704f762d7dabdb8dd6710a792848a1e3b Mon Sep 17 00:00:00 2001 From: "yuuji.yaginuma" Date: Fri, 2 Feb 2018 15:10:21 +0900 Subject: [PATCH 01/53] Update to latest rubies on CI * Update to latest version of 2.3.x. * Run test against newer rubies --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 334563c3..39713389 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,7 @@ language: ruby rvm: - - 2.3.1 + - 2.3.6 + - 2.4.3 + - 2.5.0 script: - bundle exec rake spec From a9c902b96fdb83f68f99fcc3d3f898b87d921a9a Mon Sep 17 00:00:00 2001 From: Shishir Kakaraddi Date: Thu, 1 Feb 2018 18:00:18 -0800 Subject: [PATCH 02/53] add hash benchmarking to performance tests --- .../lib/object_serializer_performance_spec.rb | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/spec/lib/object_serializer_performance_spec.rb b/spec/lib/object_serializer_performance_spec.rb index feeb7bc3..e2c4630b 100644 --- a/spec/lib/object_serializer_performance_spec.rb +++ b/spec/lib/object_serializer_performance_spec.rb @@ -34,29 +34,48 @@ end end - def print_stats(count, ams_time, our_time) + def print_stats(message, count, ams_time, our_time) format = '%-15s %-10s %s' puts '' + puts message puts format(format, 'Serializer', 'Records', 'Time') puts format(format, 'AMS serializer', count, ams_time.round(2).to_s + ' ms') puts format(format, 'Fast serializer', count, our_time.round(2).to_s + ' ms') end + def run_hash_benchmark(message, movie_count, our_serializer, ams_serializer) + our_time = Benchmark.measure { our_hash = our_serializer.serializable_hash }.real * 1000 + ams_time = Benchmark.measure { ams_hash = ams_serializer.as_json }.real * 1000 + print_stats(message, movie_count, ams_time, our_time) + end + + def run_json_benchmark(message, movie_count, our_serializer, ams_serializer) + our_json = nil + ams_json = nil + our_time = Benchmark.measure { our_json = our_serializer.serialized_json }.real * 1000 + ams_time = Benchmark.measure { ams_json = ams_serializer.to_json }.real * 1000 + print_stats(message, movie_count, ams_time, our_time) + return our_json, ams_json + end + context 'when comparing with AMS 0.10.x' do [1, 25, 250, 1000].each do |movie_count| speed_factor = 25 it "should serialize #{movie_count} records atleast #{speed_factor} times faster than AMS" do ams_movies = build_ams_movies(movie_count) movies = build_movies(movie_count) - our_json = nil - ams_json = nil our_serializer = MovieSerializer.new(movies) ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_movies) - our_time = Benchmark.measure { our_json = our_serializer.serialized_json }.real * 1000 - ams_time = Benchmark.measure { ams_json = ams_serializer.to_json }.real * 1000 - print_stats(movie_count, ams_time, our_time) + + message = "Serialize to JSON string #{movie_count} records" + our_json, ams_json = run_json_benchmark(message, movie_count, our_serializer, ams_serializer) + + message = "Serialize to Ruby Hash #{movie_count} records" + run_hash_benchmark(message, movie_count, our_serializer, ams_serializer) + expect(our_json.length).to eq ams_json.length expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times + expect { our_serializer.serializable_hash }.to perform_faster_than { ams_serializer.as_json }.at_least(speed_factor).times end end end @@ -67,20 +86,21 @@ def print_stats(count, ams_time, our_time) it "should serialize #{movie_count} records atleast #{speed_factor} times faster than AMS" do ams_movies = build_ams_movies(movie_count) movies = build_movies(movie_count) - our_json = nil - ams_json = nil - options = {} options[:meta] = { total: movie_count } options[:include] = [:actors, :movie_type] - our_serializer = MovieSerializer.new(movies, options) ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_movies, include: options[:include], meta: options[:meta]) - our_time = Benchmark.measure { our_json = our_serializer.serialized_json }.real * 1000 - ams_time = Benchmark.measure { ams_json = ams_serializer.to_json }.real * 1000 - print_stats(movie_count, ams_time, our_time) + + message = "Serialize to JSON string #{movie_count} with includes and meta" + our_json, ams_json = run_json_benchmark(message, movie_count, our_serializer, ams_serializer) + + message = "Serialize to Ruby Hash #{movie_count} with includes and meta" + run_hash_benchmark(message, movie_count, our_serializer, ams_serializer) + expect(our_json.length).to eq ams_json.length expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times + expect { our_serializer.serializable_hash }.to perform_faster_than { ams_serializer.as_json }.at_least(speed_factor).times end end end From 088d655998f39b671659161900ee87887592bc89 Mon Sep 17 00:00:00 2001 From: Benjamin Roth Date: Fri, 2 Feb 2018 14:27:37 +0100 Subject: [PATCH 03/53] get rid of temp variables --- lib/fast_jsonapi/object_serializer.rb | 10 ++++------ lib/fast_jsonapi/serialization_core.rb | 16 +++++----------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index 93b977da..db7f0645 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -161,7 +161,7 @@ def belongs_to(relationship_name, options = {}) key = options[:key] || relationship_name.to_s.dasherize.to_sym record_type = options[:record_type] || relationship_name.to_s.dasherize.to_sym end - relationship = { + add_relationship(name, { key: key, name: name, id_method_name: options[:id_method_name] || (relationship_name.to_s + '_id').to_sym, @@ -170,8 +170,7 @@ def belongs_to(relationship_name, options = {}) serializer: compute_serializer_name(serializer_key), relationship_type: :belongs_to, cached: options[:cached] || true - } - add_relationship(name, relationship) + }) end def has_one(relationship_name, options = {}) @@ -183,7 +182,7 @@ def has_one(relationship_name, options = {}) key = options[:key] || relationship_name.to_s.dasherize.to_sym record_type = options[:record_type] || relationship_name.to_s.dasherize.to_sym end - relationship = { + add_relationship(name, { key: key, name: name, id_method_name: options[:id_method_name] || (relationship_name.to_s + '_id').to_sym, @@ -192,8 +191,7 @@ def has_one(relationship_name, options = {}) serializer: compute_serializer_name(serializer_key), relationship_type: :has_one, cached: options[:cached] || false - } - add_relationship(name, relationship) + }) end def compute_serializer_name(serializer_key) diff --git a/lib/fast_jsonapi/serialization_core.rb b/lib/fast_jsonapi/serialization_core.rb index 7da75c88..3cbdb7be 100644 --- a/lib/fast_jsonapi/serialization_core.rb +++ b/lib/fast_jsonapi/serialization_core.rb @@ -27,27 +27,23 @@ def ids_hash(ids, record_type) end def attributes_hash(record) - attributes_hash = {} - attributes_to_serialize.each do |key, method_name| - attributes_hash[key] = record.send(method_name) + attributes_to_serialize.each_with_object({}) do |(key, method_name), attr_hash| + attr_hash[key] = record.send(method_name) end - attributes_hash end def relationships_hash(record, relationships = nil) - relationships_hash = {} relationships = relationships_to_serialize if relationships.nil? - relationships.each do |_k, relationship| + relationships.each_with_object({}) do |(_k, relationship), hash| name = relationship[:key] id_method_name = relationship[:id_method_name] record_type = relationship[:record_type] empty_case = relationship[:relationship_type] == :has_many ? [] : nil - relationships_hash[name] = { + hash[name] = { data: ids_hash(record.send(id_method_name), record_type) || empty_case } end - relationships_hash end def record_hash(record) @@ -76,8 +72,7 @@ def to_json(payload) # includes handler def get_included_records(record, includes_list, known_included_objects) - included_records = [] - includes_list.each do |item| + includes_list.each_with_object([]) do |item, included_records| object_method_name = @relationships_to_serialize[item][:object_method_name] record_type = @relationships_to_serialize[item][:record_type] serializer = @relationships_to_serialize[item][:serializer].to_s.constantize @@ -92,7 +87,6 @@ def get_included_records(record, includes_list, known_included_objects) included_records << serializer.record_hash(inc_obj) end end - included_records end def has_permitted_includes(requested_includes) From dc563407e0866ecc29902d1bc671592d4444a825 Mon Sep 17 00:00:00 2001 From: Benjamin Roth Date: Fri, 2 Feb 2018 14:27:52 +0100 Subject: [PATCH 04/53] rename in-block var to avoid confusion --- lib/fast_jsonapi/serialization_core.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/fast_jsonapi/serialization_core.rb b/lib/fast_jsonapi/serialization_core.rb index 3cbdb7be..77e7cf88 100644 --- a/lib/fast_jsonapi/serialization_core.rb +++ b/lib/fast_jsonapi/serialization_core.rb @@ -49,11 +49,11 @@ def relationships_hash(record, relationships = nil) def record_hash(record) if cached record_hash = Rails.cache.fetch(record.cache_key, expires_in: cache_length) do - record_hash = id_hash(record.id, record_type) || { id: nil, type: record_type } - record_hash[:attributes] = attributes_hash(record) if attributes_to_serialize.present? - record_hash[:relationships] = {} - record_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize) if cachable_relationships_to_serialize.present? - record_hash + temp_hash = id_hash(record.id, record_type) || { id: nil, type: record_type } + temp_hash[:attributes] = attributes_hash(record) if attributes_to_serialize.present? + temp_hash[:relationships] = {} + temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize) if cachable_relationships_to_serialize.present? + temp_hash end record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize)) if uncachable_relationships_to_serialize.present? record_hash From 80bd8b983c5b09c94f7c7725629c72a1b0005857 Mon Sep 17 00:00:00 2001 From: Diego Algorta Date: Fri, 2 Feb 2018 10:39:46 -0300 Subject: [PATCH 05/53] Add missing attribute in README example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d093381d..914219ad 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ $ bundle install ```ruby class Movie - attr_accessor :id, :name, :year, :actor_ids, :owner_id + attr_accessor :id, :name, :year, :actor_ids, :owner_id, :movie_type_id end ``` From fbb9db5201e1c90e4507e5c46375592e9a84e7be Mon Sep 17 00:00:00 2001 From: "Min.Kim" Date: Fri, 2 Feb 2018 14:57:40 +0000 Subject: [PATCH 06/53] Disable GC before doing performance test --- spec/lib/object_serializer_performance_spec.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/lib/object_serializer_performance_spec.rb b/spec/lib/object_serializer_performance_spec.rb index e2c4630b..b23b23a9 100644 --- a/spec/lib/object_serializer_performance_spec.rb +++ b/spec/lib/object_serializer_performance_spec.rb @@ -4,6 +4,9 @@ include_context 'movie class' include_context 'ams movie class' + before(:all) { GC.disable } + after(:all) { GC.enable } + context 'when testing performance of serialization' do it 'should create a hash of 1000 records in less than 50 ms' do movies = 1000.times.map { |_i| movie } From 0048bd2c80f9422db3fe343c053839805bd0d418 Mon Sep 17 00:00:00 2001 From: "Min.Kim" Date: Fri, 2 Feb 2018 14:58:04 +0000 Subject: [PATCH 07/53] Enable oj to AM for fair benchmark test --- spec/spec_helper.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c0eecd7b..c278cdca 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,6 +3,7 @@ require 'multi_json' require 'byebug' require 'active_model_serializers' +require 'oj' Dir[File.dirname(__FILE__) + '/shared/contexts/*.rb'].each {|file| require file } @@ -13,6 +14,7 @@ end end +Oj.optimize_rails ActiveModel::Serializer.config.adapter = :json_api ActiveModel::Serializer.config.key_transform = :underscore ActiveModelSerializers.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new('/dev/null')) From 7f275430810f215019a41f8ef76ff00da50ec651 Mon Sep 17 00:00:00 2001 From: Sergii Makagon Date: Sun, 4 Feb 2018 15:41:03 -0500 Subject: [PATCH 08/53] Refactoring --- .gitignore | 3 +- lib/fast_jsonapi/object_serializer.rb | 96 +++++++++++++++++--------- lib/fast_jsonapi/serialization_core.rb | 11 --- spec/shared/contexts/movie_context.rb | 9 +-- 4 files changed, 66 insertions(+), 53 deletions(-) diff --git a/.gitignore b/.gitignore index 816f24ba..33feb8ed 100644 --- a/.gitignore +++ b/.gitignore @@ -26,8 +26,7 @@ pkg #*.tmproj #tmtags -# For vim: -#*.swp +*.swp # For redcar: #.redcar diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index db7f0645..4f67601a 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -30,47 +30,45 @@ module ObjectSerializer end # Set record_type based on the name of the serializer class - set_type default_record_type if default_record_type + set_type(reflected_record_type) if reflected_record_type end def initialize(resource, options = {}) - if options.present? - @meta_tags = options[:meta] - @includes = options[:include].delete_if(&:blank?) if options[:include].present? - self.class.has_permitted_includes(@includes) if @includes.present? - @known_included_objects = {} # keep track of inc objects that have already been serialized - end - # @records if enumerables like Array, ActiveRecord::Relation but if Struct just make it a @record - if resource.respond_to?(:each) && !resource.respond_to?(:each_pair) - @records = resource - else - @record = resource - end + process_options(options) + + @resource = resource end def serializable_hash - serializable_hash = { data: nil } - serializable_hash[:meta] = @meta_tags if @meta_tags.present? - return hash_for_one_record(serializable_hash) if @record - return hash_for_multiple_records(serializable_hash) if @records - serializable_hash + return hash_for_collection if is_collection?(@resource) + + hash_for_one_record end - def hash_for_one_record(serializable_hash) - serializable_hash[:data] = self.class.record_hash(@record) - serializable_hash[:included] = self.class.get_included_records(@record, @includes, @known_included_objects) if @includes.present? + def hash_for_one_record + serializable_hash = { data: nil } + serializable_hash[:meta] = @meta if @meta.present? + + return serializable_hash unless @resource + + serializable_hash[:data] = self.class.record_hash(@resource) + serializable_hash[:included] = self.class.get_included_records(@resource, @includes, @known_included_objects) if @includes.present? serializable_hash end - def hash_for_multiple_records(serializable_hash) + def hash_for_collection + serializable_hash = {} + data = [] included = [] - @records.each do |record| + @resource.each do |record| data << self.class.record_hash(record) included.concat self.class.get_included_records(record, @includes, @known_included_objects) if @includes.present? end + serializable_hash[:data] = data serializable_hash[:included] = included if @includes.present? + serializable_hash[:meta] = @meta if @meta.present? serializable_hash end @@ -78,23 +76,55 @@ def serialized_json self.class.to_json(serializable_hash) end + private + + def process_options(options) + return if options.blank? + + @known_included_objects = {} + @meta = options[:meta] + + if options[:include].present? + @includes = options[:include].delete_if(&:blank?) + validate_includes!(@includes) + end + end + + def validate_includes!(includes) + return if includes.blank? + + existing_relationships = self.class.relationships_to_serialize.keys.to_set + + unless existing_relationships.superset?(includes.to_set) + raise ArgumentError, "One of keys from #{includes} is not specified as a relationship on the serializer" + end + end + + def is_collection?(resource) + resource.respond_to?(:each) && !resource.respond_to?(:each_pair) + end + class_methods do def use_hyphen @hyphenated = true end - def set_type(type_name) - self.record_type = type_name - if @hyphenated - self.record_type = type_name.to_s.dasherize.to_sym - end + def set_type(type) + return unless type + + type = type.to_s.underscore + type = type.dasherize if @hyphenated + + self.record_type = type.to_sym end - def default_record_type - if self.name.end_with?('Serializer') - class_name = self.name.demodulize - range_end = class_name.rindex('Serializer') - class_name[0...range_end].underscore.to_sym + def reflected_record_type + return @reflected_record_type if defined?(@reflected_record_type) + + @reflected_record_type ||= begin + if self.name.end_with?('Serializer') + self.name.split('::').last.chomp('Serializer').underscore.to_sym + end end end diff --git a/lib/fast_jsonapi/serialization_core.rb b/lib/fast_jsonapi/serialization_core.rb index 77e7cf88..ba3e9efb 100644 --- a/lib/fast_jsonapi/serialization_core.rb +++ b/lib/fast_jsonapi/serialization_core.rb @@ -88,17 +88,6 @@ def get_included_records(record, includes_list, known_included_objects) end end end - - def has_permitted_includes(requested_includes) - # requested includes should be within relationships defined on serializer - allowed_includes = @relationships_to_serialize.keys - intersection = allowed_includes & requested_includes - if intersection.sort == requested_includes.sort - true - else - raise ArgumentError, "One of keys from #{requested_includes} is not specified as a relationship on the serializer" - end - end end end end diff --git a/spec/shared/contexts/movie_context.rb b/spec/shared/contexts/movie_context.rb index 8a648812..1564f2b0 100644 --- a/spec/shared/contexts/movie_context.rb +++ b/spec/shared/contexts/movie_context.rb @@ -123,9 +123,7 @@ class HyphenMovieTypeSerializer :id, :name, :release_year, :actor_ids, :actors, :owner_id, :owner, :movie_type_id ) - ActorStruct = Struct.new( - :id, :name, :email - ) + ActorStruct = Struct.new(:id, :name, :email) end after(:context) do @@ -151,10 +149,7 @@ class HyphenMovieTypeSerializer actors = [] 3.times.each do |id| - a = ActorStruct.new - a[:id] = id - a[:name] = id.to_s - actors << a + actors << ActorStruct.new(id, id.to_s, id.to_s) end m = MovieStruct.new From dbdc85a8756b1a60742ee001235da8a8716085ff Mon Sep 17 00:00:00 2001 From: Shishir Kakaraddi Date: Sat, 3 Feb 2018 10:32:22 -0800 Subject: [PATCH 09/53] initial commit removing all the dependencies --- .gitignore | 3 - Gemfile.lock | 55 +---------- Rakefile | 55 ----------- fast_jsonapi.gemspec | 128 ++++++-------------------- lib/fast_jsonapi/object_serializer.rb | 1 - 5 files changed, 30 insertions(+), 212 deletions(-) delete mode 100644 Rakefile diff --git a/.gitignore b/.gitignore index 33feb8ed..4993663b 100644 --- a/.gitignore +++ b/.gitignore @@ -16,9 +16,6 @@ doc # For MacOS: .DS_Store -# juwelier generated -pkg - # For MacOS: .DS_Store diff --git a/Gemfile.lock b/Gemfile.lock index 890e43b4..47bcbea4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,10 +2,8 @@ PATH remote: . specs: fast_jsonapi (1.0.17) - activerecord (~> 5.0) activesupport (~> 5.0) multi_json (~> 1.12) - oj (~> 3.3) GEM remote: https://rubygems.org/ @@ -39,8 +37,6 @@ GEM i18n (~> 0.7) minitest (~> 5.1) tzinfo (~> 1.1) - addressable (2.5.2) - public_suffix (>= 2.0.2, < 4.0) arel (8.0.0) benchmark-perf (0.2.1) builder (3.2.3) @@ -49,55 +45,20 @@ GEM activesupport concurrent-ruby (1.0.5) crass (1.0.3) - descendants_tracker (0.0.4) - thread_safe (~> 0.3, >= 0.3.1) diff-lcs (1.3) - docile (1.1.5) erubi (1.7.0) - faraday (0.12.2) - multipart-post (>= 1.2, < 3) - git (1.3.0) - github_api (0.18.2) - addressable (~> 2.4) - descendants_tracker (~> 0.0.4) - faraday (~> 0.8) - hashie (~> 3.5, >= 3.5.2) - oauth2 (~> 1.0) - hashie (3.5.6) - highline (1.7.10) i18n (0.9.1) concurrent-ruby (~> 1.0) - json (1.8.6) jsonapi-renderer (0.2.0) - juwelier (2.1.3) - builder - bundler (>= 1.13) - git (>= 1.2.5) - github_api - highline (>= 1.6.15) - nokogiri (>= 1.5.10) - rake - rdoc - semver - jwt (1.5.6) loofah (2.1.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) mini_portile2 (2.3.0) minitest (5.10.3) - multi_json (1.12.2) - multi_xml (0.6.0) - multipart-post (2.0.0) + multi_json (1.13.1) nokogiri (1.8.1) mini_portile2 (~> 2.3.0) - oauth2 (1.4.0) - faraday (>= 0.8, < 0.13) - jwt (~> 1.0) - multi_json (~> 1.3) - multi_xml (~> 0.5) - rack (>= 1.2, < 3) oj (3.4.0) - public_suffix (3.0.1) rack (2.0.3) rack-test (0.8.2) rack (>= 1.0, < 3) @@ -106,9 +67,6 @@ GEM nokogiri (>= 1.6) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - rake (12.3.0) - rdoc (3.12.2) - json (~> 1.4) rspec (3.5.0) rspec-core (~> 3.5.0) rspec-expectations (~> 3.5.0) @@ -125,12 +83,6 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.5.0) rspec-support (3.5.0) - semver (1.0.1) - simplecov (0.15.1) - docile (~> 1.1.0) - json (>= 1.8, < 3) - simplecov-html (~> 0.10.0) - simplecov-html (0.10.2) skylight (1.5.0) activesupport (>= 3.0.0) sqlite3 (1.3.13) @@ -143,14 +95,13 @@ PLATFORMS DEPENDENCIES active_model_serializers (~> 0.10.4) + activerecord (~> 5.0) bundler (~> 1.0) byebug fast_jsonapi! - juwelier (~> 2.1.0) - rdoc (~> 3.12) + oj (~> 3.3) rspec (~> 3.5.0) rspec-benchmark (~> 0.3.0) - simplecov skylight (~> 1.3) sqlite3 (~> 1.3) diff --git a/Rakefile b/Rakefile deleted file mode 100644 index cfaa4ebd..00000000 --- a/Rakefile +++ /dev/null @@ -1,55 +0,0 @@ -# encoding: utf-8 - -require 'rubygems' -require 'bundler' -begin - Bundler.setup(:default, :development) -rescue Bundler::BundlerError => e - $stderr.puts e.message - $stderr.puts "Run `bundle install` to install missing gems" - exit e.status_code -end -require 'rake' -require 'juwelier' -Juwelier::Tasks.new do |gem| - # gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options - gem.name = "fast_jsonapi" - gem.homepage = "http://github.com/Netflix/fast_jsonapi" - gem.license = "Apache-2.0" - gem.summary = %Q{fast JSON API(jsonapi.org) serializer} - gem.description = %Q{JSON API(jsonapi.org) serializer that works with rails and can be used to serialize any kind of ruby objects} - gem.email = "" - gem.authors = ["Shishir Kakaraddi", "Srinivas Raghunathan", "Adam Gross"] - - - if gem.respond_to?(:metadata) - gem.metadata['allowed_push_host'] = 'https://rubygems.org' - else - raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.' - end - # dependencies defined in Gemfile -end -Juwelier::RubygemsDotOrgTasks.new -require 'rspec/core' -require 'rspec/core/rake_task' -RSpec::Core::RakeTask.new(:spec) do |spec| - spec.pattern = FileList['spec/**/*_spec.rb'] -end - -desc "Code coverage detail" -task :simplecov do - ENV['COVERAGE'] = "true" - Rake::Task['spec'].execute -end - -task :default => :spec - -require 'rdoc/task' -Rake::RDocTask.new do |rdoc| - version = File.exist?('VERSION') ? File.read('VERSION') : "" - - rdoc.rdoc_dir = 'rdoc' - rdoc.title = "fast_jsonapi #{version}" - rdoc.rdoc_files.include('README*') - rdoc.rdoc_files.include('lib/**/*.rb') -end diff --git a/fast_jsonapi.gemspec b/fast_jsonapi.gemspec index f52a7a94..5b7a93ca 100644 --- a/fast_jsonapi.gemspec +++ b/fast_jsonapi.gemspec @@ -1,108 +1,34 @@ -# Generated by juwelier -# DO NOT EDIT THIS FILE DIRECTLY -# Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec' -# -*- encoding: utf-8 -*- -# stub: fast_jsonapi 1.0.16 ruby lib +Gem::Specification.new do |gem| + gem.name = "fast_jsonapi" + gem.version = "1.0.17" -Gem::Specification.new do |s| - s.name = "fast_jsonapi" - s.version = "1.0.17" - - s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= - s.metadata = { "allowed_push_host" => "https://rubygems.org" } if s.respond_to? :metadata= - s.require_paths = ["lib"] - s.authors = ["Shishir Kakaraddi", "Srinivas Raghunathan", "Adam Gross"] - s.date = "2018-02-01" - s.description = "JSON API(jsonapi.org) serializer that works with rails and can be used to serialize any kind of ruby objects" - s.email = "" - s.extra_rdoc_files = [ + gem.required_rubygems_version = Gem::Requirement.new(">= 0") if gem.respond_to? :required_rubygems_version= + gem.metadata = { "allowed_push_host" => "https://rubygemgem.org" } if gem.respond_to? :metadata= + gem.require_paths = ["lib"] + gem.authors = ["Shishir Kakaraddi", "Srinivas Raghunathan", "Adam Gross"] + gem.date = "2018-02-01" + gem.description = "JSON API(jsonapi.org) serializer that works with rails and can be used to serialize any kind of ruby objects" + gem.email = "" + gem.extra_rdoc_files = [ "LICENSE.txt", "README.md", "README.rdoc" ] - s.files = [ - ".document", - ".rspec", - "Gemfile", - "Gemfile.lock", - "LICENSE.txt", - "README.md", - "README.rdoc", - "Rakefile", - "VERSION", - "docs/collection_serializer_output.json", - "docs/object_serializer.json", - "fast_jsonapi.gemspec", - "lib/extensions/has_one.rb", - "lib/fast_jsonapi.rb", - "lib/fast_jsonapi/object_serializer.rb", - "lib/fast_jsonapi/serialization_core.rb", - "spec/lib/extensions/active_record_spec.rb", - "spec/lib/object_serializer_caching_spec.rb", - "spec/lib/object_serializer_class_methods_spec.rb", - "spec/lib/object_serializer_hyphen_spec.rb", - "spec/lib/object_serializer_performance_spec.rb", - "spec/lib/object_serializer_spec.rb", - "spec/lib/object_serializer_struct_spec.rb", - "spec/lib/serialization_core_spec.rb", - "spec/shared/contexts/ams_context.rb", - "spec/shared/contexts/movie_context.rb", - "spec/spec_helper.rb" - ] - s.homepage = "http://github.com/Netflix/fast_jsonapi" - s.licenses = ["Apache-2.0"] - s.rubygems_version = "2.5.1" - s.summary = "fast JSON API(jsonapi.org) serializer" - - if s.respond_to? :specification_version then - s.specification_version = 4 + gem.files = Dir["lib/**/*"] + gem.homepage = "http://github.com/Netflix/fast_jsonapi" + gem.licenses = ["Apache-2.0"] + gem.rubygems_version = "2.5.1" + gem.summary = "fast JSON API(jsonapi.org) serializer" - if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then - s.add_runtime_dependency(%q, ["~> 5.0"]) - s.add_runtime_dependency(%q, ["~> 1.12"]) - s.add_runtime_dependency(%q, ["~> 3.3"]) - s.add_runtime_dependency(%q, ["~> 5.0"]) - s.add_development_dependency(%q, ["~> 1.3"]) - s.add_development_dependency(%q, ["~> 3.5.0"]) - s.add_development_dependency(%q, ["~> 0.3.0"]) - s.add_development_dependency(%q, ["~> 3.12"]) - s.add_development_dependency(%q, ["~> 1.0"]) - s.add_development_dependency(%q, ["~> 2.1.0"]) - s.add_development_dependency(%q, [">= 0"]) - s.add_development_dependency(%q, [">= 0"]) - s.add_development_dependency(%q, ["~> 0.10.4"]) - s.add_development_dependency(%q, ["~> 1.3"]) - else - s.add_dependency(%q, ["~> 5.0"]) - s.add_dependency(%q, ["~> 1.12"]) - s.add_dependency(%q, ["~> 3.3"]) - s.add_dependency(%q, ["~> 1.3"]) - s.add_dependency(%q, ["~> 5.0"]) - s.add_dependency(%q, ["~> 3.5.0"]) - s.add_dependency(%q, ["~> 0.3.0"]) - s.add_dependency(%q, ["~> 3.12"]) - s.add_dependency(%q, ["~> 1.0"]) - s.add_dependency(%q, ["~> 2.1.0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, ["~> 0.10.4"]) - s.add_dependency(%q, ["~> 1.3"]) - end - else - s.add_dependency(%q, ["~> 5.0"]) - s.add_dependency(%q, ["~> 1.12"]) - s.add_dependency(%q, ["~> 3.3"]) - s.add_dependency(%q, ["~> 1.3"]) - s.add_dependency(%q, ["~> 5.0"]) - s.add_dependency(%q, ["~> 3.5.0"]) - s.add_dependency(%q, ["~> 0.3.0"]) - s.add_dependency(%q, ["~> 3.12"]) - s.add_dependency(%q, ["~> 1.0"]) - s.add_dependency(%q, ["~> 2.1.0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, ["~> 0.10.4"]) - s.add_dependency(%q, ["~> 1.3"]) - end + gem.add_runtime_dependency(%q, ["~> 5.0"]) + gem.add_runtime_dependency(%q, ["~> 1.12"]) + gem.add_development_dependency(%q, ["~> 5.0"]) + gem.add_development_dependency(%q, ["~> 1.3"]) + gem.add_development_dependency(%q, ["~> 3.5.0"]) + gem.add_development_dependency(%q, ["~> 3.3"]) + gem.add_development_dependency(%q, ["~> 0.3.0"]) + gem.add_development_dependency(%q, ["~> 1.0"]) + gem.add_development_dependency(%q, [">= 0"]) + gem.add_development_dependency(%q, ["~> 0.10.4"]) + gem.add_development_dependency(%q, ["~> 1.3"]) end - diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index 4f67601a..4c77dae5 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -1,7 +1,6 @@ require 'active_support/core_ext/object' require 'active_support/concern' require 'active_support/inflector' -require 'oj' require 'multi_json' require 'fast_jsonapi/serialization_core' From 799499c159b51e61ac9800e3b2a91569b4ceb057 Mon Sep 17 00:00:00 2001 From: Shishir Kakaraddi Date: Sat, 3 Feb 2018 10:36:18 -0800 Subject: [PATCH 10/53] updating documentation and travis --- .travis.yml | 2 +- README.md | 52 +++------------------------------------------------- 2 files changed, 4 insertions(+), 50 deletions(-) diff --git a/.travis.yml b/.travis.yml index 39713389..ca4855d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,4 +4,4 @@ rvm: - 2.4.3 - 2.5.0 script: - - bundle exec rake spec + - bundle exec rspec diff --git a/README.md b/README.md index 914219ad..bac08b48 100644 --- a/README.md +++ b/README.md @@ -177,61 +177,15 @@ serializer | Set custom Serializer for a relationship | ```has_many :actors, ser ## Contributing - -Please follow the steps on [contribution check](https://github.com/Netflix/fast_jsonapi/blob/master/CONTRIBUTING.md). -This gem is built using a gem building gem called [juwelier](https://github.com/flajann2/juwelier). - -Beyond just editing source code, you’ll be interacting with the gem using rake a lot. To see all the tasks available with a brief description, you can run: - -```bash -rake -T -``` - -### Updating Project information -You can update the project information of the gem by updating the [Rakefile](Rakefile). Then you need to generate a new gemspec: - -```bash -rake gemspec -``` +Please see [contribution check](https://github.com/Netflix/fast_jsonapi/blob/master/CONTRIBUTING.md) for more details on contributing ### Running Tests -We use [RSpec](http://rspec.info/) for testing. We have unit tests, functional tests and performance tests. To run tests use the following rake task: +We use [RSpec](http://rspec.info/) for testing. We have unit tests, functional tests and performance tests. To run tests use the following command: ```bash -rake spec +rspec ``` -### Installation - -```bash -$ rake install -``` - -The install rake task builds the gem and then installs it. You're all -set if you're using [RVM](http://rvm.beginrescueend.com/), but you may -need to run it with sudo if you have a system-installed Ruby: - -### Bumping Version - -It feels good to release code. Do it, do it often. But before that, bump -the version. Then release it. There's a few ways to update the version: - -```bash -# version:write like before -$ rake version:write MAJOR=0 MINOR=3 PATCH=0 - -# bump just major, ie 0.1.0 -> 1.0.0 -$ rake version:bump:major - -# bump just minor, ie 0.1.0 -> 0.2.0 -$ rake version:bump:minor - -# bump just patch, ie 0.1.0 -> 0.1.1 -$ rake version:bump:patch -``` - ---- - ### We're Hiring! Join the Netflix Studio Engineering team and help us build gems like this! From 639c314272948f72542a20eee90db4af620b62a0 Mon Sep 17 00:00:00 2001 From: Guillermo Iguaran Date: Sun, 4 Feb 2018 17:31:32 -0500 Subject: [PATCH 11/53] Use Object#public_send to respect attributes visibility --- lib/fast_jsonapi/serialization_core.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/fast_jsonapi/serialization_core.rb b/lib/fast_jsonapi/serialization_core.rb index ba3e9efb..7e79b14c 100644 --- a/lib/fast_jsonapi/serialization_core.rb +++ b/lib/fast_jsonapi/serialization_core.rb @@ -28,7 +28,7 @@ def ids_hash(ids, record_type) def attributes_hash(record) attributes_to_serialize.each_with_object({}) do |(key, method_name), attr_hash| - attr_hash[key] = record.send(method_name) + attr_hash[key] = record.public_send(method_name) end end @@ -41,7 +41,7 @@ def relationships_hash(record, relationships = nil) record_type = relationship[:record_type] empty_case = relationship[:relationship_type] == :has_many ? [] : nil hash[name] = { - data: ids_hash(record.send(id_method_name), record_type) || empty_case + data: ids_hash(record.public_send(id_method_name), record_type) || empty_case } end end From a3a989244bf5e057cc18f27e3d819407f2ac9a4b Mon Sep 17 00:00:00 2001 From: Shishir Kakaraddi Date: Sun, 4 Feb 2018 13:01:24 -0800 Subject: [PATCH 12/53] initial commit to allow the same kind of key transforms as AMS --- lib/fast_jsonapi/object_serializer.rb | 72 ++++++++--------- spec/lib/object_serializer_hyphen_spec.rb | 40 ---------- .../object_serializer_key_transform_spec.rb | 80 +++++++++++++++++++ spec/shared/contexts/movie_context.rb | 22 ----- 4 files changed, 113 insertions(+), 101 deletions(-) delete mode 100644 spec/lib/object_serializer_hyphen_spec.rb create mode 100644 spec/lib/object_serializer_key_transform_spec.rb diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index 4c77dae5..da858f5d 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -104,19 +104,6 @@ def is_collection?(resource) end class_methods do - def use_hyphen - @hyphenated = true - end - - def set_type(type) - return unless type - - type = type.to_s.underscore - type = type.dasherize if @hyphenated - - self.record_type = type.to_sym - end - def reflected_record_type return @reflected_record_type if defined?(@reflected_record_type) @@ -127,6 +114,28 @@ def reflected_record_type end end + def set_key_transform(transform_name) + mapping = { + camel: :camelize, + camel_lower: [:camelize, :lower], + dash: :dasherize, + underscore: :underscore + } + @transform_method = mapping[transform_name.to_sym] + end + + def run_key_transform(input) + if @transform_method.present? + input.to_s.send(*@transform_method).to_sym + else + input.to_sym + end + end + + def set_type(type_name) + self.record_type = run_key_transform(type_name) + end + def cache_options(cache_options) self.cached = cache_options[:enabled] || false self.cache_length = cache_options[:cache_length] || 5.minutes @@ -137,10 +146,7 @@ def attributes(*attributes_list) self.attributes_to_serialize = {} if self.attributes_to_serialize.nil? attributes_list.each do |attr_name| method_name = attr_name - key = method_name - if @hyphenated - key = attr_name.to_s.dasherize.to_sym - end + key = run_key_transform(method_name) attributes_to_serialize[key] = method_name end end @@ -159,15 +165,11 @@ def add_relationship(name, relationship) end def has_many(relationship_name, options = {}) - singular_name = relationship_name.to_s.singularize - record_type = options[:record_type] || singular_name.to_sym name = relationship_name.to_sym - key = options[:key] || name - if @hyphenated - key = options[:key] || relationship_name.to_s.dasherize.to_sym - record_type = options[:record_type] || singular_name.to_s.dasherize.to_sym - end - serializer_key = options[:serializer] || record_type + singular_name = relationship_name.to_s.singularize + serializer_key = options[:serializer] || singular_name.to_sym + key = options[:key] || run_key_transform(relationship_name) + record_type = options[:record_type] || run_key_transform(singular_name) relationship = { key: key, name: name, @@ -183,13 +185,9 @@ def has_many(relationship_name, options = {}) def belongs_to(relationship_name, options = {}) name = relationship_name.to_sym - key = options[:key] || name - record_type = options[:record_type] || name - serializer_key = options[:serializer] || record_type - if @hyphenated - key = options[:key] || relationship_name.to_s.dasherize.to_sym - record_type = options[:record_type] || relationship_name.to_s.dasherize.to_sym - end + serializer_key = options[:serializer] || relationship_name.to_sym + key = options[:key] || run_key_transform(relationship_name) + record_type = options[:record_type] || run_key_transform(relationship_name) add_relationship(name, { key: key, name: name, @@ -204,13 +202,9 @@ def belongs_to(relationship_name, options = {}) def has_one(relationship_name, options = {}) name = relationship_name.to_sym - key = options[:key] || name - record_type = options[:record_type] || name - serializer_key = options[:serializer] || record_type - if @hyphenated - key = options[:key] || relationship_name.to_s.dasherize.to_sym - record_type = options[:record_type] || relationship_name.to_s.dasherize.to_sym - end + serializer_key = options[:serializer] || name + key = options[:key] || run_key_transform(relationship_name) + record_type = options[:record_type] || run_key_transform(relationship_name) add_relationship(name, { key: key, name: name, diff --git a/spec/lib/object_serializer_hyphen_spec.rb b/spec/lib/object_serializer_hyphen_spec.rb deleted file mode 100644 index fc529792..00000000 --- a/spec/lib/object_serializer_hyphen_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'spec_helper' - -describe FastJsonapi::ObjectSerializer do - include_context 'movie class' - include_context 'ams movie class' - - context 'when using hyphens for word separation in the JSON API members' do - it 'returns correct hash when serializable_hash is called' do - serializable_hash = HyphenMovieSerializer.new([movie, movie]).serializable_hash - expect(serializable_hash[:data].length).to eq 2 - expect(serializable_hash[:data][0][:relationships].length).to eq 3 - expect(serializable_hash[:data][0][:relationships]).to have_key('movie-type'.to_sym) - expect(serializable_hash[:data][0][:attributes].length).to eq 2 - expect(serializable_hash[:data][0][:attributes]).to have_key("release-year".to_sym) - - serializable_hash = HyphenMovieSerializer.new(movie_struct).serializable_hash - expect(serializable_hash[:data][:relationships].length).to eq 3 - expect(serializable_hash[:data][:relationships]).to have_key('movie-type'.to_sym) - expect(serializable_hash[:data][:attributes].length).to eq 2 - expect(serializable_hash[:data][:attributes]).to have_key('release-year'.to_sym) - expect(serializable_hash[:data][:id]).to eq movie_struct.id.to_s - end - - it 'returns same thing as ams' do - ams_movie = build_ams_movies(1).first - movie = build_movies(1).first - our_json = HyphenMovieSerializer.new([movie]).serialized_json - ams_json = ActiveModelSerializers::SerializableResource.new([ams_movie], key_transform: :dash).to_json - expect(our_json.length).to eq (ams_json.length) - end - - it 'returns type hypenated when trying to serializing a class with multiple words' do - movie_type = MovieType.new - movie_type.id = 3 - movie_type.name = "x" - serializable_hash = HyphenMovieTypeSerializer.new(movie_type).serializable_hash - expect(serializable_hash[:data][:type].to_sym).to eq 'movie-type'.to_sym - end - end -end diff --git a/spec/lib/object_serializer_key_transform_spec.rb b/spec/lib/object_serializer_key_transform_spec.rb new file mode 100644 index 00000000..77338b14 --- /dev/null +++ b/spec/lib/object_serializer_key_transform_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe FastJsonapi::ObjectSerializer do + include_context 'movie class' + include_context 'ams movie class' + + before(:context) do + [:dash, :camel, :camel_lower, :underscore].each do |transform_type| + movie_serializer_name = "#{transform_type}_movie_serializer".classify + movie_type_serializer_name = "#{transform_type}_movie_type_serializer".classify + # https://stackoverflow.com/questions/4113479/dynamic-class-definition-with-a-class-name + movie_serializer_class = Object.const_set( + movie_serializer_name, + Class.new { + } + ) + # https://rubymonk.com/learning/books/5-metaprogramming-ruby-ascent/chapters/24-eval/lessons/67-instance-eval + movie_serializer_class.instance_eval do + include FastJsonapi::ObjectSerializer + set_type :movie + set_key_transform transform_type + attributes :name, :release_year + has_many :actors + belongs_to :owner, record_type: :user + belongs_to :movie_type + end + movie_type_serializer_class = Object.const_set( + movie_type_serializer_name, + Class.new { + } + ) + movie_type_serializer_class.instance_eval do + include FastJsonapi::ObjectSerializer + set_key_transform transform_type + set_type :movie_type + attributes :name + end + end + end + + context 'when using dashes for word separation in the JSON API members' do + it 'returns correct hash when serializable_hash is called' do + serializable_hash = DashMovieSerializer.new([movie, movie]).serializable_hash + expect(serializable_hash[:data].length).to eq 2 + expect(serializable_hash[:data][0][:relationships].length).to eq 3 + expect(serializable_hash[:data][0][:relationships]).to have_key('movie-type'.to_sym) + expect(serializable_hash[:data][0][:attributes].length).to eq 2 + expect(serializable_hash[:data][0][:attributes]).to have_key("release-year".to_sym) + + serializable_hash = DashMovieSerializer.new(movie_struct).serializable_hash + expect(serializable_hash[:data][:relationships].length).to eq 3 + expect(serializable_hash[:data][:relationships]).to have_key('movie-type'.to_sym) + expect(serializable_hash[:data][:attributes].length).to eq 2 + expect(serializable_hash[:data][:attributes]).to have_key('release-year'.to_sym) + expect(serializable_hash[:data][:id]).to eq movie_struct.id.to_s + end + + it 'returns type hypenated when trying to serializing a class with multiple words' do + movie_type = MovieType.new + movie_type.id = 3 + movie_type.name = "x" + serializable_hash = DashMovieTypeSerializer.new(movie_type).serializable_hash + expect(serializable_hash[:data][:type].to_sym).to eq 'movie-type'.to_sym + end + end + + context 'when using other key transforms' do + [:camel, :camel_lower, :underscore, :dash].each do |transform_type| + it "returns same thing as ams when using #{transform_type}" do + ams_movie = build_ams_movies(1).first + movie = build_movies(1).first + movie_serializer_class = "#{transform_type}_movie_serializer".classify.constantize + our_json = movie_serializer_class.new([movie]).serialized_json + ams_json = ActiveModelSerializers::SerializableResource.new([ams_movie], key_transform: transform_type).to_json + expect(our_json.length).to eq (ams_json.length) + end + end + end + +end diff --git a/spec/shared/contexts/movie_context.rb b/spec/shared/contexts/movie_context.rb index 1564f2b0..6ffe9a81 100644 --- a/spec/shared/contexts/movie_context.rb +++ b/spec/shared/contexts/movie_context.rb @@ -95,28 +95,6 @@ class MovieSerializer end end - - # Hyphenated keys for the serializer - before(:context) do - class HyphenMovieSerializer - include FastJsonapi::ObjectSerializer - use_hyphen - set_type :movie - attributes :name, :release_year - has_many :actors - belongs_to :owner, record_type: :user - belongs_to :movie_type - end - - class HyphenMovieTypeSerializer - include FastJsonapi::ObjectSerializer - use_hyphen - set_type :movie_type - attributes :name - end - end - - # Movie and Actor struct before(:context) do MovieStruct = Struct.new( From fa332019bc815c4cc3b28bb360eb39c9faf105e5 Mon Sep 17 00:00:00 2001 From: Shishir Kakaraddi Date: Sun, 4 Feb 2018 13:11:14 -0800 Subject: [PATCH 13/53] adding documentation about key transforms --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index bac08b48..7b23755e 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Fast JSON API serialized 250 records in 3.01 ms * [Serializer Definition](#serializer-definition) * [Object Serialization](#object-serialization) * [Compound Document](#compound-document) + * [Key Transforms](#key-transforms) * [Collection Serialization](#collection-serialization) * [Caching](#caching) * [Contributing](#contributing) @@ -134,6 +135,26 @@ json_string = MovieSerializer.new(movie).serialized_json } ``` + +### Key Transforms +By default fast_jsonapi underscores the key names. It supports the same key transforms that are supported by AMS. Here is the syntax of specifying a key transform + +```ruby +class MovieSerializer + include FastJsonapi::ObjectSerializer + # Available options :camel, :camel_lower, :dash, :underscore(default) + set_key_transform :camel +end +``` +Here are examples of how these options transform the keys + +```ruby +set_key_transform :camel # "some_key" => "SomeKey" +set_key_transform :camel_lower # "some_key" => "someKey" +set_key_transform :dash # "some_key" => "some-key" +set_key_transform :underscore # "some_key" => "some_key" +``` + ### Compound Document Support for top-level included member through ` options[:include] `. From 30b254e88ca1b103acf53006a051f181be2ced05 Mon Sep 17 00:00:00 2001 From: Nicolas Leger Date: Sat, 3 Feb 2018 12:53:37 +0100 Subject: [PATCH 14/53] Test against Ruby 2.2 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index ca4855d8..00b98f79 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: ruby rvm: + - 2.2.9 - 2.3.6 - 2.4.3 - 2.5.0 From 5861f27ebb1c5f9f53e12c29e969f5deead77c08 Mon Sep 17 00:00:00 2001 From: "Min.Kim" Date: Fri, 2 Feb 2018 15:33:25 +0000 Subject: [PATCH 15/53] Add jsonapi-rb / require for jsonapi-rb benchmark test --- Gemfile.lock | 23 +++++++++++++++- fast_jsonapi.gemspec | 64 ++++++++++++++++++++++++++++++++++++-------- spec/spec_helper.rb | 1 + 3 files changed, 76 insertions(+), 12 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 47bcbea4..5cc42dec 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -50,6 +50,25 @@ GEM i18n (0.9.1) concurrent-ruby (~> 1.0) jsonapi-renderer (0.2.0) + json (1.8.6) + jsonapi-deserializable (0.2.0) + jsonapi-rb (0.5.0) + jsonapi-deserializable (~> 0.2.0) + jsonapi-serializable (~> 0.3.0) + jsonapi-renderer (0.2.0) + jsonapi-serializable (0.3.0) + jsonapi-renderer (~> 0.2.0) + juwelier (2.1.3) + builder + bundler (>= 1.13) + git (>= 1.2.5) + github_api + highline (>= 1.6.15) + nokogiri (>= 1.5.10) + rake + rdoc + semver + jwt (1.5.6) loofah (2.1.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) @@ -99,7 +118,9 @@ DEPENDENCIES bundler (~> 1.0) byebug fast_jsonapi! - oj (~> 3.3) + jsonapi-rb (~> 0.5.0) + juwelier (~> 2.1.0) + rdoc (~> 3.12) rspec (~> 3.5.0) rspec-benchmark (~> 0.3.0) skylight (~> 1.3) diff --git a/fast_jsonapi.gemspec b/fast_jsonapi.gemspec index 5b7a93ca..00cff648 100644 --- a/fast_jsonapi.gemspec +++ b/fast_jsonapi.gemspec @@ -20,15 +20,57 @@ Gem::Specification.new do |gem| gem.rubygems_version = "2.5.1" gem.summary = "fast JSON API(jsonapi.org) serializer" - gem.add_runtime_dependency(%q, ["~> 5.0"]) - gem.add_runtime_dependency(%q, ["~> 1.12"]) - gem.add_development_dependency(%q, ["~> 5.0"]) - gem.add_development_dependency(%q, ["~> 1.3"]) - gem.add_development_dependency(%q, ["~> 3.5.0"]) - gem.add_development_dependency(%q, ["~> 3.3"]) - gem.add_development_dependency(%q, ["~> 0.3.0"]) - gem.add_development_dependency(%q, ["~> 1.0"]) - gem.add_development_dependency(%q, [">= 0"]) - gem.add_development_dependency(%q, ["~> 0.10.4"]) - gem.add_development_dependency(%q, ["~> 1.3"]) + if s.respond_to? :specification_version then + s.specification_version = 4 + + if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then + s.add_runtime_dependency(%q, ["~> 5.0"]) + s.add_runtime_dependency(%q, ["~> 1.12"]) + s.add_runtime_dependency(%q, ["~> 3.3"]) + s.add_runtime_dependency(%q, ["~> 5.0"]) + s.add_development_dependency(%q, ["~> 1.3"]) + s.add_development_dependency(%q, ["~> 3.5.0"]) + s.add_development_dependency(%q, ["~> 0.3.0"]) + s.add_development_dependency(%q, ["~> 3.12"]) + s.add_development_dependency(%q, ["~> 1.0"]) + s.add_development_dependency(%q, ["~> 2.1.0"]) + s.add_development_dependency(%q, [">= 0"]) + s.add_development_dependency(%q, [">= 0"]) + s.add_development_dependency(%q, ["~> 0.10.4"]) + s.add_development_dependency(%q, ["~> 1.3"]) + s.add_development_dependency(%q, ["~> 0.5.0"]) + else + s.add_dependency(%q, ["~> 5.0"]) + s.add_dependency(%q, ["~> 1.12"]) + s.add_dependency(%q, ["~> 3.3"]) + s.add_dependency(%q, ["~> 1.3"]) + s.add_dependency(%q, ["~> 5.0"]) + s.add_dependency(%q, ["~> 3.5.0"]) + s.add_dependency(%q, ["~> 0.3.0"]) + s.add_dependency(%q, ["~> 3.12"]) + s.add_dependency(%q, ["~> 1.0"]) + s.add_dependency(%q, ["~> 2.1.0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, ["~> 0.10.4"]) + s.add_dependency(%q, ["~> 1.3"]) + s.add_dependency(%q, ["~> 0.5.0"]) + end + else + s.add_dependency(%q, ["~> 5.0"]) + s.add_dependency(%q, ["~> 1.12"]) + s.add_dependency(%q, ["~> 3.3"]) + s.add_dependency(%q, ["~> 1.3"]) + s.add_dependency(%q, ["~> 5.0"]) + s.add_dependency(%q, ["~> 3.5.0"]) + s.add_dependency(%q, ["~> 0.3.0"]) + s.add_dependency(%q, ["~> 3.12"]) + s.add_dependency(%q, ["~> 1.0"]) + s.add_dependency(%q, ["~> 2.1.0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, ["~> 0.10.4"]) + s.add_dependency(%q, ["~> 1.3"]) + s.add_dependency(%q, ["~> 0.5.0"]) + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c278cdca..a82b893e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,6 +4,7 @@ require 'byebug' require 'active_model_serializers' require 'oj' +require 'jsonapi/serializable' Dir[File.dirname(__FILE__) + '/shared/contexts/*.rb'].each {|file| require file } From 4ccd40ac29cedf2a065a4fe39d3c5d3de0288991 Mon Sep 17 00:00:00 2001 From: "Min.Kim" Date: Mon, 5 Feb 2018 12:17:29 +0000 Subject: [PATCH 16/53] Add jsonapi data models for benchmark --- .../lib/object_serializer_performance_spec.rb | 137 +++++++++++------- spec/shared/contexts/jsonapi_context.rb | 95 ++++++++++++ 2 files changed, 178 insertions(+), 54 deletions(-) create mode 100644 spec/shared/contexts/jsonapi_context.rb diff --git a/spec/lib/object_serializer_performance_spec.rb b/spec/lib/object_serializer_performance_spec.rb index b23b23a9..6b6e874c 100644 --- a/spec/lib/object_serializer_performance_spec.rb +++ b/spec/lib/object_serializer_performance_spec.rb @@ -3,39 +3,40 @@ describe FastJsonapi::ObjectSerializer, performance: true do include_context 'movie class' include_context 'ams movie class' + include_context 'jsonapi movie class' before(:all) { GC.disable } after(:all) { GC.enable } - context 'when testing performance of serialization' do - it 'should create a hash of 1000 records in less than 50 ms' do - movies = 1000.times.map { |_i| movie } - expect { MovieSerializer.new(movies).serializable_hash }.to perform_under(50).ms - end - - it 'should serialize 1000 records to jsonapi in less than 60 ms' do - movies = 1000.times.map { |_i| movie } - expect { MovieSerializer.new(movies).serialized_json }.to perform_under(60).ms - end - - it 'should create a hash of 1000 records with includes and meta in less than 75 ms' do - count = 1000 - movies = count.times.map { |_i| movie } - options = {} - options[:meta] = { total: count } - options[:include] = [:actors] - expect { MovieSerializer.new(movies, options).serializable_hash }.to perform_under(75).ms - end - - it 'should serialize 1000 records to jsonapi with includes and meta in less than 75 ms' do - count = 1000 - movies = count.times.map { |_i| movie } - options = {} - options[:meta] = { total: count } - options[:include] = [:actors] - expect { MovieSerializer.new(movies, options).serialized_json }.to perform_under(75).ms - end - end +# context 'when testing performance of serialization' do +# it 'should create a hash of 1000 records in less than 50 ms' do +# movies = 1000.times.map { |_i| movie } +# expect { MovieSerializer.new(movies).serializable_hash }.to perform_under(50).ms +# end +# +# it 'should serialize 1000 records to jsonapi in less than 60 ms' do +# movies = 1000.times.map { |_i| movie } +# expect { MovieSerializer.new(movies).serialized_json }.to perform_under(60).ms +# end +# +# it 'should create a hash of 1000 records with includes and meta in less than 75 ms' do +# count = 1000 +# movies = count.times.map { |_i| movie } +# options = {} +# options[:meta] = { total: count } +# options[:include] = [:actors] +# expect { MovieSerializer.new(movies, options).serializable_hash }.to perform_under(75).ms +# end +# +# it 'should serialize 1000 records to jsonapi with includes and meta in less than 75 ms' do +# count = 1000 +# movies = count.times.map { |_i| movie } +# options = {} +# options[:meta] = { total: count } +# options[:include] = [:actors] +# expect { MovieSerializer.new(movies, options).serialized_json }.to perform_under(75).ms +# end +# end def print_stats(message, count, ams_time, our_time) format = '%-15s %-10s %s' @@ -62,14 +63,26 @@ def run_json_benchmark(message, movie_count, our_serializer, ams_serializer) end context 'when comparing with AMS 0.10.x' do - [1, 25, 250, 1000].each do |movie_count| + #[1, 25, 250, 1000].each do |movie_count| + [1].each do |movie_count| speed_factor = 25 it "should serialize #{movie_count} records atleast #{speed_factor} times faster than AMS" do ams_movies = build_ams_movies(movie_count) + jsonapi_movies = build_jsonapi_movies(movie_count) movies = build_movies(movie_count) our_serializer = MovieSerializer.new(movies) ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_movies) + puts JSONAPI::Serializable::Renderer.new.render( + jsonapi_movies, + class: { + JSONAPIMovie: JSONAPIMovieSerializer, + JSONAPIActor: JSONAPIActorSerializer, + JSONAPIUser: JSONAPIUserSerializer, + JSONAPIMovieType: JSONAPIMovieTypeSerializer + }, + ).to_json + message = "Serialize to JSON string #{movie_count} records" our_json, ams_json = run_json_benchmark(message, movie_count, our_serializer, ams_serializer) @@ -83,28 +96,44 @@ def run_json_benchmark(message, movie_count, our_serializer, ams_serializer) end end - context 'when comparing with AMS 0.10.x and with includes and meta' do - [1, 25, 250, 1000].each do |movie_count| - speed_factor = 25 - it "should serialize #{movie_count} records atleast #{speed_factor} times faster than AMS" do - ams_movies = build_ams_movies(movie_count) - movies = build_movies(movie_count) - options = {} - options[:meta] = { total: movie_count } - options[:include] = [:actors, :movie_type] - our_serializer = MovieSerializer.new(movies, options) - ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_movies, include: options[:include], meta: options[:meta]) - - message = "Serialize to JSON string #{movie_count} with includes and meta" - our_json, ams_json = run_json_benchmark(message, movie_count, our_serializer, ams_serializer) - - message = "Serialize to Ruby Hash #{movie_count} with includes and meta" - run_hash_benchmark(message, movie_count, our_serializer, ams_serializer) - - expect(our_json.length).to eq ams_json.length - expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times - expect { our_serializer.serializable_hash }.to perform_faster_than { ams_serializer.as_json }.at_least(speed_factor).times - end - end - end +# context 'when comparing with AMS 0.10.x and with includes and meta' do +# #[1, 25, 250, 1000].each do |movie_count| +# [2].each do |movie_count| +# speed_factor = 25 +# it "should serialize #{movie_count} records atleast #{speed_factor} times faster than AMS" do +# ams_movies = build_ams_movies(movie_count) +# movies = build_movies(movie_count) +# options = {} +# options[:meta] = { total: movie_count } +# options[:include] = [:actors, :movie_type] +# our_serializer = MovieSerializer.new(movies, options) +# ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_movies, include: options[:include], meta: options[:meta]) +# +# message = "Serialize to JSON string #{movie_count} with includes and meta" +# our_json, ams_json = run_json_benchmark(message, movie_count, our_serializer, ams_serializer) +# +# puts our_json +# puts ams_json +# +# jsonapi_movies = build_jsonapi_movies(movie_count) +# puts JSONAPI::Serializable::Renderer.new.render( +# jsonapi_movies, +# class: { +# JSONAPIMovie: JSONAPIMovieSerializer, +# JSONAPIActor: JSONAPIActorSerializer, +# JSONAPIUser: JSONAPIUserSerializer, +# JSONAPIMovieType: JSONAPIMovieTypeSerializer +# }, +# include: options[:include] +# ).to_json +# +# message = "Serialize to Ruby Hash #{movie_count} with includes and meta" +# run_hash_benchmark(message, movie_count, our_serializer, ams_serializer) +# +# expect(our_json.length).to eq ams_json.length +# expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times +# expect { our_serializer.serializable_hash }.to perform_faster_than { ams_serializer.as_json }.at_least(speed_factor).times +# end +# end +# end end diff --git a/spec/shared/contexts/jsonapi_context.rb b/spec/shared/contexts/jsonapi_context.rb new file mode 100644 index 00000000..d3152da9 --- /dev/null +++ b/spec/shared/contexts/jsonapi_context.rb @@ -0,0 +1,95 @@ +RSpec.shared_context 'jsonapi movie class' do + before(:context) do + # models + class JSONAPIMovie + attr_accessor :id, :name, :release_year, :actors, :owner, :movie_type + end + + class JSONAPIActor + attr_accessor :id, :name, :email + end + + class JSONAPIUser + attr_accessor :id, :name + end + + class JSONAPIMovieType + attr_accessor :id, :name + end + + # serializers + class JSONAPIMovieSerializer < JSONAPI::Serializable::Resource + type 'movie' + attributes :name, :release_year + + has_many :actors + has_one :owner + belongs_to :movie_type + end + + class JSONAPIActorSerializer < JSONAPI::Serializable::Resource + type 'actor' + attributes :name, :email + end + + class JSONAPIUserSerializer < JSONAPI::Serializable::Resource + type 'user' + attributes :name + end + + class JSONAPIMovieTypeSerializer < JSONAPI::Serializable::Resource + type 'movie_type' + attributes :name + end + end + + after :context do + classes_to_remove = %i[ + JSONAPIMovie + JSONAPIActor + JSONAPIUser + JSONAPIMovieType + JSONAPIMovieSerializer + JSONAPIActorSerializer + JSONAPIUserSerializer + JSONAPIMovieTypeSerializer] + classes_to_remove.each do |klass_name| + Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name) + end + end + + let(:jsonapi_actors) do + 3.times.map do |i| + j = JSONAPIActor.new + j.id = i + 1 + j.name = "Test #{j.id}" + j.email = "test#{j.id}@test.com" + j + end + end + + let(:jsonapi_user) do + jsonapi_user = JSONAPIUser.new + jsonapi_user.id = 3 + jsonapi_user + end + + let(:jsonapi_movie_type) do + jsonapi_movie_type = JSONAPIMovieType.new + jsonapi_movie_type.id = 1 + jsonapi_movie_type.name = 'episode' + jsonapi_movie_type + end + + def build_jsonapi_movies(count) + count.times.map do |i| + m = JSONAPIMovie.new + m.id = i + 1 + m.name = 'test movie' + m.actors = jsonapi_actors + m.owner = jsonapi_user + m.movie_type = jsonapi_movie_type + m + end + end +end From 22bea1505f135b8e6cd36c7e0a70dd82928bde65 Mon Sep 17 00:00:00 2001 From: "Min.Kim" Date: Mon, 5 Feb 2018 15:12:27 +0000 Subject: [PATCH 17/53] Add jsonapi-rb test to benchmark suite --- .../lib/object_serializer_performance_spec.rb | 104 ++++++++---------- spec/shared/contexts/jsonapi_context.rb | 21 ++++ 2 files changed, 64 insertions(+), 61 deletions(-) diff --git a/spec/lib/object_serializer_performance_spec.rb b/spec/lib/object_serializer_performance_spec.rb index 6b6e874c..78916bed 100644 --- a/spec/lib/object_serializer_performance_spec.rb +++ b/spec/lib/object_serializer_performance_spec.rb @@ -38,56 +38,52 @@ # end # end - def print_stats(message, count, ams_time, our_time) + def print_stats(message, count, ams_time, jsonapi_time, our_time) format = '%-15s %-10s %s' puts '' puts message puts format(format, 'Serializer', 'Records', 'Time') puts format(format, 'AMS serializer', count, ams_time.round(2).to_s + ' ms') + puts format(format, 'jsonapi-rb serializer', count, jsonapi_time.round(2).to_s + ' ms') puts format(format, 'Fast serializer', count, our_time.round(2).to_s + ' ms') end - def run_hash_benchmark(message, movie_count, our_serializer, ams_serializer) + def run_hash_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer) our_time = Benchmark.measure { our_hash = our_serializer.serializable_hash }.real * 1000 ams_time = Benchmark.measure { ams_hash = ams_serializer.as_json }.real * 1000 - print_stats(message, movie_count, ams_time, our_time) + jsonapi_time = Benchmark.measure { ams_hash = jsonapi_serializer.to_hash }.real * 1000 + + print_stats(message, movie_count, ams_time, jsonapi_time, our_time) end - def run_json_benchmark(message, movie_count, our_serializer, ams_serializer) + def run_json_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer) our_json = nil ams_json = nil + jsonapi_json = nil our_time = Benchmark.measure { our_json = our_serializer.serialized_json }.real * 1000 ams_time = Benchmark.measure { ams_json = ams_serializer.to_json }.real * 1000 - print_stats(message, movie_count, ams_time, our_time) - return our_json, ams_json + jsonapi_time = Benchmark.measure { jsonapi_json = jsonapi_serializer.to_json }.real * 1000 + + print_stats(message, movie_count, ams_time, jsonapi_time, our_time) + return our_json, ams_json, jsonapi_json end context 'when comparing with AMS 0.10.x' do - #[1, 25, 250, 1000].each do |movie_count| - [1].each do |movie_count| + [1, 25, 250, 1000].each do |movie_count| speed_factor = 25 it "should serialize #{movie_count} records atleast #{speed_factor} times faster than AMS" do ams_movies = build_ams_movies(movie_count) - jsonapi_movies = build_jsonapi_movies(movie_count) movies = build_movies(movie_count) + jsonapi_movies = build_jsonapi_movies(movie_count) our_serializer = MovieSerializer.new(movies) ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_movies) - - puts JSONAPI::Serializable::Renderer.new.render( - jsonapi_movies, - class: { - JSONAPIMovie: JSONAPIMovieSerializer, - JSONAPIActor: JSONAPIActorSerializer, - JSONAPIUser: JSONAPIUserSerializer, - JSONAPIMovieType: JSONAPIMovieTypeSerializer - }, - ).to_json + jsonapi_serializer = JSONAPISerializer.new(jsonapi_movies) message = "Serialize to JSON string #{movie_count} records" - our_json, ams_json = run_json_benchmark(message, movie_count, our_serializer, ams_serializer) + our_json, ams_json, jsonapi_json = run_json_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer) message = "Serialize to Ruby Hash #{movie_count} records" - run_hash_benchmark(message, movie_count, our_serializer, ams_serializer) + run_hash_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer) expect(our_json.length).to eq ams_json.length expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times @@ -96,44 +92,30 @@ def run_json_benchmark(message, movie_count, our_serializer, ams_serializer) end end -# context 'when comparing with AMS 0.10.x and with includes and meta' do -# #[1, 25, 250, 1000].each do |movie_count| -# [2].each do |movie_count| -# speed_factor = 25 -# it "should serialize #{movie_count} records atleast #{speed_factor} times faster than AMS" do -# ams_movies = build_ams_movies(movie_count) -# movies = build_movies(movie_count) -# options = {} -# options[:meta] = { total: movie_count } -# options[:include] = [:actors, :movie_type] -# our_serializer = MovieSerializer.new(movies, options) -# ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_movies, include: options[:include], meta: options[:meta]) -# -# message = "Serialize to JSON string #{movie_count} with includes and meta" -# our_json, ams_json = run_json_benchmark(message, movie_count, our_serializer, ams_serializer) -# -# puts our_json -# puts ams_json -# -# jsonapi_movies = build_jsonapi_movies(movie_count) -# puts JSONAPI::Serializable::Renderer.new.render( -# jsonapi_movies, -# class: { -# JSONAPIMovie: JSONAPIMovieSerializer, -# JSONAPIActor: JSONAPIActorSerializer, -# JSONAPIUser: JSONAPIUserSerializer, -# JSONAPIMovieType: JSONAPIMovieTypeSerializer -# }, -# include: options[:include] -# ).to_json -# -# message = "Serialize to Ruby Hash #{movie_count} with includes and meta" -# run_hash_benchmark(message, movie_count, our_serializer, ams_serializer) -# -# expect(our_json.length).to eq ams_json.length -# expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times -# expect { our_serializer.serializable_hash }.to perform_faster_than { ams_serializer.as_json }.at_least(speed_factor).times -# end -# end -# end + context 'when comparing with AMS 0.10.x and with includes and meta' do + [1, 25, 250, 1000].each do |movie_count| + speed_factor = 25 + it "should serialize #{movie_count} records atleast #{speed_factor} times faster than AMS" do + ams_movies = build_ams_movies(movie_count) + movies = build_movies(movie_count) + jsonapi_movies = build_jsonapi_movies(movie_count) + options = {} + options[:meta] = { total: movie_count } + options[:include] = [:actors, :movie_type] + our_serializer = MovieSerializer.new(movies, options) + ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_movies, include: options[:include], meta: options[:meta]) + jsonapi_serializer = JSONAPISerializer.new(jsonapi_movies, include: options[:include], meta: options[:meta]) + + message = "Serialize to JSON string #{movie_count} with includes and meta" + our_json, ams_json = run_json_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer) + + message = "Serialize to Ruby Hash #{movie_count} with includes and meta" + run_hash_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer) + + expect(our_json.length).to eq ams_json.length + expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times + expect { our_serializer.serializable_hash }.to perform_faster_than { ams_serializer.as_json }.at_least(speed_factor).times + end + end + end end diff --git a/spec/shared/contexts/jsonapi_context.rb b/spec/shared/contexts/jsonapi_context.rb index d3152da9..a3d2e6d9 100644 --- a/spec/shared/contexts/jsonapi_context.rb +++ b/spec/shared/contexts/jsonapi_context.rb @@ -41,6 +41,27 @@ class JSONAPIMovieTypeSerializer < JSONAPI::Serializable::Resource type 'movie_type' attributes :name end + + class JSONAPISerializer + def initialize(data, options = {}) + @serializer = JSONAPI::Serializable::Renderer.new + @options = options.merge(class: { + JSONAPIMovie: JSONAPIMovieSerializer, + JSONAPIActor: JSONAPIActorSerializer, + JSONAPIUser: JSONAPIUserSerializer, + JSONAPIMovieType: JSONAPIMovieTypeSerializer + }) + @data = data + end + + def to_json + @serializer.render(@data, @options).to_json + end + + def to_hash + @serializer.render(@data, @options) + end + end end after :context do From c7b211b0a78ba6a0ea1a81a339b83d190f8e54f4 Mon Sep 17 00:00:00 2001 From: "Min.Kim" Date: Mon, 5 Feb 2018 15:28:03 +0000 Subject: [PATCH 18/53] Fix fast_jsonapi.gemspec --- Gemfile.lock | 18 ++---------- fast_jsonapi.gemspec | 65 ++++++++------------------------------------ 2 files changed, 14 insertions(+), 69 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5cc42dec..333e0e7f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -49,8 +49,6 @@ GEM erubi (1.7.0) i18n (0.9.1) concurrent-ruby (~> 1.0) - jsonapi-renderer (0.2.0) - json (1.8.6) jsonapi-deserializable (0.2.0) jsonapi-rb (0.5.0) jsonapi-deserializable (~> 0.2.0) @@ -58,17 +56,6 @@ GEM jsonapi-renderer (0.2.0) jsonapi-serializable (0.3.0) jsonapi-renderer (~> 0.2.0) - juwelier (2.1.3) - builder - bundler (>= 1.13) - git (>= 1.2.5) - github_api - highline (>= 1.6.15) - nokogiri (>= 1.5.10) - rake - rdoc - semver - jwt (1.5.6) loofah (2.1.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) @@ -119,12 +106,11 @@ DEPENDENCIES byebug fast_jsonapi! jsonapi-rb (~> 0.5.0) - juwelier (~> 2.1.0) - rdoc (~> 3.12) + oj (~> 3.3) rspec (~> 3.5.0) rspec-benchmark (~> 0.3.0) skylight (~> 1.3) sqlite3 (~> 1.3) BUNDLED WITH - 1.16.0 + 1.16.1 diff --git a/fast_jsonapi.gemspec b/fast_jsonapi.gemspec index 00cff648..ee2ccfc8 100644 --- a/fast_jsonapi.gemspec +++ b/fast_jsonapi.gemspec @@ -20,57 +20,16 @@ Gem::Specification.new do |gem| gem.rubygems_version = "2.5.1" gem.summary = "fast JSON API(jsonapi.org) serializer" - if s.respond_to? :specification_version then - s.specification_version = 4 - - if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then - s.add_runtime_dependency(%q, ["~> 5.0"]) - s.add_runtime_dependency(%q, ["~> 1.12"]) - s.add_runtime_dependency(%q, ["~> 3.3"]) - s.add_runtime_dependency(%q, ["~> 5.0"]) - s.add_development_dependency(%q, ["~> 1.3"]) - s.add_development_dependency(%q, ["~> 3.5.0"]) - s.add_development_dependency(%q, ["~> 0.3.0"]) - s.add_development_dependency(%q, ["~> 3.12"]) - s.add_development_dependency(%q, ["~> 1.0"]) - s.add_development_dependency(%q, ["~> 2.1.0"]) - s.add_development_dependency(%q, [">= 0"]) - s.add_development_dependency(%q, [">= 0"]) - s.add_development_dependency(%q, ["~> 0.10.4"]) - s.add_development_dependency(%q, ["~> 1.3"]) - s.add_development_dependency(%q, ["~> 0.5.0"]) - else - s.add_dependency(%q, ["~> 5.0"]) - s.add_dependency(%q, ["~> 1.12"]) - s.add_dependency(%q, ["~> 3.3"]) - s.add_dependency(%q, ["~> 1.3"]) - s.add_dependency(%q, ["~> 5.0"]) - s.add_dependency(%q, ["~> 3.5.0"]) - s.add_dependency(%q, ["~> 0.3.0"]) - s.add_dependency(%q, ["~> 3.12"]) - s.add_dependency(%q, ["~> 1.0"]) - s.add_dependency(%q, ["~> 2.1.0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, ["~> 0.10.4"]) - s.add_dependency(%q, ["~> 1.3"]) - s.add_dependency(%q, ["~> 0.5.0"]) - end - else - s.add_dependency(%q, ["~> 5.0"]) - s.add_dependency(%q, ["~> 1.12"]) - s.add_dependency(%q, ["~> 3.3"]) - s.add_dependency(%q, ["~> 1.3"]) - s.add_dependency(%q, ["~> 5.0"]) - s.add_dependency(%q, ["~> 3.5.0"]) - s.add_dependency(%q, ["~> 0.3.0"]) - s.add_dependency(%q, ["~> 3.12"]) - s.add_dependency(%q, ["~> 1.0"]) - s.add_dependency(%q, ["~> 2.1.0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, ["~> 0.10.4"]) - s.add_dependency(%q, ["~> 1.3"]) - s.add_dependency(%q, ["~> 0.5.0"]) - end + gem.add_runtime_dependency(%q, ["~> 5.0"]) + gem.add_runtime_dependency(%q, ["~> 1.12"]) + gem.add_development_dependency(%q, ["~> 5.0"]) + gem.add_development_dependency(%q, ["~> 1.3"]) + gem.add_development_dependency(%q, ["~> 3.5.0"]) + gem.add_development_dependency(%q, ["~> 3.3"]) + gem.add_development_dependency(%q, ["~> 0.3.0"]) + gem.add_development_dependency(%q, ["~> 1.0"]) + gem.add_development_dependency(%q, [">= 0"]) + gem.add_development_dependency(%q, ["~> 0.10.4"]) + gem.add_development_dependency(%q, ["~> 1.3"]) + gem.add_development_dependency(%q, ["~> 0.5.0"]) end From 7b029991b8dcec58fa9fd818b86a72d7714a12ca Mon Sep 17 00:00:00 2001 From: "Min.Kim" Date: Mon, 5 Feb 2018 15:29:46 +0000 Subject: [PATCH 19/53] Put all tests back --- .../lib/object_serializer_performance_spec.rb | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/spec/lib/object_serializer_performance_spec.rb b/spec/lib/object_serializer_performance_spec.rb index 78916bed..19118ca2 100644 --- a/spec/lib/object_serializer_performance_spec.rb +++ b/spec/lib/object_serializer_performance_spec.rb @@ -8,35 +8,35 @@ before(:all) { GC.disable } after(:all) { GC.enable } -# context 'when testing performance of serialization' do -# it 'should create a hash of 1000 records in less than 50 ms' do -# movies = 1000.times.map { |_i| movie } -# expect { MovieSerializer.new(movies).serializable_hash }.to perform_under(50).ms -# end -# -# it 'should serialize 1000 records to jsonapi in less than 60 ms' do -# movies = 1000.times.map { |_i| movie } -# expect { MovieSerializer.new(movies).serialized_json }.to perform_under(60).ms -# end -# -# it 'should create a hash of 1000 records with includes and meta in less than 75 ms' do -# count = 1000 -# movies = count.times.map { |_i| movie } -# options = {} -# options[:meta] = { total: count } -# options[:include] = [:actors] -# expect { MovieSerializer.new(movies, options).serializable_hash }.to perform_under(75).ms -# end -# -# it 'should serialize 1000 records to jsonapi with includes and meta in less than 75 ms' do -# count = 1000 -# movies = count.times.map { |_i| movie } -# options = {} -# options[:meta] = { total: count } -# options[:include] = [:actors] -# expect { MovieSerializer.new(movies, options).serialized_json }.to perform_under(75).ms -# end -# end + context 'when testing performance of serialization' do + it 'should create a hash of 1000 records in less than 50 ms' do + movies = 1000.times.map { |_i| movie } + expect { MovieSerializer.new(movies).serializable_hash }.to perform_under(50).ms + end + + it 'should serialize 1000 records to jsonapi in less than 60 ms' do + movies = 1000.times.map { |_i| movie } + expect { MovieSerializer.new(movies).serialized_json }.to perform_under(60).ms + end + + it 'should create a hash of 1000 records with includes and meta in less than 75 ms' do + count = 1000 + movies = count.times.map { |_i| movie } + options = {} + options[:meta] = { total: count } + options[:include] = [:actors] + expect { MovieSerializer.new(movies, options).serializable_hash }.to perform_under(75).ms + end + + it 'should serialize 1000 records to jsonapi with includes and meta in less than 75 ms' do + count = 1000 + movies = count.times.map { |_i| movie } + options = {} + options[:meta] = { total: count } + options[:include] = [:actors] + expect { MovieSerializer.new(movies, options).serialized_json }.to perform_under(75).ms + end + end def print_stats(message, count, ams_time, jsonapi_time, our_time) format = '%-15s %-10s %s' From 7843e411f663433d3541308e1a72b1acfce555f4 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Mon, 5 Feb 2018 16:36:51 -0600 Subject: [PATCH 20/53] Better usage of AMS::Model --- spec/shared/contexts/ams_context.rb | 35 +++++++++++++++-------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/spec/shared/contexts/ams_context.rb b/spec/shared/contexts/ams_context.rb index 14d87bd8..46832230 100644 --- a/spec/shared/contexts/ams_context.rb +++ b/spec/shared/contexts/ams_context.rb @@ -1,34 +1,28 @@ RSpec.shared_context 'ams movie class' do before(:context) do # models - class AMSMovie < ActiveModelSerializers::Model - attr_accessor :id, :name, :release_year, :actors, :owner, :movie_type + class AMSModel < ActiveModelSerializers::Model + derive_attributes_from_names_and_fix_accessors + end + class AMSMovie < AMSModel + attributes :id, :name, :release_year, :actors, :owner, :movie_type end - class AMSActor < ActiveModelSerializers::Model - attr_accessor :id, :name, :email + class AMSActor < AMSModel + attributes :id, :name, :email end - class AMSUser < ActiveModelSerializers::Model - attr_accessor :id, :name + class AMSUser < AMSModel + attributes :id, :name end - class AMSMovieType < ActiveModelSerializers::Model - attr_accessor :id, :name + class AMSMovieType < AMSModel + attributes :id, :name end # serializers - class AMSMovieSerializer < ActiveModel::Serializer - type 'movie' - attributes :name, :release_year - has_many :actors - has_one :owner - belongs_to :movie_type - end - class AMSActorSerializer < ActiveModel::Serializer type 'actor' attributes :name, :email end - class AMSUserSerializer < ActiveModel::Serializer type 'user' attributes :name @@ -37,6 +31,13 @@ class AMSMovieTypeSerializer < ActiveModel::Serializer type 'movie_type' attributes :name end + class AMSMovieSerializer < ActiveModel::Serializer + type 'movie' + attributes :name, :release_year + has_many :actors + has_one :owner + belongs_to :movie_type + end end after(:context) do From d7df16ba02df39330fca6824c9f71255b52f02f3 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Mon, 5 Feb 2018 16:39:07 -0600 Subject: [PATCH 21/53] Update AMS --- Gemfile.lock | 2 +- fast_jsonapi.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 333e0e7f..01455c81 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,7 +100,7 @@ PLATFORMS ruby DEPENDENCIES - active_model_serializers (~> 0.10.4) + active_model_serializers (~> 0.10.7) activerecord (~> 5.0) bundler (~> 1.0) byebug diff --git a/fast_jsonapi.gemspec b/fast_jsonapi.gemspec index ee2ccfc8..209712cd 100644 --- a/fast_jsonapi.gemspec +++ b/fast_jsonapi.gemspec @@ -29,7 +29,7 @@ Gem::Specification.new do |gem| gem.add_development_dependency(%q, ["~> 0.3.0"]) gem.add_development_dependency(%q, ["~> 1.0"]) gem.add_development_dependency(%q, [">= 0"]) - gem.add_development_dependency(%q, ["~> 0.10.4"]) + gem.add_development_dependency(%q, ["~> 0.10.7"]) gem.add_development_dependency(%q, ["~> 1.3"]) gem.add_development_dependency(%q, ["~> 0.5.0"]) end From fcca5063ca3d541a53fa384cf36cb3131a91c005 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Tue, 6 Feb 2018 00:23:56 -0600 Subject: [PATCH 22/53] Correct rubygems url --- fast_jsonapi.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fast_jsonapi.gemspec b/fast_jsonapi.gemspec index 209712cd..dbb32f05 100644 --- a/fast_jsonapi.gemspec +++ b/fast_jsonapi.gemspec @@ -3,7 +3,7 @@ Gem::Specification.new do |gem| gem.version = "1.0.17" gem.required_rubygems_version = Gem::Requirement.new(">= 0") if gem.respond_to? :required_rubygems_version= - gem.metadata = { "allowed_push_host" => "https://rubygemgem.org" } if gem.respond_to? :metadata= + gem.metadata = { "allowed_push_host" => "https://rubygems.org" } if gem.respond_to? :metadata= gem.require_paths = ["lib"] gem.authors = ["Shishir Kakaraddi", "Srinivas Raghunathan", "Adam Gross"] gem.date = "2018-02-01" From 5f7bc4fe62f5ccbbc65d0d42e5f2c32751874051 Mon Sep 17 00:00:00 2001 From: Andrey Lompart Date: Tue, 6 Feb 2018 16:43:30 +0800 Subject: [PATCH 23/53] changed activesupport and activerecord dep to '>= 4.2' --- Gemfile.lock | 4 ++-- fast_jsonapi.gemspec | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 01455c81..ae484210 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,7 +2,7 @@ PATH remote: . specs: fast_jsonapi (1.0.17) - activesupport (~> 5.0) + activesupport (>= 4.2) multi_json (~> 1.12) GEM @@ -101,7 +101,7 @@ PLATFORMS DEPENDENCIES active_model_serializers (~> 0.10.7) - activerecord (~> 5.0) + activerecord (>= 4.2) bundler (~> 1.0) byebug fast_jsonapi! diff --git a/fast_jsonapi.gemspec b/fast_jsonapi.gemspec index dbb32f05..1600f557 100644 --- a/fast_jsonapi.gemspec +++ b/fast_jsonapi.gemspec @@ -20,9 +20,9 @@ Gem::Specification.new do |gem| gem.rubygems_version = "2.5.1" gem.summary = "fast JSON API(jsonapi.org) serializer" - gem.add_runtime_dependency(%q, ["~> 5.0"]) + gem.add_runtime_dependency(%q, [">= 4.2"]) gem.add_runtime_dependency(%q, ["~> 1.12"]) - gem.add_development_dependency(%q, ["~> 5.0"]) + gem.add_development_dependency(%q, [">= 4.2"]) gem.add_development_dependency(%q, ["~> 1.3"]) gem.add_development_dependency(%q, ["~> 3.5.0"]) gem.add_development_dependency(%q, ["~> 3.3"]) From e7caa7afd5ca4fa590d96ae26705ae5be2c85f29 Mon Sep 17 00:00:00 2001 From: jeremyjung Date: Wed, 7 Feb 2018 08:54:53 -0800 Subject: [PATCH 24/53] Allow has_one nil association (#48) * Allow has_one nil association * add test for when has_one returns nil * modify has_one extension method, add active record test * Use try operator to support old rubies --- lib/extensions/has_one.rb | 2 +- spec/lib/extensions/active_record_spec.rb | 8 +++++- spec/lib/object_serializer_spec.rb | 7 +++++ spec/shared/contexts/movie_context.rb | 35 +++++++++++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/lib/extensions/has_one.rb b/lib/extensions/has_one.rb index deaa73a5..4e1ef252 100644 --- a/lib/extensions/has_one.rb +++ b/lib/extensions/has_one.rb @@ -9,7 +9,7 @@ def self.define_accessors(mixin, reflection) name = reflection.name mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name.to_s}_id - association(:#{name}).reader.id + association(:#{name}).reader.try(:id) end CODE end diff --git a/spec/lib/extensions/active_record_spec.rb b/spec/lib/extensions/active_record_spec.rb index d407943a..e31ffe78 100644 --- a/spec/lib/extensions/active_record_spec.rb +++ b/spec/lib/extensions/active_record_spec.rb @@ -29,8 +29,10 @@ # Insert records @account_id = 2 @supplier_id = 1 + @supplier_id_without_account = 3 db.execute_batch <<-SQL - insert into suppliers values ('Supplier1', #{@supplier_id}); + insert into suppliers values ('Supplier1', #{@supplier_id}), + ('SupplierWithoutAccount', #{@supplier_id_without_account}); insert into accounts values ('Dollar Account', #{@account_id}, #{@supplier_id}); SQL end @@ -58,6 +60,10 @@ class Account < ActiveRecord::Base expect(Supplier.first.account_id).to eq @account_id end + it 'has account_id method return nil if account not present' do + expect(Supplier.find(@supplier_id_without_account).account_id).to eq nil + end + end # Clean up DB diff --git a/spec/lib/object_serializer_spec.rb b/spec/lib/object_serializer_spec.rb index 6c29f86e..dd7fd920 100644 --- a/spec/lib/object_serializer_spec.rb +++ b/spec/lib/object_serializer_spec.rb @@ -69,6 +69,13 @@ expect(serializable_hash['data']['relationships']['owner']['data']).to be nil end + it 'returns correct json when has_one returns nil' do + supplier.account_id = nil + json = SupplierSerializer.new(supplier).serialized_json + serializable_hash = JSON.parse(json) + expect(serializable_hash['data']['relationships']['account']['data']).to be nil + end + it 'returns correct json when serializing []' do json = MovieSerializer.new([]).serialized_json serializable_hash = JSON.parse(json) diff --git a/spec/shared/contexts/movie_context.rb b/spec/shared/contexts/movie_context.rb index 6ffe9a81..dace0361 100644 --- a/spec/shared/contexts/movie_context.rb +++ b/spec/shared/contexts/movie_context.rb @@ -36,6 +36,22 @@ class MovieType attr_accessor :id, :name end + class Supplier + attr_accessor :id, :account_id + + def account + if account_id + a = Account.new + a.id = account_id + a + end + end + end + + class Account + attr_accessor :id + end + # serializers class MovieSerializer include FastJsonapi::ObjectSerializer @@ -79,6 +95,18 @@ class MovieTypeSerializer set_type :movie_type attributes :name end + + class SupplierSerializer + include FastJsonapi::ObjectSerializer + set_type :supplier + has_one :account + end + + class AccountSerializer + include FastJsonapi::ObjectSerializer + set_type :account + belongs_to :supplier + end end @@ -151,6 +179,13 @@ class MovieSerializer m end + let(:supplier) do + s = Supplier.new + s.id = 1 + s.account_id = 1 + s + end + def build_movies(count) count.times.map do |i| m = Movie.new From b717ffeccf893827afb7abd2430669626f06e3ea Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Tue, 6 Feb 2018 01:26:52 -0600 Subject: [PATCH 25/53] Let people choose their JSON serializer; JSON:API is not circular --- Gemfile.lock | 2 -- fast_jsonapi.gemspec | 1 - lib/fast_jsonapi/object_serializer.rb | 1 - lib/fast_jsonapi/serialization_core.rb | 3 ++- spec/spec_helper.rb | 1 - 5 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index ae484210..c0899cdf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,7 +3,6 @@ PATH specs: fast_jsonapi (1.0.17) activesupport (>= 4.2) - multi_json (~> 1.12) GEM remote: https://rubygems.org/ @@ -61,7 +60,6 @@ GEM nokogiri (>= 1.5.9) mini_portile2 (2.3.0) minitest (5.10.3) - multi_json (1.13.1) nokogiri (1.8.1) mini_portile2 (~> 2.3.0) oj (3.4.0) diff --git a/fast_jsonapi.gemspec b/fast_jsonapi.gemspec index 1600f557..ccae628c 100644 --- a/fast_jsonapi.gemspec +++ b/fast_jsonapi.gemspec @@ -21,7 +21,6 @@ Gem::Specification.new do |gem| gem.summary = "fast JSON API(jsonapi.org) serializer" gem.add_runtime_dependency(%q, [">= 4.2"]) - gem.add_runtime_dependency(%q, ["~> 1.12"]) gem.add_development_dependency(%q, [">= 4.2"]) gem.add_development_dependency(%q, ["~> 1.3"]) gem.add_development_dependency(%q, ["~> 3.5.0"]) diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index da858f5d..339ca028 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -1,7 +1,6 @@ require 'active_support/core_ext/object' require 'active_support/concern' require 'active_support/inflector' -require 'multi_json' require 'fast_jsonapi/serialization_core' begin diff --git a/lib/fast_jsonapi/serialization_core.rb b/lib/fast_jsonapi/serialization_core.rb index 7e79b14c..d8d7d6d4 100644 --- a/lib/fast_jsonapi/serialization_core.rb +++ b/lib/fast_jsonapi/serialization_core.rb @@ -65,8 +65,9 @@ def record_hash(record) end end + # Override #to_json for alternative implementation def to_json(payload) - MultiJson.dump(payload) if payload.present? + JSON.fast_generate(payload) if payload.present? end # includes handler diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a82b893e..77f9226c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,5 @@ require 'fast_jsonapi' require 'rspec-benchmark' -require 'multi_json' require 'byebug' require 'active_model_serializers' require 'oj' From d4b6216ff3348a35184a2da55bb15b46b32d2b09 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Tue, 6 Feb 2018 01:26:52 -0600 Subject: [PATCH 26/53] Make our own fast MultiToJson --- lib/fast_jsonapi/multi_to_json.rb | 90 ++++++++++++++++++++++++++ lib/fast_jsonapi/serialization_core.rb | 3 +- 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 lib/fast_jsonapi/multi_to_json.rb diff --git a/lib/fast_jsonapi/multi_to_json.rb b/lib/fast_jsonapi/multi_to_json.rb new file mode 100644 index 00000000..b43f1a0a --- /dev/null +++ b/lib/fast_jsonapi/multi_to_json.rb @@ -0,0 +1,90 @@ +# Usage: +# class Movie +# def to_json(payload) +# FastJsonapi::MultiToJson.to_json(payload) +# end +# end +module FastJsonapi + module MultiToJson + # Result object pattern is from https://johnnunemaker.com/resilience-in-ruby/ + # e.g. https://github.com/github/github-ds/blob/fbda5389711edfb4c10b6c6bad19311dfcb1bac1/lib/github/result.rb + class Result + def initialize(*rescued_exceptions) + rescued_exceptions = [StandardError] if rescued_exceptions.empty? + @value = yield + @error = nil + rescue *rescued_exceptions => e + @error = e + end + + def ok? + @error.nil? + end + + def value! + if ok? + @value + else + raise @error + end + end + + def rescue + return self if ok? + Result.new { yield(@error) } + end + end + + def self.logger(device=nil) + return @logger = Logger.new(device) if device + @logger ||= Logger.new(IO::NULL) + end + + # Encoder-compatible with default MultiJSON adapters and defaults + def self.to_json_method + encode_method = String.new(%(def _fast_to_json(object)\n )) + encode_method << Result.new(LoadError) { + require 'oj' + %(::Oj.dump(object, mode: :compat, time_format: :ruby, use_to_json: true)) + }.rescue { + require 'yajl' + %(::Yajl::Encoder.encode(object)) + }.rescue { + require 'jrjackson' unless defined?(::JrJackson) + %(::JrJackson::Json.dump(object)) + }.rescue { + require 'json' + %(JSON.fast_generate(object, create_additions: false, quirks_mode: true)) + }.rescue { + require 'gson' + %(::Gson::Encoder.new({}).encode(object)) + }.rescue { + require 'active_support/json/encoding' + %(::ActiveSupport::JSON.encode(object)) + }.rescue { + warn "No JSON encoder found. Falling back to `object.to_json`" + %(object.to_json) + }.value! + encode_method << "\nend" + end + + def self.to_json(object) + _fast_to_json(object) + rescue NameError + define_to_json(FastJsonapi::MultiToJson) + _fast_to_json(object) + end + + def self.define_to_json(receiver) + cl = caller_locations[0] + method_body = to_json_method + logger.debug { "Defining #{receiver}._fast_to_json as #{method_body.inspect}" } + receiver.instance_eval method_body, cl.absolute_path, cl.lineno + end + + def self.reset_to_json! + undef :_fast_to_json if method_defined?(:_fast_to_json) + logger.debug { "Undefining #{receiver}._fast_to_json" } + end + end +end diff --git a/lib/fast_jsonapi/serialization_core.rb b/lib/fast_jsonapi/serialization_core.rb index d8d7d6d4..7daea60a 100644 --- a/lib/fast_jsonapi/serialization_core.rb +++ b/lib/fast_jsonapi/serialization_core.rb @@ -1,4 +1,5 @@ require 'active_support/concern' +require 'fast_jsonapi/multi_to_json' module FastJsonapi module SerializationCore @@ -67,7 +68,7 @@ def record_hash(record) # Override #to_json for alternative implementation def to_json(payload) - JSON.fast_generate(payload) if payload.present? + FastJsonapi::MultiToJson.to_json(payload) if payload.present? end # includes handler From 6d516c217c9ac71069eda38cf34902ea3569fc50 Mon Sep 17 00:00:00 2001 From: Roberto Quintanilla Date: Fri, 9 Feb 2018 00:18:46 -0600 Subject: [PATCH 27/53] Support for polymorphic associations (#64) * add hash benchmarking to performance tests * Add missing attribute in README example * Disable GC before doing performance test * Enable oj to AM for fair benchmark test * Support for polymorphic associations * Optional dictionary for polymorphic associations * Added polymorphic record types memoization * Updated performance tests for polymorphic examples to include jsonapi-rb --- lib/fast_jsonapi/object_serializer.rb | 16 ++- lib/fast_jsonapi/serialization_core.rb | 26 +++- .../lib/object_serializer_performance_spec.rb | 28 ++++ spec/lib/serialization_core_spec.rb | 8 +- spec/shared/contexts/ams_group_context.rb | 87 ++++++++++++ spec/shared/contexts/group_context.rb | 131 ++++++++++++++++++ spec/shared/contexts/jsonapi_group_context.rb | 112 +++++++++++++++ 7 files changed, 403 insertions(+), 5 deletions(-) create mode 100644 spec/shared/contexts/ams_group_context.rb create mode 100644 spec/shared/contexts/group_context.rb create mode 100644 spec/shared/contexts/jsonapi_group_context.rb diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index 339ca028..a09dc03f 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -177,7 +177,8 @@ def has_many(relationship_name, options = {}) object_method_name: options[:object_method_name] || name, serializer: compute_serializer_name(serializer_key), relationship_type: :has_many, - cached: options[:cached] || false + cached: options[:cached] || false, + polymorphic: fetch_polymorphic_option(options) } add_relationship(name, relationship) end @@ -195,7 +196,8 @@ def belongs_to(relationship_name, options = {}) object_method_name: options[:object_method_name] || name, serializer: compute_serializer_name(serializer_key), relationship_type: :belongs_to, - cached: options[:cached] || true + cached: options[:cached] || true, + polymorphic: fetch_polymorphic_option(options) }) end @@ -212,7 +214,8 @@ def has_one(relationship_name, options = {}) object_method_name: options[:object_method_name] || name, serializer: compute_serializer_name(serializer_key), relationship_type: :has_one, - cached: options[:cached] || false + cached: options[:cached] || false, + polymorphic: fetch_polymorphic_option(options) }) end @@ -222,6 +225,13 @@ def compute_serializer_name(serializer_key) return (namespace + serializer_name).to_sym if namespace.present? (serializer_key.to_s.classify + 'Serializer').to_sym end + + def fetch_polymorphic_option(options) + option = options[:polymorphic] + return false unless option.present? + return option if option.respond_to? :keys + {} + end end end end diff --git a/lib/fast_jsonapi/serialization_core.rb b/lib/fast_jsonapi/serialization_core.rb index 7daea60a..95af4fe1 100644 --- a/lib/fast_jsonapi/serialization_core.rb +++ b/lib/fast_jsonapi/serialization_core.rb @@ -27,6 +27,30 @@ def ids_hash(ids, record_type) 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: record.id.to_s, type: record_type } + end + + def ids_hash_from_record_and_relationship(record, relationship) + polymorphic = relationship[:polymorphic] + + return ids_hash( + record.public_send(relationship[:id_method_name]), + relationship[:record_type] + ) unless polymorphic + + object_method_name = relationship.fetch(:object_method_name, relationship[:name]) + return unless associated_object = record.send(object_method_name) + + 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 attributes_hash(record) attributes_to_serialize.each_with_object({}) do |(key, method_name), attr_hash| attr_hash[key] = record.public_send(method_name) @@ -42,7 +66,7 @@ def relationships_hash(record, relationships = nil) record_type = relationship[:record_type] empty_case = relationship[:relationship_type] == :has_many ? [] : nil hash[name] = { - data: ids_hash(record.public_send(id_method_name), record_type) || empty_case + data: ids_hash_from_record_and_relationship(record, relationship) || empty_case } end end diff --git a/spec/lib/object_serializer_performance_spec.rb b/spec/lib/object_serializer_performance_spec.rb index 19118ca2..03e1ddd8 100644 --- a/spec/lib/object_serializer_performance_spec.rb +++ b/spec/lib/object_serializer_performance_spec.rb @@ -5,6 +5,10 @@ include_context 'ams movie class' include_context 'jsonapi movie class' + include_context 'group class' + include_context 'ams group class' + include_context 'jsonapi group class' + before(:all) { GC.disable } after(:all) { GC.enable } @@ -118,4 +122,28 @@ def run_json_benchmark(message, movie_count, our_serializer, ams_serializer, jso end end end + + context 'when comparing with AMS 0.10.x and with polymorphic has_many' do + [1, 25, 250, 1000].each do |group_count| + speed_factor = 25 + it "should serialize #{group_count} records at least #{speed_factor} times faster than AMS" do + ams_groups = build_ams_groups(group_count) + groups = build_groups(group_count) + options = {} + our_serializer = GroupSerializer.new(groups, options) + ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_groups) + jsonapi_serializer = JSONAPISerializerB.new(jsonapi_groups) + + message = "Serialize to JSON string #{group_count} with polymorphic has_many" + our_json, ams_json, jsonapi_json = run_json_benchmark(message, group_count, our_serializer, ams_serializer, jsonapi_serializer) + + message = "Serialize to Ruby Hash #{group_count} with polymorphic has_many" + run_hash_benchmark(message, group_count, our_serializer, ams_serializer, jsonapi_serializer) + + expect(our_json.length).to eq ams_json.length + expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times + expect { our_serializer.serializable_hash }.to perform_faster_than { ams_serializer.as_json }.at_least(speed_factor).times + end + end + end end diff --git a/spec/lib/serialization_core_spec.rb b/spec/lib/serialization_core_spec.rb index 2d2bc724..2d327d42 100644 --- a/spec/lib/serialization_core_spec.rb +++ b/spec/lib/serialization_core_spec.rb @@ -2,6 +2,7 @@ describe FastJsonapi::ObjectSerializer do include_context "movie class" + include_context 'group class' context 'when testing class methods of serialization core' do it 'returns correct hash when id_hash is called' do @@ -16,6 +17,12 @@ 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, 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| @@ -80,5 +87,4 @@ expect(included_records.size).to eq 3 end end - end diff --git a/spec/shared/contexts/ams_group_context.rb b/spec/shared/contexts/ams_group_context.rb new file mode 100644 index 00000000..5a36d66e --- /dev/null +++ b/spec/shared/contexts/ams_group_context.rb @@ -0,0 +1,87 @@ +RSpec.shared_context 'ams group class' do + before(:context) do + # models + class AMSPerson < ActiveModelSerializers::Model + attr_accessor :id, :first_name, :last_name + end + + class AMSGroup < ActiveModelSerializers::Model + attr_accessor :id, :name, :groupees + end + + # serializers + class AMSPersonSerializer < ActiveModel::Serializer + type 'person' + attributes :first_name, :last_name + end + + class AMSGroupSerializer < ActiveModel::Serializer + type 'group' + attributes :name + has_many :groupees + end + end + + after(:context) do + classes_to_remove = %i[AMSPerson AMSGroup AMSPersonSerializer AMSGroupSerializer] + classes_to_remove.each do |klass_name| + Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name) + end + end + + let(:ams_groups) do + group_count = 0 + person_count = 0 + 3.times.map do |i| + group = AMSGroup.new + group.id = group_count + 1 + group.name = "Test Group #{group.id}" + group_count = group.id + + person = AMSPerson.new + person.id = person_count + 1 + person.last_name = "Last Name #{person.id}" + person.first_name = "First Name #{person.id}" + person_count = person.id + + child_group = AMSGroup.new + child_group.id = group_count + 1 + child_group.name = "Test Group #{child_group.id}" + group_count = child_group.id + + group.groupees = [person, child_group] + group + end + end + + let(:ams_person) do + ams_person = AMSPerson.new + ams_person.id = 3 + ams_person + end + + def build_ams_groups(count) + group_count = 0 + person_count = 0 + count.times.map do |i| + group = AMSGroup.new + group.id = group_count + 1 + group.name = "Test Group #{group.id}" + group_count = group.id + + person = AMSPerson.new + person.id = person_count + 1 + person.last_name = "Last Name #{person.id}" + person.first_name = "First Name #{person.id}" + person_count = person.id + + child_group = AMSGroup.new + child_group.id = group_count + 1 + child_group.name = "Test Group #{child_group.id}" + group_count = child_group.id + + group.groupees = [person, child_group] + group + end + end +end diff --git a/spec/shared/contexts/group_context.rb b/spec/shared/contexts/group_context.rb new file mode 100644 index 00000000..22f2919e --- /dev/null +++ b/spec/shared/contexts/group_context.rb @@ -0,0 +1,131 @@ +RSpec.shared_context 'group class' do + + # Person, Group Classes and serializers + before(:context) do + # models + class Person + attr_accessor :id, :first_name, :last_name + end + + class Group + attr_accessor :id, :name, :groupees # Let's assume groupees can be Person or Group objects + end + + # serializers + class PersonSerializer + include FastJsonapi::ObjectSerializer + set_type :person + attributes :first_name, :last_name + end + + class GroupSerializer + include FastJsonapi::ObjectSerializer + set_type :group + attributes :name + has_many :groupees, polymorphic: true + end + end + + + # Namespaced PersonSerializer + before(:context) do + # namespaced model stub + module AppName + module V1 + class PersonSerializer + include FastJsonapi::ObjectSerializer + # to test if compute_serializer_name works + end + end + end + end + + # Movie and Actor struct + before(:context) do + PersonStruct = Struct.new( + :id, :first_name, :last_name + ) + + GroupStruct = Struct.new( + :id, :name, :groupees, :groupee_ids + ) + end + + after(:context) do + classes_to_remove = %i[ + Person + PersonSerializer + Group + GroupSerializer + AppName::V1::PersonSerializer + PersonStruct + GroupStruct + ] + classes_to_remove.each do |klass_name| + Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name) + end + end + + let(:group_struct) do + group = GroupStruct.new + group[:id] = 1 + group[:name] = 'Group 1' + group[:groupees] = [] + + person = PersonStruct.new + person[:id] = 1 + person[:last_name] = "Last Name 1" + person[:first_name] = "First Name 1" + + child_group = GroupStruct.new + child_group[:id] = 2 + child_group[:name] = 'Group 2' + + group.groupees = [person, child_group] + group + end + + let(:group) do + group = Group.new + group.id = 1 + group.name = 'Group 1' + + person = Person.new + person.id = 1 + person.last_name = "Last Name 1" + person.first_name = "First Name 1" + + child_group = Group.new + child_group.id = 2 + child_group.name = 'Group 2' + + group.groupees = [person, child_group] + group + end + + def build_groups(count) + group_count = 0 + person_count = 0 + + count.times.map do |i| + group = Group.new + group.id = group_count + 1 + group.name = "Test Group #{group.id}" + group_count = group.id + + person = Person.new + person.id = person_count + 1 + person.last_name = "Last Name #{person.id}" + person.first_name = "First Name #{person.id}" + person_count = person.id + + child_group = Group.new + child_group.id = group_count + 1 + child_group.name = "Test Group #{child_group.id}" + group_count = child_group.id + + group.groupees = [person, child_group] + group + end + end +end diff --git a/spec/shared/contexts/jsonapi_group_context.rb b/spec/shared/contexts/jsonapi_group_context.rb new file mode 100644 index 00000000..373faf1b --- /dev/null +++ b/spec/shared/contexts/jsonapi_group_context.rb @@ -0,0 +1,112 @@ +RSpec.shared_context 'jsonapi group class' do + + # Person, Group Classes and serializers + before(:context) do + # models + class JSONAPIPerson + attr_accessor :id, :first_name, :last_name + end + + class JSONAPIGroup + attr_accessor :id, :name, :groupees # Let's assume groupees can be Person or Group objects + end + + # serializers + class JSONAPIPersonSerializer < JSONAPI::Serializable::Resource + type 'person' + attributes :first_name, :last_name + end + + class JSONAPIGroupSerializer < JSONAPI::Serializable::Resource + type 'group' + attributes :name + has_many :groupees + end + + class JSONAPISerializerB + def initialize(data, options = {}) + @serializer = JSONAPI::Serializable::Renderer.new + @options = options.merge(class: { + JSONAPIPerson: JSONAPIPersonSerializer, + JSONAPIGroup: JSONAPIGroupSerializer + }) + @data = data + end + + def to_json + @serializer.render(@data, @options).to_json + end + + def to_hash + @serializer.render(@data, @options) + end + end + end + + after :context do + classes_to_remove = %i[ + JSONAPIPerson + JSONAPIGroup + JSONAPIPersonSerializer + JSONAPIGroupSerializer] + classes_to_remove.each do |klass_name| + Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name) + end + end + + let(:jsonapi_groups) do + group_count = 0 + person_count = 0 + 3.times.map do |i| + group = JSONAPIGroup.new + group.id = group_count + 1 + group.name = "Test Group #{group.id}" + group_count = group.id + + person = JSONAPIPerson.new + person.id = person_count + 1 + person.last_name = "Last Name #{person.id}" + person.first_name = "First Name #{person.id}" + person_count = person.id + + child_group = JSONAPIGroup.new + child_group.id = group_count + 1 + child_group.name = "Test Group #{child_group.id}" + group_count = child_group.id + + group.groupees = [person, child_group] + group + end + end + + let(:jsonapi_person) do + person = JSONAPIPerson.new + person.id = 3 + person + end + + def build_jsonapi_groups(count) + group_count = 0 + person_count = 0 + count.times.map do |i| + group = JSONAPIGroup.new + group.id = group_count + 1 + group.name = "Test Group #{group.id}" + group_count = group.id + + person = JSONAPIPerson.new + person.id = person_count + 1 + person.last_name = "Last Name #{person.id}" + person.first_name = "First Name #{person.id}" + person_count = person.id + + child_group = JSONAPIGroup.new + child_group.id = group_count + 1 + child_group.name = "Test Group #{child_group.id}" + group_count = child_group.id + + group.groupees = [person, child_group] + group + end + end +end From b30a53bc5fcf9e71e8fb16773511133de711df63 Mon Sep 17 00:00:00 2001 From: Christopher Sansone Date: Fri, 9 Feb 2018 11:59:07 -0500 Subject: [PATCH 28/53] ability to customize rendering of attributes via a block (#54) * add hash benchmarking to performance tests * Add missing attribute in README example * Disable GC before doing performance test * Enable oj to AM for fair benchmark test * ability to customize rendering of attributes via a block * fixed attribute render spec * minimized specs to specifially test this feature * Update README to include attribute definitions * Fixed syntax error * Fixed merge issues --- README.md | 39 +++++++++++++++++++ lib/fast_jsonapi/object_serializer.rb | 6 ++- lib/fast_jsonapi/serialization_core.rb | 4 +- ...ct_serializer_with_attribute_block_spec.rb | 13 +++++++ spec/shared/contexts/movie_context.rb | 10 +++++ 5 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 spec/lib/object_serializer_with_attribute_block_spec.rb diff --git a/README.md b/README.md index 7b23755e..f054650c 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,45 @@ set_key_transform :dash # "some_key" => "some-key" set_key_transform :underscore # "some_key" => "some_key" ``` +### Attributes +Attributes are defined in FastJsonapi using the `attributes` method. This method is also aliased as `attribute`, which is useful when defining a single attribute. + +By default, attributes are read directly from the model property of the same name. In this example, `name` is expected to be a property of the object being serialized: + +```ruby +class MovieSerializer + include FastJsonapi::ObjectSerializer + + attribute :name +end +``` + +Custom attributes that must be serialized but do not exist on the model can be declared using Ruby block syntax: + +```ruby +class MovieSerializer + include FastJsonapi::ObjectSerializer + + attributes :name, :year + + attribute :name_with_year do |object| + "#{object.name} (#{object.year})" + end +end +``` + +The block syntax can also be used to override the property on the object: + +```ruby +class MovieSerializer + include FastJsonapi::ObjectSerializer + + attribute :name do |object| + "#{object.name} Part 2" + end +end +``` + ### Compound Document Support for top-level included member through ` options[:include] `. diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index a09dc03f..9ee80e95 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -140,16 +140,18 @@ def cache_options(cache_options) self.cache_length = cache_options[:cache_length] || 5.minutes end - def attributes(*attributes_list) + def attributes(*attributes_list, &block) attributes_list = attributes_list.first if attributes_list.first.class.is_a?(Array) 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] = method_name + attributes_to_serialize[key] = block || method_name end end + alias_method :attribute, :attributes + def add_relationship(name, relationship) self.relationships_to_serialize = {} if relationships_to_serialize.nil? self.cachable_relationships_to_serialize = {} if cachable_relationships_to_serialize.nil? diff --git a/lib/fast_jsonapi/serialization_core.rb b/lib/fast_jsonapi/serialization_core.rb index 95af4fe1..5ac1b8be 100644 --- a/lib/fast_jsonapi/serialization_core.rb +++ b/lib/fast_jsonapi/serialization_core.rb @@ -52,8 +52,8 @@ def ids_hash_from_record_and_relationship(record, relationship) end def attributes_hash(record) - attributes_to_serialize.each_with_object({}) do |(key, method_name), attr_hash| - attr_hash[key] = record.public_send(method_name) + attributes_to_serialize.each_with_object({}) do |(key, method), attr_hash| + attr_hash[key] = method.is_a?(Proc) ? method.call(record) : record.public_send(method) end end diff --git a/spec/lib/object_serializer_with_attribute_block_spec.rb b/spec/lib/object_serializer_with_attribute_block_spec.rb new file mode 100644 index 00000000..885cae0f --- /dev/null +++ b/spec/lib/object_serializer_with_attribute_block_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe FastJsonapi::ObjectSerializer do + include_context 'movie class' + + context 'when including attribute blocks' do + it 'returns correct hash when serializable_hash is called' do + serializable_hash = MovieSerializerWithAttributeBlock.new([movie]).serializable_hash + expect(serializable_hash[:data][0][:attributes][:name]).to eq movie.name + expect(serializable_hash[:data][0][:attributes][:title_with_year]).to eq "#{movie.name} (#{movie.release_year})" + end + end +end diff --git a/spec/shared/contexts/movie_context.rb b/spec/shared/contexts/movie_context.rb index dace0361..49bb53db 100644 --- a/spec/shared/contexts/movie_context.rb +++ b/spec/shared/contexts/movie_context.rb @@ -96,6 +96,15 @@ class MovieTypeSerializer attributes :name end + class MovieSerializerWithAttributeBlock + include FastJsonapi::ObjectSerializer + set_type :movie + attributes :name, :release_year + attribute :title_with_year do |record| + "#{record.name} (#{record.release_year})" + end + end + class SupplierSerializer include FastJsonapi::ObjectSerializer set_type :supplier @@ -140,6 +149,7 @@ class MovieSerializer ActorSerializer MovieType MovieTypeSerializer + MovieSerializerWithAttributeBlock AppName::V1::MovieSerializer MovieStruct ActorStruct From 6b593cb36a20aed71d348637e7e8b52757d03f27 Mon Sep 17 00:00:00 2001 From: Guillermo Iguaran Date: Sun, 11 Feb 2018 01:13:31 -0500 Subject: [PATCH 29/53] Allow the use of fast_jsonapi when ActiveRecord isn't present --- lib/extensions/has_one.rb | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/lib/extensions/has_one.rb b/lib/extensions/has_one.rb index 4e1ef252..0bd134c7 100644 --- a/lib/extensions/has_one.rb +++ b/lib/extensions/has_one.rb @@ -1,16 +1,20 @@ -require 'active_record' +begin + require 'active_record' -::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.to_s}_id - association(:#{name}).reader.try(:id) - end - CODE + ::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.to_s}_id + association(:#{name}).reader.try(:id) + end + CODE + end end +rescue LoadError + # active_record can't be loaded so we shouldn't try to monkey-patch it. end From 013f01dd47bcddd4a0d6dbe12b712b05713407c5 Mon Sep 17 00:00:00 2001 From: Guillermo Iguaran Date: Sun, 11 Feb 2018 01:11:25 -0500 Subject: [PATCH 30/53] Add a generator for Rails --- lib/generators/serializer/USAGE | 8 ++++++++ .../serializer/serializer_generator.rb | 17 +++++++++++++++++ .../serializer/templates/serializer.rb.tt | 6 ++++++ 3 files changed, 31 insertions(+) create mode 100644 lib/generators/serializer/USAGE create mode 100644 lib/generators/serializer/serializer_generator.rb create mode 100644 lib/generators/serializer/templates/serializer.rb.tt diff --git a/lib/generators/serializer/USAGE b/lib/generators/serializer/USAGE new file mode 100644 index 00000000..88ea169a --- /dev/null +++ b/lib/generators/serializer/USAGE @@ -0,0 +1,8 @@ +Description: + Generates a serializer for the given model. + +Example: + rails generate serializer Movie + + This will create: + app/serializers/movie_serializer.rb diff --git a/lib/generators/serializer/serializer_generator.rb b/lib/generators/serializer/serializer_generator.rb new file mode 100644 index 00000000..6c8cf602 --- /dev/null +++ b/lib/generators/serializer/serializer_generator.rb @@ -0,0 +1,17 @@ +require 'rails/generators/base' + +class SerializerGenerator < Rails::Generators::NamedBase + source_root File.expand_path('templates', __dir__) + + argument :attributes, type: :array, default: [], banner: 'field field' + + def create_serializer_file + template 'serializer.rb.tt', File.join('app', 'serializers', class_path, "#{file_name}_serializer.rb") + end + + private + + def attributes_names + attributes.map { |a| a.name.to_sym.inspect } + end +end diff --git a/lib/generators/serializer/templates/serializer.rb.tt b/lib/generators/serializer/templates/serializer.rb.tt new file mode 100644 index 00000000..90da6d66 --- /dev/null +++ b/lib/generators/serializer/templates/serializer.rb.tt @@ -0,0 +1,6 @@ +<% module_namespacing do -%> +class <%= class_name %>Serializer + include FastJsonapi::ObjectSerializer + attributes <%= attributes_names.join(", ") %> +end +<% end -%> From 33f08d925a97436ec72435b051859b2517b2b015 Mon Sep 17 00:00:00 2001 From: Guillermo Iguaran Date: Sun, 11 Feb 2018 15:09:31 -0500 Subject: [PATCH 31/53] Update README to include docs about the Rails generator --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index f054650c..b149dd82 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Fast JSON API serialized 250 records in 3.01 ms * [Features](#features) * [Installation](#installation) * [Usage](#usage) + * [Rails Generator](#rails-generator) * [Model Definition](#model-definition) * [Serializer Definition](#serializer-definition) * [Object Serialization](#object-serialization) @@ -55,6 +56,14 @@ $ bundle install ## Usage +### Rails Generator +You can use the bundled generator if you are using the library inside of +a Rails project: + + rails g Serializer Movie name year + +This will create a new serializer in `app/serializers/movie_serializer.rb` + ### Model Definition ```ruby From eea4496e7b1965dddc3001f00a32c99e7b74f4ee Mon Sep 17 00:00:00 2001 From: Guillermo Iguaran Date: Sun, 11 Feb 2018 14:27:17 -0500 Subject: [PATCH 32/53] Add benchmarks for jsonapi-serializers library --- Gemfile.lock | 3 + fast_jsonapi.gemspec | 1 + .../lib/object_serializer_performance_spec.rb | 43 +++--- spec/shared/contexts/js_context.rb | 123 ++++++++++++++++++ spec/shared/contexts/js_group_context.rb | 116 +++++++++++++++++ spec/spec_helper.rb | 1 + 6 files changed, 272 insertions(+), 15 deletions(-) create mode 100644 spec/shared/contexts/js_context.rb create mode 100644 spec/shared/contexts/js_group_context.rb diff --git a/Gemfile.lock b/Gemfile.lock index c0899cdf..5e082ef0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -55,6 +55,8 @@ GEM jsonapi-renderer (0.2.0) jsonapi-serializable (0.3.0) jsonapi-renderer (~> 0.2.0) + jsonapi-serializers (1.0.0) + activesupport loofah (2.1.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) @@ -104,6 +106,7 @@ DEPENDENCIES byebug fast_jsonapi! jsonapi-rb (~> 0.5.0) + jsonapi-serializers (~> 1.0.0) oj (~> 3.3) rspec (~> 3.5.0) rspec-benchmark (~> 0.3.0) diff --git a/fast_jsonapi.gemspec b/fast_jsonapi.gemspec index ccae628c..8ea08bc6 100644 --- a/fast_jsonapi.gemspec +++ b/fast_jsonapi.gemspec @@ -31,4 +31,5 @@ Gem::Specification.new do |gem| gem.add_development_dependency(%q, ["~> 0.10.7"]) gem.add_development_dependency(%q, ["~> 1.3"]) gem.add_development_dependency(%q, ["~> 0.5.0"]) + gem.add_development_dependency(%q, ["~> 1.0.0"]) end diff --git a/spec/lib/object_serializer_performance_spec.rb b/spec/lib/object_serializer_performance_spec.rb index 03e1ddd8..74ed293b 100644 --- a/spec/lib/object_serializer_performance_spec.rb +++ b/spec/lib/object_serializer_performance_spec.rb @@ -4,10 +4,12 @@ include_context 'movie class' include_context 'ams movie class' include_context 'jsonapi movie class' + include_context 'jsonapi-serializers movie class' include_context 'group class' include_context 'ams group class' include_context 'jsonapi group class' + include_context 'jsonapi-serializers group class' before(:all) { GC.disable } after(:all) { GC.enable } @@ -42,34 +44,38 @@ end end - def print_stats(message, count, ams_time, jsonapi_time, our_time) + def print_stats(message, count, ams_time, jsonapi_time, jsonapis_time, our_time) format = '%-15s %-10s %s' puts '' puts message puts format(format, 'Serializer', 'Records', 'Time') puts format(format, 'AMS serializer', count, ams_time.round(2).to_s + ' ms') puts format(format, 'jsonapi-rb serializer', count, jsonapi_time.round(2).to_s + ' ms') + puts format(format, 'jsonapi-serializers', count, jsonapis_time.round(2).to_s + ' ms') puts format(format, 'Fast serializer', count, our_time.round(2).to_s + ' ms') end - def run_hash_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer) - our_time = Benchmark.measure { our_hash = our_serializer.serializable_hash }.real * 1000 - ams_time = Benchmark.measure { ams_hash = ams_serializer.as_json }.real * 1000 - jsonapi_time = Benchmark.measure { ams_hash = jsonapi_serializer.to_hash }.real * 1000 + def run_hash_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer, jsonapis_serializer) + our_time = Benchmark.measure { our_serializer.serializable_hash }.real * 1000 + ams_time = Benchmark.measure { ams_serializer.as_json }.real * 1000 + jsonapi_time = Benchmark.measure { jsonapi_serializer.to_hash }.real * 1000 + jsonapis_time = Benchmark.measure { jsonapis_serializer.to_hash }.real * 1000 - print_stats(message, movie_count, ams_time, jsonapi_time, our_time) + print_stats(message, movie_count, ams_time, jsonapi_time, jsonapis_time, our_time) end - def run_json_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer) + def run_json_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer, jsonapis_serializer) our_json = nil ams_json = nil jsonapi_json = nil + jsonapis_json = nil our_time = Benchmark.measure { our_json = our_serializer.serialized_json }.real * 1000 ams_time = Benchmark.measure { ams_json = ams_serializer.to_json }.real * 1000 jsonapi_time = Benchmark.measure { jsonapi_json = jsonapi_serializer.to_json }.real * 1000 + jsonapis_time = Benchmark.measure { jsonapis_json = jsonapis_serializer.to_json }.real * 1000 - print_stats(message, movie_count, ams_time, jsonapi_time, our_time) - return our_json, ams_json, jsonapi_json + print_stats(message, movie_count, ams_time, jsonapi_time, jsonapis_time, our_time) + return our_json, ams_json, jsonapi_json, jsonapis_json end context 'when comparing with AMS 0.10.x' do @@ -79,15 +85,17 @@ def run_json_benchmark(message, movie_count, our_serializer, ams_serializer, jso ams_movies = build_ams_movies(movie_count) movies = build_movies(movie_count) jsonapi_movies = build_jsonapi_movies(movie_count) + jsonapis_movies = build_js_movies(movie_count) our_serializer = MovieSerializer.new(movies) ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_movies) jsonapi_serializer = JSONAPISerializer.new(jsonapi_movies) + jsonapis_serializer = JSONAPISSerializer.new(jsonapis_movies) message = "Serialize to JSON string #{movie_count} records" - our_json, ams_json, jsonapi_json = run_json_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer) + our_json, ams_json, _, _ = run_json_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer, jsonapis_serializer) message = "Serialize to Ruby Hash #{movie_count} records" - run_hash_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer) + run_hash_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer, jsonapis_serializer) expect(our_json.length).to eq ams_json.length expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times @@ -103,18 +111,20 @@ def run_json_benchmark(message, movie_count, our_serializer, ams_serializer, jso ams_movies = build_ams_movies(movie_count) movies = build_movies(movie_count) jsonapi_movies = build_jsonapi_movies(movie_count) + jsonapis_movies = build_js_movies(movie_count) options = {} options[:meta] = { total: movie_count } options[:include] = [:actors, :movie_type] our_serializer = MovieSerializer.new(movies, options) ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_movies, include: options[:include], meta: options[:meta]) jsonapi_serializer = JSONAPISerializer.new(jsonapi_movies, include: options[:include], meta: options[:meta]) + jsonapis_serializer = JSONAPISSerializer.new(jsonapis_movies, include: options[:include].map{|i| i.to_s.dasherize}, meta: options[:meta]) message = "Serialize to JSON string #{movie_count} with includes and meta" - our_json, ams_json = run_json_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer) + our_json, ams_json = run_json_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer, jsonapis_serializer) message = "Serialize to Ruby Hash #{movie_count} with includes and meta" - run_hash_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer) + run_hash_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer, jsonapis_serializer) expect(our_json.length).to eq ams_json.length expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times @@ -129,16 +139,19 @@ def run_json_benchmark(message, movie_count, our_serializer, ams_serializer, jso it "should serialize #{group_count} records at least #{speed_factor} times faster than AMS" do ams_groups = build_ams_groups(group_count) groups = build_groups(group_count) + jsonapi_groups = build_jsonapi_groups(group_count) + jsonapis_groups = build_jsonapis_groups(group_count) options = {} our_serializer = GroupSerializer.new(groups, options) ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_groups) jsonapi_serializer = JSONAPISerializerB.new(jsonapi_groups) + jsonapis_serializer = JSONAPISSerializerB.new(jsonapis_groups) message = "Serialize to JSON string #{group_count} with polymorphic has_many" - our_json, ams_json, jsonapi_json = run_json_benchmark(message, group_count, our_serializer, ams_serializer, jsonapi_serializer) + our_json, ams_json, _, _ = run_json_benchmark(message, group_count, our_serializer, ams_serializer, jsonapi_serializer, jsonapis_serializer) message = "Serialize to Ruby Hash #{group_count} with polymorphic has_many" - run_hash_benchmark(message, group_count, our_serializer, ams_serializer, jsonapi_serializer) + run_hash_benchmark(message, group_count, our_serializer, ams_serializer, jsonapi_serializer, jsonapis_serializer) expect(our_json.length).to eq ams_json.length expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times diff --git a/spec/shared/contexts/js_context.rb b/spec/shared/contexts/js_context.rb new file mode 100644 index 00000000..b6d5def3 --- /dev/null +++ b/spec/shared/contexts/js_context.rb @@ -0,0 +1,123 @@ +RSpec.shared_context 'jsonapi-serializers movie class' do + before(:context) do + # models + class JSMovie + attr_accessor :id, :name, :release_year, :actors, :owner, :movie_type + end + + class JSActor + attr_accessor :id, :name, :email + end + + class JSUser + attr_accessor :id, :name + end + + class JSMovieType + attr_accessor :id, :name + end + + # serializers + class JSActorSerializer + include JSONAPI::Serializer + attributes :name, :email + + def type + 'actor' + end + end + class JSUserSerializer + include JSONAPI::Serializer + attributes :name + + def type + 'user' + end + end + class JSMovieTypeSerializer + include JSONAPI::Serializer + attributes :name + + def type + 'movie_type' + end + end + class JSMovieSerializer + include JSONAPI::Serializer + attributes :name, :release_year + has_many :actors + has_one :owner + has_one :movie_type + + def type + 'movie' + end + end + + class JSONAPISSerializer + def initialize(data, options = {}) + @options = options.merge(is_collection: true) + @data = data + end + + def to_json + JSONAPI::Serializer.serialize(@data, @options).to_json + end + + def to_hash + JSONAPI::Serializer.serialize(@data, @options) + end + end + end + + after(:context) do + classes_to_remove = %i[ + JSMovie + JSActor + JSUser + JSMovieType + JSONAPISSerializer + JSActorSerializer + JSUserSerializer + JSMovieTypeSerializer + JSMovieSerializer] + classes_to_remove.each do |klass_name| + Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name) + end + end + + let(:js_actors) do + 3.times.map do |i| + a = JSActor.new + a.id = i + 1 + a.name = "Test #{a.id}" + a.email = "test#{a.id}@test.com" + a + end + end + + let(:js_user) do + ams_user = JSUser.new + ams_user.id = 3 + ams_user + end + + let(:js_movie_type) do + ams_movie_type = JSMovieType.new + ams_movie_type.id = 1 + ams_movie_type.name = 'episode' + ams_movie_type + end + + def build_js_movies(count) + count.times.map do |i| + m = JSMovie.new + m.id = i + 1 + m.name = 'test movie' + m.actors = js_actors + m.owner = js_user + m.movie_type = js_movie_type + m + end + end +end diff --git a/spec/shared/contexts/js_group_context.rb b/spec/shared/contexts/js_group_context.rb new file mode 100644 index 00000000..fe4b4270 --- /dev/null +++ b/spec/shared/contexts/js_group_context.rb @@ -0,0 +1,116 @@ +RSpec.shared_context 'jsonapi-serializers group class' do + + # Person, Group Classes and serializers + before(:context) do + # models + class JSPerson + attr_accessor :id, :first_name, :last_name + end + + class JSGroup + attr_accessor :id, :name, :groupees # Let's assume groupees can be Person or Group objects + end + + # serializers + class JSPersonSerializer + include JSONAPI::Serializer + attributes :first_name, :last_name + + def type + 'person' + end + end + + class JSGroupSerializer + include JSONAPI::Serializer + attributes :name + has_many :groupees + + def type + 'group' + end + end + + class JSONAPISSerializerB + def initialize(data, options = {}) + @options = options.merge(is_collection: true) + @data = data + end + + def to_json + JSON.fast_generate(to_hash) + end + + def to_hash + JSONAPI::Serializer.serialize(@data, @options) + end + end + end + + after :context do + classes_to_remove = %i[ + JSPerson + JSGroup + JSPersonSerializer + JSGroupSerializer] + classes_to_remove.each do |klass_name| + Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name) + end + end + + let(:jsonapi_groups) do + group_count = 0 + person_count = 0 + 3.times.map do |i| + group = JSGroup.new + group.id = group_count + 1 + group.name = "Test Group #{group.id}" + group_count = group.id + + person = JSPerson.new + person.id = person_count + 1 + person.last_name = "Last Name #{person.id}" + person.first_name = "First Name #{person.id}" + person_count = person.id + + child_group = JSGroup.new + child_group.id = group_count + 1 + child_group.name = "Test Group #{child_group.id}" + group_count = child_group.id + + group.groupees = [person, child_group] + group + end + end + + let(:jsonapis_person) do + person = JSPerson.new + person.id = 3 + person + end + + def build_jsonapis_groups(count) + group_count = 0 + person_count = 0 + count.times.map do |i| + group = JSGroup.new + group.id = group_count + 1 + group.name = "Test Group #{group.id}" + group_count = group.id + + person = JSPerson.new + person.id = person_count + 1 + person.last_name = "Last Name #{person.id}" + person.first_name = "First Name #{person.id}" + person_count = person.id + + child_group = JSGroup.new + child_group.id = group_count + 1 + child_group.name = "Test Group #{child_group.id}" + group_count = child_group.id + + group.groupees = [person, child_group] + group + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 77f9226c..d7aac0cf 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,6 +4,7 @@ require 'active_model_serializers' require 'oj' require 'jsonapi/serializable' +require 'jsonapi-serializers' Dir[File.dirname(__FILE__) + '/shared/contexts/*.rb'].each {|file| require file } From 08cee7b82d10097217a276d10b827ae2c4f493f8 Mon Sep 17 00:00:00 2001 From: Les Fletcher Date: Thu, 15 Feb 2018 14:38:04 -0800 Subject: [PATCH 33/53] refactor performance specs --- README.md | 12 ++ .../lib/object_serializer_performance_spec.rb | 175 ++++++++++++------ 2 files changed, 128 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index b149dd82..d198dbdb 100644 --- a/README.md +++ b/README.md @@ -255,6 +255,18 @@ We use [RSpec](http://rspec.info/) for testing. We have unit tests, functional t rspec ``` +To run tests without the performance tests (for quicker test runs): + +```bash +rspec spec --tag ~performance:true +``` + +To run tests only performance tests: + +```bash +rspec spec --tag performance:true +``` + ### We're Hiring! Join the Netflix Studio Engineering team and help us build gems like this! diff --git a/spec/lib/object_serializer_performance_spec.rb b/spec/lib/object_serializer_performance_spec.rb index 74ed293b..e718953c 100644 --- a/spec/lib/object_serializer_performance_spec.rb +++ b/spec/lib/object_serializer_performance_spec.rb @@ -14,6 +14,25 @@ before(:all) { GC.disable } after(:all) { GC.enable } + SERIALIZERS = { + fast_jsonapi: { + name: 'Fast Serializer', + hash_method: :serializable_hash, + json_method: :serialized_json + }, + ams: { + name: 'AMS serializer', + speed_factor: 25, + hash_method: :as_json + }, + jsonapi: { + name: 'jsonapi-rb serializer' + }, + jsonapis: { + name: 'jsonapi-serializers' + } + } + context 'when testing performance of serialization' do it 'should create a hash of 1000 records in less than 50 ms' do movies = 1000.times.map { |_i| movie } @@ -44,118 +63,156 @@ end end - def print_stats(message, count, ams_time, jsonapi_time, jsonapis_time, our_time) - format = '%-15s %-10s %s' - puts '' + def print_stats(message, count, data) + puts puts message - puts format(format, 'Serializer', 'Records', 'Time') - puts format(format, 'AMS serializer', count, ams_time.round(2).to_s + ' ms') - puts format(format, 'jsonapi-rb serializer', count, jsonapi_time.round(2).to_s + ' ms') - puts format(format, 'jsonapi-serializers', count, jsonapis_time.round(2).to_s + ' ms') - puts format(format, 'Fast serializer', count, our_time.round(2).to_s + ' ms') + + name_length = SERIALIZERS.collect { |s| s[1].fetch(:name, s[0]).length }.max + + puts format("%-#{name_length+1}s %-10s %-10s %s", 'Serializer', 'Records', 'Time', 'Speed Up') + + report_format = "%-#{name_length+1}s %-10s %-10s" + fast_jsonapi_time = data[:fast_jsonapi][:time] + puts format(report_format, 'Fast serializer', count, fast_jsonapi_time.round(2).to_s + ' ms') + + data.reject { |k,v| k == :fast_jsonapi }.each_pair do |k,v| + t = v[:time] + factor = t / fast_jsonapi_time + + speed_factor = SERIALIZERS[k].fetch(:speed_factor, 1) + result = factor >= speed_factor ? '✔' : '✘' + + puts format("%-#{name_length+1}s %-10s %-10s %sx %s", SERIALIZERS[k][:name], count, t.round(2).to_s + ' ms', factor.round(2), result) + end end - def run_hash_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer, jsonapis_serializer) - our_time = Benchmark.measure { our_serializer.serializable_hash }.real * 1000 - ams_time = Benchmark.measure { ams_serializer.as_json }.real * 1000 - jsonapi_time = Benchmark.measure { jsonapi_serializer.to_hash }.real * 1000 - jsonapis_time = Benchmark.measure { jsonapis_serializer.to_hash }.real * 1000 + def run_hash_benchmark(message, movie_count, serializers) + data = Hash[serializers.keys.collect { |k| [ k, { hash: nil, time: nil, speed_factor: nil }] }] + + serializers.each_pair do |k,v| + hash_method = SERIALIZERS[k].key?(:hash_method) ? SERIALIZERS[k][:hash_method] : :to_hash + data[k][:time] = Benchmark.measure { data[k][:hash] = v.send(hash_method) }.real * 1000 + end + + print_stats(message, movie_count, data) - print_stats(message, movie_count, ams_time, jsonapi_time, jsonapis_time, our_time) + data end - def run_json_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer, jsonapis_serializer) - our_json = nil - ams_json = nil - jsonapi_json = nil - jsonapis_json = nil - our_time = Benchmark.measure { our_json = our_serializer.serialized_json }.real * 1000 - ams_time = Benchmark.measure { ams_json = ams_serializer.to_json }.real * 1000 - jsonapi_time = Benchmark.measure { jsonapi_json = jsonapi_serializer.to_json }.real * 1000 - jsonapis_time = Benchmark.measure { jsonapis_json = jsonapis_serializer.to_json }.real * 1000 - - print_stats(message, movie_count, ams_time, jsonapi_time, jsonapis_time, our_time) - return our_json, ams_json, jsonapi_json, jsonapis_json + def run_json_benchmark(message, movie_count, serializers) + data = Hash[serializers.keys.collect { |k| [ k, { json: nil, time: nil, speed_factor: nil }] }] + + serializers.each_pair do |k,v| + json_method = SERIALIZERS[k].key?(:json_method) ? SERIALIZERS[k][:json_method] : :to_json + data[k][:time] = Benchmark.measure { data[k][:json] = v.send(json_method) }.real * 1000 + end + + print_stats(message, movie_count, data) + + data end context 'when comparing with AMS 0.10.x' do [1, 25, 250, 1000].each do |movie_count| - speed_factor = 25 - it "should serialize #{movie_count} records atleast #{speed_factor} times faster than AMS" do + it "should serialize #{movie_count} records atleast #{SERIALIZERS[:ams][:speed_factor]} times faster than AMS" do ams_movies = build_ams_movies(movie_count) movies = build_movies(movie_count) jsonapi_movies = build_jsonapi_movies(movie_count) jsonapis_movies = build_js_movies(movie_count) - our_serializer = MovieSerializer.new(movies) - ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_movies) - jsonapi_serializer = JSONAPISerializer.new(jsonapi_movies) - jsonapis_serializer = JSONAPISSerializer.new(jsonapis_movies) + + serializers = { + fast_jsonapi: MovieSerializer.new(movies), + ams: ActiveModelSerializers::SerializableResource.new(ams_movies), + jsonapi: JSONAPISerializer.new(jsonapi_movies), + jsonapis: JSONAPISSerializer.new(jsonapis_movies) + } message = "Serialize to JSON string #{movie_count} records" - our_json, ams_json, _, _ = run_json_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer, jsonapis_serializer) + json_benchmarks = run_json_benchmark(message, movie_count, serializers) message = "Serialize to Ruby Hash #{movie_count} records" - run_hash_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer, jsonapis_serializer) + hash_benchmarks = run_hash_benchmark(message, movie_count, serializers) + + # json + expect(json_benchmarks[:fast_jsonapi][:json].length).to eq json_benchmarks[:ams][:json].length + json_speed_up = json_benchmarks[:ams][:time] / json_benchmarks[:fast_jsonapi][:time] + expect(json_speed_up).to be >= SERIALIZERS[:ams][:speed_factor] - expect(our_json.length).to eq ams_json.length - expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times - expect { our_serializer.serializable_hash }.to perform_faster_than { ams_serializer.as_json }.at_least(speed_factor).times + # hash + hash_speed_up = hash_benchmarks[:ams][:time] / hash_benchmarks[:fast_jsonapi][:time] + expect(hash_speed_up).to be >= SERIALIZERS[:ams][:speed_factor] end end end context 'when comparing with AMS 0.10.x and with includes and meta' do [1, 25, 250, 1000].each do |movie_count| - speed_factor = 25 - it "should serialize #{movie_count} records atleast #{speed_factor} times faster than AMS" do + it "should serialize #{movie_count} records atleast #{SERIALIZERS[:ams][:speed_factor]} times faster than AMS" do ams_movies = build_ams_movies(movie_count) movies = build_movies(movie_count) jsonapi_movies = build_jsonapi_movies(movie_count) jsonapis_movies = build_js_movies(movie_count) + options = {} options[:meta] = { total: movie_count } options[:include] = [:actors, :movie_type] - our_serializer = MovieSerializer.new(movies, options) - ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_movies, include: options[:include], meta: options[:meta]) - jsonapi_serializer = JSONAPISerializer.new(jsonapi_movies, include: options[:include], meta: options[:meta]) - jsonapis_serializer = JSONAPISSerializer.new(jsonapis_movies, include: options[:include].map{|i| i.to_s.dasherize}, meta: options[:meta]) + + serializers = { + fast_jsonapi: MovieSerializer.new(movies, options), + ams: ActiveModelSerializers::SerializableResource.new(ams_movies, include: options[:include], meta: options[:meta]), + jsonapi: JSONAPISerializer.new(jsonapi_movies, include: options[:include], meta: options[:meta]), + jsonapis: JSONAPISSerializer.new(jsonapis_movies, include: options[:include].map { |i| i.to_s.dasherize }, meta: options[:meta]) + } message = "Serialize to JSON string #{movie_count} with includes and meta" - our_json, ams_json = run_json_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer, jsonapis_serializer) + json_benchmarks = run_json_benchmark(message, movie_count, serializers) message = "Serialize to Ruby Hash #{movie_count} with includes and meta" - run_hash_benchmark(message, movie_count, our_serializer, ams_serializer, jsonapi_serializer, jsonapis_serializer) + hash_benchmarks = run_hash_benchmark(message, movie_count, serializers) + + # json + expect(json_benchmarks[:fast_jsonapi][:json].length).to eq json_benchmarks[:ams][:json].length + json_speed_up = json_benchmarks[:ams][:time] / json_benchmarks[:fast_jsonapi][:time] + expect(json_speed_up).to be >= SERIALIZERS[:ams][:speed_factor] - expect(our_json.length).to eq ams_json.length - expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times - expect { our_serializer.serializable_hash }.to perform_faster_than { ams_serializer.as_json }.at_least(speed_factor).times + # hash + hash_speed_up = hash_benchmarks[:ams][:time] / hash_benchmarks[:fast_jsonapi][:time] + expect(hash_speed_up).to be >= SERIALIZERS[:ams][:speed_factor] end end end context 'when comparing with AMS 0.10.x and with polymorphic has_many' do [1, 25, 250, 1000].each do |group_count| - speed_factor = 25 - it "should serialize #{group_count} records at least #{speed_factor} times faster than AMS" do + it "should serialize #{group_count} records at least #{SERIALIZERS[:ams][:speed_factor]} times faster than AMS" do ams_groups = build_ams_groups(group_count) groups = build_groups(group_count) jsonapi_groups = build_jsonapi_groups(group_count) jsonapis_groups = build_jsonapis_groups(group_count) + options = {} - our_serializer = GroupSerializer.new(groups, options) - ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_groups) - jsonapi_serializer = JSONAPISerializerB.new(jsonapi_groups) - jsonapis_serializer = JSONAPISSerializerB.new(jsonapis_groups) + + serializers = { + fast_jsonapi: GroupSerializer.new(groups, options), + ams: ActiveModelSerializers::SerializableResource.new(ams_groups), + jsonapi: JSONAPISerializerB.new(jsonapi_groups), + jsonapis: JSONAPISSerializerB.new(jsonapis_groups) + } message = "Serialize to JSON string #{group_count} with polymorphic has_many" - our_json, ams_json, _, _ = run_json_benchmark(message, group_count, our_serializer, ams_serializer, jsonapi_serializer, jsonapis_serializer) + json_benchmarks = run_json_benchmark(message, group_count, serializers) message = "Serialize to Ruby Hash #{group_count} with polymorphic has_many" - run_hash_benchmark(message, group_count, our_serializer, ams_serializer, jsonapi_serializer, jsonapis_serializer) + hash_benchmarks = run_hash_benchmark(message, group_count, serializers) + + # json + expect(json_benchmarks[:fast_jsonapi][:json].length).to eq json_benchmarks[:ams][:json].length + json_speed_up = json_benchmarks[:ams][:time] / json_benchmarks[:fast_jsonapi][:time] + expect(json_speed_up).to be >= SERIALIZERS[:ams][:speed_factor] - expect(our_json.length).to eq ams_json.length - expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times - expect { our_serializer.serializable_hash }.to perform_faster_than { ams_serializer.as_json }.at_least(speed_factor).times + # hash + hash_speed_up = hash_benchmarks[:ams][:time] / hash_benchmarks[:fast_jsonapi][:time] + expect(hash_speed_up).to be >= SERIALIZERS[:ams][:speed_factor] end end end From 95c136fa0f7dc99f48a8aa7e8eafe3b08be7f847 Mon Sep 17 00:00:00 2001 From: Jay Hayes Date: Mon, 19 Feb 2018 12:51:28 -0600 Subject: [PATCH 34/53] Add #to_hash conversion protocol to object serializer --- 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 9ee80e95..e9006a06 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -42,6 +42,7 @@ def serializable_hash hash_for_one_record end + alias_method :to_hash, :serializable_hash def hash_for_one_record serializable_hash = { data: nil } From 1ff5fe3b1ec67834e1bbcd4da20c0277400c02e9 Mon Sep 17 00:00:00 2001 From: Guillermo Iguaran Date: Wed, 21 Feb 2018 12:28:06 -0500 Subject: [PATCH 35/53] Fix gemspec, README.rdoc isn't present in the project --- fast_jsonapi.gemspec | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fast_jsonapi.gemspec b/fast_jsonapi.gemspec index 8ea08bc6..302c0a55 100644 --- a/fast_jsonapi.gemspec +++ b/fast_jsonapi.gemspec @@ -11,8 +11,7 @@ Gem::Specification.new do |gem| gem.email = "" gem.extra_rdoc_files = [ "LICENSE.txt", - "README.md", - "README.rdoc" + "README.md" ] gem.files = Dir["lib/**/*"] gem.homepage = "http://github.com/Netflix/fast_jsonapi" From dfd215d3a1b0695143520f695891656ee4de18af Mon Sep 17 00:00:00 2001 From: Sam Morgan Date: Wed, 28 Feb 2018 16:29:24 +0000 Subject: [PATCH 36/53] 91 allow includes strings (#93) * add hash benchmarking to performance tests * Add missing attribute in README example * Disable GC before doing performance test * Enable oj to AM for fair benchmark test * add information on performance methodology * add oss metadata * Make an error that demonstrates [Issue * Simple RSpec test that fails with a non-empty string but passes with a non-empty symbol * To run the test, rspec spec/lib/object_serializer_spec.rb * Map includes to symbols if they are provided as strings * Includes would fail with an ArgumentError unless they were explicitly provided as symbols (see #97) * This is solved by mapping the strings to symbols in the ObjectSerializer initializer * No real impact on performance here --- OSSMETADATA | 1 + README.md | 2 +- lib/fast_jsonapi/object_serializer.rb | 2 +- performance_methodology.md | 44 +++++++++++++++++++ .../lib/object_serializer_performance_spec.rb | 3 ++ spec/lib/object_serializer_spec.rb | 6 +++ 6 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 OSSMETADATA create mode 100644 performance_methodology.md diff --git a/OSSMETADATA b/OSSMETADATA new file mode 100644 index 00000000..b96d4a4d --- /dev/null +++ b/OSSMETADATA @@ -0,0 +1 @@ +osslifecycle=active diff --git a/README.md b/README.md index d198dbdb..bbfb8d3d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A lightning fast [JSON:API](http://jsonapi.org/) serializer for Ruby Objects. # 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. +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. ## Benchmark times for 250 records diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index e9006a06..0c55991f 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -84,7 +84,7 @@ def process_options(options) @meta = options[:meta] if options[:include].present? - @includes = options[:include].delete_if(&:blank?) + @includes = options[:include].delete_if(&:blank?).map(&:to_sym) validate_includes!(@includes) end end diff --git a/performance_methodology.md b/performance_methodology.md new file mode 100644 index 00000000..e1de9897 --- /dev/null +++ b/performance_methodology.md @@ -0,0 +1,44 @@ +# Performance using Fast JSON API + +We have been getting a few questions on Github about [Fast JSON API’s](https://github.com/Netflix/fast_jsonapi) performance statistics and the methodology used to measure the performance. This article is an attempt at addressing this aspect of the gem. + +## Prologue + +With use cases like infinite scroll on complex models and bulk update on index pages, we started observing performance degradation on our Rails APIs. Our first step was to enable instrumentation and then tune for performance. We realized that, on average, more than 50% of the time was being spent on AMS serialization. At the same time, we had a couple of APIs that were simply proxying requests on top of a non-Rails, non-JSON API endpoint. Guess what? The non-Rails endpoints were giving us serialized JSON back in a fraction of the time spent by AMS. + +This led us to explore AMS documentation in depth in an effort to try a variety of techniques such as caching, using OJ for JSON string generation etc. It didn’t yield the consistent results we were hoping to get. We loved the developer experience of using AMS, but wanted better performance for our use cases. + +We came up with patterns that we can rely upon such as: + +* We always use [JSON:API](http://jsonapi.org/) for our APIs +* We almost always serialize a homogenous list of objects (Example: An array of movies) + +On the other hand: + +* AMS is designed to serialize JSON in several different formats, not just JSON:API +* AMS can also handle lists that are not homogenous + +This led us to build our own object serialization library that would be faster because it would be tailored to our requirements. The usage of fast_jsonapi internally on production environments resulted in significant performance gains. + +## Benchmark Setup + +The benchmark setup is simple with classes for ``` Movie, Actor, MovieType, User ``` on ```movie_context.rb``` for fast_jsonapi serializers and on ```ams_context.rb``` for AMS serializers. We benchmark the serializers with ```1, 25, 250, 1000``` movies, then we output the result. We also ensure that JSON string output is equivalent to ensure neither library is doing excess work compared to the other. Please checkout [object_serializer_performance_spec](https://github.com/Netflix/fast_jsonapi/blob/master/spec/lib/object_serializer_performance_spec.rb). + +## Benchmark Results + +We benchmarked results for creating a Ruby Hash. This approach removes the effect of chosen JSON string generation engines like OJ, Yajl etc. Benchmarks indicate that fast_jsonapi consistently performs around ```25 times``` faster than AMS in generating a ruby hash. + +We applied a similar benchmark on the operation to serialize the objects to a JSON string. This approach helps with ensuring some important criterias, such as: + +* OJ is used as the JSON engine for benchmarking both AMS and fast_jsonapi +* The benchmark is easy to understand +* The benchmark helps to improve performance +* The benchmark influences design decisions for the gem + +This gem is currently used in several APIs at Netflix and has reduced the response times by more than half on many of these APIs. We truly appreciate the Ruby and Rails communities and wanted to contribute in an effort to help improve the performance of your APIs too. + +## Epilogue + +[Fast JSON API](https://github.com/Netflix/fast_jsonapi) is not a replacement for AMS. AMS is a great gem, and it does many things and is very flexible. We still use it for non JSON:API serialization and deserialization. What started off as an internal performance exercise evolved into fast_jsonapi and created an opportunity to give something back to the awesome **Ruby and Rails communities**. + +We are excited to share it with all of you since we believe that there will be **no** end to this need for speed on APIs. :) diff --git a/spec/lib/object_serializer_performance_spec.rb b/spec/lib/object_serializer_performance_spec.rb index e718953c..ab34a1d4 100644 --- a/spec/lib/object_serializer_performance_spec.rb +++ b/spec/lib/object_serializer_performance_spec.rb @@ -33,6 +33,9 @@ } } + before(:all) { GC.disable } + after(:all) { GC.enable } + context 'when testing performance of serialization' do it 'should create a hash of 1000 records in less than 50 ms' do movies = 1000.times.map { |_i| movie } diff --git a/spec/lib/object_serializer_spec.rb b/spec/lib/object_serializer_spec.rb index dd7fd920..fb0aab85 100644 --- a/spec/lib/object_serializer_spec.rb +++ b/spec/lib/object_serializer_spec.rb @@ -89,6 +89,12 @@ expect { MovieSerializer.new([movie, movie], options).serializable_hash }.to raise_error(ArgumentError) end + it 'does not throw an error with non-empty string array includes key' do + options = {} + options[:include] = ['actors'] + 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 } From 765eaa70d61fdf3314449bad8e672be3679ac6e3 Mon Sep 17 00:00:00 2001 From: Vlado Cingel Date: Sun, 11 Mar 2018 21:00:16 +0100 Subject: [PATCH 37/53] Title fixed for one spec --- spec/lib/object_serializer_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/object_serializer_spec.rb b/spec/lib/object_serializer_spec.rb index fb0aab85..ccdb6fb6 100644 --- a/spec/lib/object_serializer_spec.rb +++ b/spec/lib/object_serializer_spec.rb @@ -127,7 +127,7 @@ class BlahBlahSerializerBuilder expect(BlahBlahSerializerBuilder.record_type).to be_nil end - it 'shouldnt set default_type for a serializer that doesnt follow convention' do + it 'should set default_type for a namespaced serializer' do module V1 class BlahSerializer include FastJsonapi::ObjectSerializer From 8c630d9b1cc977e127afd39c39db50762e307857 Mon Sep 17 00:00:00 2001 From: Shuhei Kitagawa Date: Sun, 11 Mar 2018 12:07:35 +0900 Subject: [PATCH 38/53] Remove unused local variables from #relationships_hash --- lib/fast_jsonapi/serialization_core.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/fast_jsonapi/serialization_core.rb b/lib/fast_jsonapi/serialization_core.rb index 5ac1b8be..bed95d03 100644 --- a/lib/fast_jsonapi/serialization_core.rb +++ b/lib/fast_jsonapi/serialization_core.rb @@ -62,8 +62,6 @@ def relationships_hash(record, relationships = nil) relationships.each_with_object({}) do |(_k, relationship), hash| name = relationship[:key] - id_method_name = relationship[:id_method_name] - record_type = relationship[:record_type] empty_case = relationship[:relationship_type] == :has_many ? [] : nil hash[name] = { data: ids_hash_from_record_and_relationship(record, relationship) || empty_case From b4eaa04c09487c93e8da0cf7571d95beb3d7843a Mon Sep 17 00:00:00 2001 From: Shuhei Kitagawa Date: Sun, 11 Mar 2018 11:49:03 +0900 Subject: [PATCH 39/53] Add frozen_string_literal: true --- lib/extensions/has_one.rb | 2 ++ lib/fast_jsonapi.rb | 2 ++ lib/fast_jsonapi/multi_to_json.rb | 2 ++ lib/fast_jsonapi/object_serializer.rb | 2 ++ lib/fast_jsonapi/serialization_core.rb | 2 ++ lib/generators/serializer/serializer_generator.rb | 2 ++ 6 files changed, 12 insertions(+) diff --git a/lib/extensions/has_one.rb b/lib/extensions/has_one.rb index 0bd134c7..e4b38cc2 100644 --- a/lib/extensions/has_one.rb +++ b/lib/extensions/has_one.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + begin require 'active_record' diff --git a/lib/fast_jsonapi.rb b/lib/fast_jsonapi.rb index 46af4e9c..d257b6eb 100644 --- a/lib/fast_jsonapi.rb +++ b/lib/fast_jsonapi.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module FastJsonapi require 'fast_jsonapi/object_serializer' require 'extensions/has_one' diff --git a/lib/fast_jsonapi/multi_to_json.rb b/lib/fast_jsonapi/multi_to_json.rb index b43f1a0a..9dd7b5c8 100644 --- a/lib/fast_jsonapi/multi_to_json.rb +++ b/lib/fast_jsonapi/multi_to_json.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Usage: # class Movie # def to_json(payload) diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index 0c55991f..70bf1672 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/core_ext/object' require 'active_support/concern' require 'active_support/inflector' diff --git a/lib/fast_jsonapi/serialization_core.rb b/lib/fast_jsonapi/serialization_core.rb index bed95d03..db6a2cf0 100644 --- a/lib/fast_jsonapi/serialization_core.rb +++ b/lib/fast_jsonapi/serialization_core.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/concern' require 'fast_jsonapi/multi_to_json' diff --git a/lib/generators/serializer/serializer_generator.rb b/lib/generators/serializer/serializer_generator.rb index 6c8cf602..7eb0e0ab 100644 --- a/lib/generators/serializer/serializer_generator.rb +++ b/lib/generators/serializer/serializer_generator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails/generators/base' class SerializerGenerator < Rails::Generators::NamedBase From 93391aff6a60cf078fcfc4b7931ca9afba8234b7 Mon Sep 17 00:00:00 2001 From: Shuhei Kitagawa Date: Sat, 10 Mar 2018 18:09:16 +0900 Subject: [PATCH 40/53] Remove redundant to_s --- lib/extensions/has_one.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/extensions/has_one.rb b/lib/extensions/has_one.rb index e4b38cc2..5d68f1b3 100644 --- a/lib/extensions/has_one.rb +++ b/lib/extensions/has_one.rb @@ -11,7 +11,7 @@ def self.define_accessors(mixin, reflection) super name = reflection.name mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name.to_s}_id + def #{name}_id association(:#{name}).reader.try(:id) end CODE From 1e40b7d2f721e120b41edabf83619a87910b3d90 Mon Sep 17 00:00:00 2001 From: Shuhei Kitagawa Date: Mon, 12 Mar 2018 21:11:48 +0900 Subject: [PATCH 41/53] Remove redundant GC.disable and GC.enable --- spec/lib/object_serializer_performance_spec.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/spec/lib/object_serializer_performance_spec.rb b/spec/lib/object_serializer_performance_spec.rb index ab34a1d4..e718953c 100644 --- a/spec/lib/object_serializer_performance_spec.rb +++ b/spec/lib/object_serializer_performance_spec.rb @@ -33,9 +33,6 @@ } } - before(:all) { GC.disable } - after(:all) { GC.enable } - context 'when testing performance of serialization' do it 'should create a hash of 1000 records in less than 50 ms' do movies = 1000.times.map { |_i| movie } From f029cf737a8d33d981be35ca83f19e0fde26a6af Mon Sep 17 00:00:00 2001 From: corinnekunze Date: Sat, 10 Mar 2018 10:14:21 -0800 Subject: [PATCH 42/53] Adding specs for #as_json method on ObjectSerializer and making sure non-included attributes are removed --- .gitignore | 6 ++++++ spec/lib/object_serializer_spec.rb | 18 ++++++++++++++++++ spec/shared/contexts/movie_context.rb | 18 ++++++++++++++++-- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 4993663b..6973e11c 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,9 @@ doc # For the gem test.db + +# For those using rbenv +.ruby-version + +# For those who install gems locally to a vendor dir +/vendor diff --git a/spec/lib/object_serializer_spec.rb b/spec/lib/object_serializer_spec.rb index ccdb6fb6..83e1eeec 100644 --- a/spec/lib/object_serializer_spec.rb +++ b/spec/lib/object_serializer_spec.rb @@ -82,6 +82,24 @@ expect(serializable_hash['data']).to eq [] end + describe '#as_json' do + it 'returns a json hash' do + json_hash = MovieSerializer.new(movie).as_json + expect(json_hash['data']['id']).to eq movie.id.to_s + end + + it 'returns multiple records' do + json_hash = MovieSerializer.new([movie, movie]).as_json + expect(json_hash['data'].length).to eq 2 + end + + it 'removes non-relevant attributes' do + movie.director = 'steven spielberg' + json_hash = MovieSerializer.new(movie).as_json + expect(json_hash['data']['director']).to eq(nil) + end + end + it 'returns errors when serializing with non-existent includes key' do options = {} options[:meta] = { total: 2 } diff --git a/spec/shared/contexts/movie_context.rb b/spec/shared/contexts/movie_context.rb index 49bb53db..f26f21fd 100644 --- a/spec/shared/contexts/movie_context.rb +++ b/spec/shared/contexts/movie_context.rb @@ -4,7 +4,13 @@ before(:context) do # models class Movie - attr_accessor :id, :name, :release_year, :actor_ids, :owner_id, :movie_type_id + attr_accessor :id, + :name, + :release_year, + :director, + :actor_ids, + :owner_id, + :movie_type_id def actors actor_ids.map do |id| @@ -56,6 +62,7 @@ class Account class MovieSerializer include FastJsonapi::ObjectSerializer set_type :movie + # director attr is not mentioned intentionally attributes :name, :release_year has_many :actors belongs_to :owner, record_type: :user @@ -135,7 +142,14 @@ class MovieSerializer # Movie and Actor struct before(:context) do MovieStruct = Struct.new( - :id, :name, :release_year, :actor_ids, :actors, :owner_id, :owner, :movie_type_id + :id, + :name, + :release_year, + :actor_ids, + :actors, + :owner_id, + :owner, + :movie_type_id ) ActorStruct = Struct.new(:id, :name, :email) From 2da0b5bd87ecdf03acd118653a41de44a95d6ac7 Mon Sep 17 00:00:00 2001 From: Les Fletcher Date: Sat, 17 Feb 2018 11:55:59 -0800 Subject: [PATCH 43/53] rework of AS Notifications --- lib/fast_jsonapi/instrumentation.rb | 2 + .../instrumentation/serializable_hash.rb | 15 ++++ .../instrumentation/serialized_json.rb | 15 ++++ lib/fast_jsonapi/object_serializer.rb | 21 +----- .../as_notifications_negative_spec.rb | 56 +++++++++++++++ .../instrumentation/as_notifications_spec.rb | 69 +++++++++++++++++++ 6 files changed, 160 insertions(+), 18 deletions(-) create mode 100644 lib/fast_jsonapi/instrumentation.rb create mode 100644 lib/fast_jsonapi/instrumentation/serializable_hash.rb create mode 100644 lib/fast_jsonapi/instrumentation/serialized_json.rb create mode 100644 spec/lib/instrumentation/as_notifications_negative_spec.rb create mode 100644 spec/lib/instrumentation/as_notifications_spec.rb diff --git a/lib/fast_jsonapi/instrumentation.rb b/lib/fast_jsonapi/instrumentation.rb new file mode 100644 index 00000000..936aeb59 --- /dev/null +++ b/lib/fast_jsonapi/instrumentation.rb @@ -0,0 +1,2 @@ +require 'fast_jsonapi/instrumentation/serializable_hash' +require 'fast_jsonapi/instrumentation/serialized_json' diff --git a/lib/fast_jsonapi/instrumentation/serializable_hash.rb b/lib/fast_jsonapi/instrumentation/serializable_hash.rb new file mode 100644 index 00000000..eb86a60e --- /dev/null +++ b/lib/fast_jsonapi/instrumentation/serializable_hash.rb @@ -0,0 +1,15 @@ +require 'active_support/notifications' + +module FastJsonapi + module ObjectSerializer + + alias_method :serializable_hash_without_instrumentation, :serializable_hash + + def serializable_hash + ActiveSupport::Notifications.instrument(SERIALIZABLE_HASH_NOTIFICATION, { name: self.class.name }) do + serializable_hash_without_instrumentation + end + end + + end +end diff --git a/lib/fast_jsonapi/instrumentation/serialized_json.rb b/lib/fast_jsonapi/instrumentation/serialized_json.rb new file mode 100644 index 00000000..4fa3cb57 --- /dev/null +++ b/lib/fast_jsonapi/instrumentation/serialized_json.rb @@ -0,0 +1,15 @@ +require 'active_support/notifications' + +module FastJsonapi + module ObjectSerializer + + alias_method :serialized_json_without_instrumentation, :serialized_json + + def serialized_json + ActiveSupport::Notifications.instrument(SERIALIZED_JSON_NOTIFICATION, { name: self.class.name }) do + serialized_json_without_instrumentation + end + end + + end +end diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index 70bf1672..e0823938 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -5,30 +5,15 @@ require 'active_support/inflector' require 'fast_jsonapi/serialization_core' -begin - require 'skylight' - SKYLIGHT_ENABLED = true -rescue LoadError - SKYLIGHT_ENABLED = false -end - module FastJsonapi module ObjectSerializer extend ActiveSupport::Concern include SerializationCore - included do - # Skylight integration - # To remove Skylight - # Remove the included do block - # Remove the Gemfile entry - if SKYLIGHT_ENABLED - include Skylight::Helpers - - instrument_method :serializable_hash - instrument_method :to_json - end + SERIALIZABLE_HASH_NOTIFICATION = 'render.fast_jsonapi.serializable_hash'.freeze + SERIALIZED_JSON_NOTIFICATION = 'render.fast_jsonapi.serialized_json'.freeze + included do # Set record_type based on the name of the serializer class set_type(reflected_record_type) if reflected_record_type end diff --git a/spec/lib/instrumentation/as_notifications_negative_spec.rb b/spec/lib/instrumentation/as_notifications_negative_spec.rb new file mode 100644 index 00000000..6912ed49 --- /dev/null +++ b/spec/lib/instrumentation/as_notifications_negative_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe FastJsonapi::ObjectSerializer do + include_context 'movie class' + + context 'instrument' do + + before(:each) do + options = {} + options[:meta] = { total: 2 } + options[:include] = [:actors] + + @serializer = MovieSerializer.new([movie, movie], options) + end + + context 'serializable_hash' do + + it 'should send not notifications' do + events = [] + + ActiveSupport::Notifications.subscribe(FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION) do |*args| + events << ActiveSupport::Notifications::Event.new(*args) + end + + serialized_hash = @serializer.serializable_hash + + expect(events.length).to eq(0) + + expect(serialized_hash.key?(:data)).to eq(true) + expect(serialized_hash.key?(:meta)).to eq(true) + expect(serialized_hash.key?(:included)).to eq(true) + end + + end + + context 'serialized_json' do + + it 'should send not notifications' do + events = [] + + ActiveSupport::Notifications.subscribe(FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION) do |*args| + events << ActiveSupport::Notifications::Event.new(*args) + end + + json = @serializer.serialized_json + + expect(events.length).to eq(0) + + expect(json.length).to be > 50 + end + + end + + end + +end diff --git a/spec/lib/instrumentation/as_notifications_spec.rb b/spec/lib/instrumentation/as_notifications_spec.rb new file mode 100644 index 00000000..27bfdad8 --- /dev/null +++ b/spec/lib/instrumentation/as_notifications_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' +require 'fast_jsonapi/instrumentation' + +describe FastJsonapi::ObjectSerializer do + include_context 'movie class' + + context 'instrument' do + + before(:each) do + options = {} + options[:meta] = { total: 2 } + options[:include] = [:actors] + + @serializer = MovieSerializer.new([movie, movie], options) + end + + context 'serializable_hash' do + + it 'should send notifications' do + events = [] + + ActiveSupport::Notifications.subscribe(FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION) do |*args| + events << ActiveSupport::Notifications::Event.new(*args) + end + + serialized_hash = @serializer.serializable_hash + + expect(events.length).to eq(1) + + event = events.first + + expect(event.duration).to be > 0 + expect(event.payload).to eq({ name: 'MovieSerializer' }) + expect(event.name).to eq(FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION) + + expect(serialized_hash.key?(:data)).to eq(true) + expect(serialized_hash.key?(:meta)).to eq(true) + expect(serialized_hash.key?(:included)).to eq(true) + end + + end + + context 'serialized_json' do + + it 'should send notifications' do + events = [] + + ActiveSupport::Notifications.subscribe(FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION) do |*args| + events << ActiveSupport::Notifications::Event.new(*args) + end + + json = @serializer.serialized_json + + expect(events.length).to eq(1) + + event = events.first + + expect(event.duration).to be > 0 + expect(event.payload).to eq({ name: 'MovieSerializer' }) + expect(event.name).to eq(FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION) + + expect(json.length).to be > 50 + end + + end + + end + +end From 8d905dbdfd4bdb3299d962803f48edc8bb90b453 Mon Sep 17 00:00:00 2001 From: Les Fletcher Date: Sat, 17 Feb 2018 14:31:20 -0800 Subject: [PATCH 44/53] taking a crack at the normalizers --- lib/fast_jsonapi/instrumentation/skylight.rb | 2 ++ .../skylight/normalizers/serializable_hash.rb | 22 +++++++++++++++++++ .../skylight/normalizers/serialized_json.rb | 22 +++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 lib/fast_jsonapi/instrumentation/skylight.rb create mode 100644 lib/fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash.rb create mode 100644 lib/fast_jsonapi/instrumentation/skylight/normalizers/serialized_json.rb diff --git a/lib/fast_jsonapi/instrumentation/skylight.rb b/lib/fast_jsonapi/instrumentation/skylight.rb new file mode 100644 index 00000000..1f3dc7eb --- /dev/null +++ b/lib/fast_jsonapi/instrumentation/skylight.rb @@ -0,0 +1,2 @@ +require 'fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash' +require 'fast_jsonapi/instrumentation/skylight/normalizers/serialized_json' diff --git a/lib/fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash.rb b/lib/fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash.rb new file mode 100644 index 00000000..bc27ee49 --- /dev/null +++ b/lib/fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash.rb @@ -0,0 +1,22 @@ +require 'skylight' +require 'fast_jsonapi/instrumentation/serializable_hash' + +module FastJsonapi + module Instrumentation + module Skylight + module Normalizers + class SerializableHash < Skylight::Normalizers::Normalizer + + register FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION + + CAT = "view.#{FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION}".freeze + + def normalize(trace, name, payload) + [ CAT, payload[:name], nil ] + end + + end + end + end + end +end diff --git a/lib/fast_jsonapi/instrumentation/skylight/normalizers/serialized_json.rb b/lib/fast_jsonapi/instrumentation/skylight/normalizers/serialized_json.rb new file mode 100644 index 00000000..a04f6c0f --- /dev/null +++ b/lib/fast_jsonapi/instrumentation/skylight/normalizers/serialized_json.rb @@ -0,0 +1,22 @@ +require 'skylight' +require 'fast_jsonapi/instrumentation/serializable_hash' + +module FastJsonapi + module Instrumentation + module Skylight + module Normalizers + class SerializedJson < Skylight::Normalizers::Normalizer + + register FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION + + CAT = "view.#{FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION}".freeze + + def normalize(trace, name, payload) + [ CAT, payload[:name], nil ] + end + + end + end + end + end +end From 41bffb1e20233fdf34850036d28a9729f135c0d7 Mon Sep 17 00:00:00 2001 From: Les Fletcher Date: Sat, 17 Feb 2018 14:31:33 -0800 Subject: [PATCH 45/53] Some docs for the instrumentation --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index bbfb8d3d..343e1dc2 100644 --- a/README.md +++ b/README.md @@ -244,6 +244,36 @@ object_method_name | Set custom method name to get related objects | ```has_many 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``` +### Instrumentation + +`fast_jsonapi` also has builtin [Skylight](https://www.skylight.io/) integration. To enable, add the following to an initializer: + +```ruby +require 'fast_jsonapi/instrumentation/skylight' +``` + +Skylight relies on `ActiveSupport::Notifications` to track these two core methods. If you would like to use these notifications without using Skylight, simply require the instrumentation integration: + +```ruby +require 'fast_jsonapi/instrumentation' +``` + +The two instrumented notifcations are supplied by these two constants: +* `FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION` +* `FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION` + +It is also possible to instrument one method without the other by using one of the following require statements: + +```ruby +require 'fast_jsonapi/instrumentation/serializable_hash' +require 'fast_jsonapi/instrumentation/serialized_json' +``` + +Same goes for the Skylight integration: +```ruby +require 'fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash' +require 'fast_jsonapi/instrumentation/skylight/normalizers/serialized_json' +``` ## Contributing Please see [contribution check](https://github.com/Netflix/fast_jsonapi/blob/master/CONTRIBUTING.md) for more details on contributing From c75d7ceadc0e20635501002d472cc0cbb86536ea Mon Sep 17 00:00:00 2001 From: Les Fletcher Date: Tue, 27 Feb 2018 15:49:01 -0800 Subject: [PATCH 46/53] do two separate rspec runs because of require issue --- .travis.yml | 1 + .../{as_notifications_spec.rb => as_notifications.rb} | 0 2 files changed, 1 insertion(+) rename spec/lib/instrumentation/{as_notifications_spec.rb => as_notifications.rb} (100%) diff --git a/.travis.yml b/.travis.yml index 00b98f79..1a9864bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,3 +6,4 @@ rvm: - 2.5.0 script: - bundle exec rspec + - bundle exec rspec spec/lib/instrumentation/as_notifications.rb diff --git a/spec/lib/instrumentation/as_notifications_spec.rb b/spec/lib/instrumentation/as_notifications.rb similarity index 100% rename from spec/lib/instrumentation/as_notifications_spec.rb rename to spec/lib/instrumentation/as_notifications.rb From 3a66a6f80b2f6e44435ad5e1470085935e90792d Mon Sep 17 00:00:00 2001 From: Les Fletcher Date: Thu, 8 Mar 2018 21:41:11 -0800 Subject: [PATCH 47/53] tear down the aliases --- .travis.yml | 1 - ..._notifications.rb => as_notifications_spec.rb} | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) rename spec/lib/instrumentation/{as_notifications.rb => as_notifications_spec.rb} (80%) diff --git a/.travis.yml b/.travis.yml index 1a9864bf..00b98f79 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,4 +6,3 @@ rvm: - 2.5.0 script: - bundle exec rspec - - bundle exec rspec spec/lib/instrumentation/as_notifications.rb diff --git a/spec/lib/instrumentation/as_notifications.rb b/spec/lib/instrumentation/as_notifications_spec.rb similarity index 80% rename from spec/lib/instrumentation/as_notifications.rb rename to spec/lib/instrumentation/as_notifications_spec.rb index 27bfdad8..f7769ffe 100644 --- a/spec/lib/instrumentation/as_notifications.rb +++ b/spec/lib/instrumentation/as_notifications_spec.rb @@ -1,11 +1,24 @@ require 'spec_helper' -require 'fast_jsonapi/instrumentation' describe FastJsonapi::ObjectSerializer do include_context 'movie class' context 'instrument' do + before(:all) do + require 'fast_jsonapi/instrumentation' + end + + after(:all) do + [ :serialized_json, :serializable_hash ].each do |m| + alias_command = "alias_method :#{m}, :#{m}_without_instrumentation" + FastJsonapi::ObjectSerializer.class_eval(alias_command) + + remove_command = "remove_method :#{m}_without_instrumentation" + FastJsonapi::ObjectSerializer.class_eval(remove_command) + end + end + before(:each) do options = {} options[:meta] = { total: 2 } From 00e960f88338eb4196aebc411021b406764e6783 Mon Sep 17 00:00:00 2001 From: Shishir Kakaraddi Date: Sat, 17 Mar 2018 17:46:42 -0700 Subject: [PATCH 48/53] adds the use_hyphen feature back and adds a deprecation warning --- lib/fast_jsonapi/object_serializer.rb | 5 +++++ spec/lib/object_serializer_class_methods_spec.rb | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index e0823938..735fdb84 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -119,6 +119,11 @@ def run_key_transform(input) end end + def use_hyphen + warn('DEPRECATION WARNING: use_hyphen is deprecated and will be removed from fast_jsonapi 2.0 use (set_key_transform :dash) instead') + set_key_transform :dash + end + def set_type(type_name) self.record_type = run_key_transform(type_name) end diff --git a/spec/lib/object_serializer_class_methods_spec.rb b/spec/lib/object_serializer_class_methods_spec.rb index a83cbcc0..4d23c2bf 100644 --- a/spec/lib/object_serializer_class_methods_spec.rb +++ b/spec/lib/object_serializer_class_methods_spec.rb @@ -64,6 +64,13 @@ relationship = AppName::V1::MovieSerializer.relationships_to_serialize[:area] expect(relationship[:serializer]).to be :'AppName::V1::AreaSerializer' end + + it 'sets the correct transform_method when use_hyphen is used' do + MovieSerializer.use_hyphen + warning_message = 'DEPRECATION WARNING: use_hyphen is deprecated and will be removed from fast_jsonapi 2.0 use (set_key_transform :dash) instead' + expect { MovieSerializer.use_hyphen }.to output.to_stderr + expect(MovieSerializer.instance_variable_get(:@transform_method)).to eq :dasherize + end end end From 8f4e7161535eab5332f7ab78c509fea65d58871c Mon Sep 17 00:00:00 2001 From: Shuhei Kitagawa Date: Wed, 14 Mar 2018 09:46:20 +0900 Subject: [PATCH 49/53] Define set_id method --- lib/fast_jsonapi/object_serializer.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index 735fdb84..2f5ebe4e 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -128,6 +128,10 @@ def set_type(type_name) self.record_type = run_key_transform(type_name) end + def set_id(id_name) + self.record_id = id_name + end + def cache_options(cache_options) self.cached = cache_options[:enabled] || false self.cache_length = cache_options[:cache_length] || 5.minutes From 43239aff490a6f57cb39e2bb7403d0a06cd61657 Mon Sep 17 00:00:00 2001 From: Shuhei Kitagawa Date: Wed, 14 Mar 2018 09:48:13 +0900 Subject: [PATCH 50/53] Enable to set customized id column --- lib/fast_jsonapi/serialization_core.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/fast_jsonapi/serialization_core.rb b/lib/fast_jsonapi/serialization_core.rb index db6a2cf0..3c2e9b71 100644 --- a/lib/fast_jsonapi/serialization_core.rb +++ b/lib/fast_jsonapi/serialization_core.rb @@ -14,6 +14,7 @@ class << self :cachable_relationships_to_serialize, :uncachable_relationships_to_serialize, :record_type, + :record_id, :cache_length, :cached end @@ -74,7 +75,8 @@ def relationships_hash(record, relationships = nil) def record_hash(record) if cached record_hash = Rails.cache.fetch(record.cache_key, expires_in: cache_length) do - temp_hash = id_hash(record.id, record_type) || { id: nil, type: record_type } + id = record_id ? record.send(record_id) : record.id + temp_hash = id_hash(id, record_type) || { id: nil, type: record_type } temp_hash[:attributes] = attributes_hash(record) if attributes_to_serialize.present? temp_hash[:relationships] = {} temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize) if cachable_relationships_to_serialize.present? @@ -83,7 +85,8 @@ def record_hash(record) record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize)) if uncachable_relationships_to_serialize.present? record_hash else - record_hash = id_hash(record.id, record_type) || { id: nil, type: record_type } + id = record_id ? record.send(record_id) : record.id + record_hash = id_hash(id, record_type) || { id: nil, type: record_type } record_hash[:attributes] = attributes_hash(record) if attributes_to_serialize.present? record_hash[:relationships] = relationships_hash(record) if relationships_to_serialize.present? record_hash From a15232b47f13c574e3a25513627a66031c64c3d2 Mon Sep 17 00:00:00 2001 From: Shuhei Kitagawa Date: Wed, 14 Mar 2018 10:16:04 +0900 Subject: [PATCH 51/53] Add tests for set_id method --- spec/lib/object_serializer_set_id_spec.rb | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 spec/lib/object_serializer_set_id_spec.rb diff --git a/spec/lib/object_serializer_set_id_spec.rb b/spec/lib/object_serializer_set_id_spec.rb new file mode 100644 index 00000000..2f731fde --- /dev/null +++ b/spec/lib/object_serializer_set_id_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe FastJsonapi::ObjectSerializer do + include_context 'movie class' + + context 'when setting id' do + subject(:serializable_hash) { MovieSerializer.new(resource).serializable_hash } + + before(:all) do + MovieSerializer.set_id :owner_id + 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 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][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 +end From fa09c291907b2633bc28e6b2d470544d84e07501 Mon Sep 17 00:00:00 2001 From: Shuhei Kitagawa Date: Sun, 18 Mar 2018 15:24:15 +0900 Subject: [PATCH 52/53] Add document for for set_id method --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 343e1dc2..bff28e59 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ end class MovieSerializer include FastJsonapi::ObjectSerializer set_type :movie # optional + set_id :owner_id # optional attributes :name, :year has_many :actors belongs_to :owner, record_type: :user @@ -114,7 +115,7 @@ json_string = MovieSerializer.new(movie).serialized_json ```json { "data": { - "id": "232", + "id": "3", "type": "movie", "attributes": { "name": "test movie", @@ -238,6 +239,7 @@ end Option | Purpose | Example ------------ | ------------- | ------------- set_type | Type name of Object | ```set_type :movie ``` +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``` 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 c40354e375281369f7145f58c5bf02101f5e40c1 Mon Sep 17 00:00:00 2001 From: Shishir Kakaraddi Date: Sun, 18 Mar 2018 09:04:59 -0700 Subject: [PATCH 53/53] Bump version to 1.1.0 --- Gemfile.lock | 2 +- fast_jsonapi.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5e082ef0..5a3cb842 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - fast_jsonapi (1.0.17) + fast_jsonapi (1.1.0) activesupport (>= 4.2) GEM diff --git a/fast_jsonapi.gemspec b/fast_jsonapi.gemspec index 302c0a55..24edd232 100644 --- a/fast_jsonapi.gemspec +++ b/fast_jsonapi.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |gem| gem.name = "fast_jsonapi" - gem.version = "1.0.17" + gem.version = "1.1.0" gem.required_rubygems_version = Gem::Requirement.new(">= 0") if gem.respond_to? :required_rubygems_version= gem.metadata = { "allowed_push_host" => "https://rubygems.org" } if gem.respond_to? :metadata=