fast_jsonapi 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,111 @@
1
+ require 'active_support/concern'
2
+
3
+ module FastJsonapi
4
+ module SerializationCore
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class << self
9
+ attr_accessor :attributes_to_serialize,
10
+ :relationships_to_serialize,
11
+ :cachable_relationships_to_serialize,
12
+ :uncachable_relationships_to_serialize,
13
+ :record_type,
14
+ :cache_length,
15
+ :cached
16
+ :record_type
17
+ end
18
+ end
19
+
20
+ class_methods do
21
+ def id_hash(id, record_type)
22
+ return { id: id.to_s, type: record_type } if id.present?
23
+ end
24
+
25
+ def ids_hash(ids, record_type)
26
+ return ids.map { |id| id_hash(id, record_type) } if ids.respond_to? :map
27
+ id_hash(ids, record_type) # ids variable is just a single id here
28
+ end
29
+
30
+ def attributes_hash(record)
31
+ attributes_hash = {}
32
+ attributes_to_serialize.each do |key, method_name|
33
+ attributes_hash[key] = record.send(method_name)
34
+ end
35
+ attributes_hash
36
+ end
37
+
38
+ def relationships_hash(record, relationships = nil)
39
+ relationships_hash = {}
40
+ relationships = relationships_to_serialize if relationships.nil?
41
+
42
+ relationships.each do |_k, relationship|
43
+ name = relationship[:key]
44
+ id_method_name = relationship[:id_method_name]
45
+ record_type = relationship[:record_type]
46
+ empty_case = relationship[:relationship_type] == :has_many ? [] : nil
47
+ relationships_hash[name] = {
48
+ data: ids_hash(record.send(id_method_name), record_type) || empty_case
49
+ }
50
+ end
51
+ relationships_hash
52
+ end
53
+
54
+ def record_hash(record)
55
+ if cached
56
+ record_hash = Rails.cache.fetch(record.cache_key, expires_in: cache_length) do
57
+ record_hash = id_hash(record.id, record_type) || { id: nil, type: record_type }
58
+ record_hash[:attributes] = attributes_hash(record) if attributes_to_serialize.present?
59
+ record_hash[:relationships] = {}
60
+ record_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize) if cachable_relationships_to_serialize.present?
61
+ record_hash
62
+ end
63
+ record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize)) if uncachable_relationships_to_serialize.present?
64
+ record_hash
65
+ else
66
+ record_hash = id_hash(record.id, record_type) || { id: nil, type: record_type }
67
+ record_hash[:attributes] = attributes_hash(record) if attributes_to_serialize.present?
68
+ record_hash[:relationships] = relationships_hash(record) if relationships_to_serialize.present?
69
+ record_hash
70
+ end
71
+ end
72
+
73
+ def to_json(payload)
74
+ MultiJson.dump(payload) if payload.present?
75
+ end
76
+
77
+ # includes handler
78
+
79
+ def get_included_records(record, includes_list, known_included_objects)
80
+ included_records = []
81
+ includes_list.each do |item|
82
+ object_method_name = @relationships_to_serialize[item][:object_method_name]
83
+ record_type = @relationships_to_serialize[item][:record_type]
84
+ serializer = @relationships_to_serialize[item][:serializer].to_s.constantize
85
+ relationship_type = @relationships_to_serialize[item][:relationship_type]
86
+ included_objects = record.send(object_method_name)
87
+ next if included_objects.blank?
88
+ included_objects = [included_objects] unless relationship_type == :has_many
89
+ included_objects.each do |inc_obj|
90
+ code = "#{record_type}_#{inc_obj.id}"
91
+ next if known_included_objects.key?(code)
92
+ known_included_objects[code] = inc_obj
93
+ included_records << serializer.record_hash(inc_obj)
94
+ end
95
+ end
96
+ included_records
97
+ end
98
+
99
+ def has_permitted_includes(requested_includes)
100
+ # requested includes should be within relationships defined on serializer
101
+ allowed_includes = @relationships_to_serialize.keys
102
+ intersection = allowed_includes & requested_includes
103
+ if intersection.sort == requested_includes.sort
104
+ true
105
+ else
106
+ raise ArgumentError, "One of keys from #{requested_includes} is not specified as a relationship on the serializer"
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper'
2
+ require 'active_record'
3
+ require 'sqlite3'
4
+
5
+ describe 'active record' do
6
+
7
+ # Setup DB
8
+ before(:all) do
9
+ @db_file = "test.db"
10
+
11
+ # Open a database
12
+ db = SQLite3::Database.new @db_file
13
+
14
+ # Create tables
15
+ db.execute_batch <<-SQL
16
+ create table suppliers (
17
+ name varchar(30),
18
+ id int primary key
19
+ );
20
+
21
+ create table accounts (
22
+ name varchar(30),
23
+ id int primary key,
24
+ supplier_id int,
25
+ FOREIGN KEY (supplier_id) REFERENCES suppliers(id)
26
+ );
27
+ SQL
28
+
29
+ # Insert records
30
+ @account_id = 2
31
+ @supplier_id = 1
32
+ db.execute_batch <<-SQL
33
+ insert into suppliers values ('Supplier1', #{@supplier_id});
34
+ insert into accounts values ('Dollar Account', #{@account_id}, #{@supplier_id});
35
+ SQL
36
+ end
37
+
38
+ # Setup Active Record
39
+ before(:all) do
40
+ class Supplier < ActiveRecord::Base
41
+ has_one :account
42
+ end
43
+
44
+ class Account < ActiveRecord::Base
45
+ belongs_to :supplier
46
+ end
47
+
48
+ ActiveRecord::Base.establish_connection(
49
+ :adapter => 'sqlite3',
50
+ :database => @db_file
51
+ )
52
+ end
53
+
54
+ context 'has one patch' do
55
+
56
+ it 'has account_id method for a supplier' do
57
+ expect(Supplier.first.respond_to?(:account_id)).to be true
58
+ expect(Supplier.first.account_id).to eq @account_id
59
+ end
60
+
61
+ end
62
+
63
+ # Clean up DB
64
+ after(:all) do
65
+ File.delete(@db_file) if File.exists?(@db_file)
66
+ end
67
+ end
@@ -0,0 +1,68 @@
1
+ require 'spec_helper'
2
+
3
+ describe FastJsonapi::ObjectSerializer do
4
+ include_context 'movie class'
5
+
6
+ context 'when caching has_many' do
7
+ before(:each) do
8
+ rails = OpenStruct.new
9
+ rails.cache = ActiveSupport::Cache::MemoryStore.new
10
+ stub_const('Rails', rails)
11
+ end
12
+
13
+ it 'returns correct hash when serializable_hash is called' do
14
+ options = {}
15
+ options[:meta] = { total: 2 }
16
+ options[:include] = [:actors]
17
+ serializable_hash = CachingMovieSerializer.new([movie, movie], options).serializable_hash
18
+
19
+ expect(serializable_hash[:data].length).to eq 2
20
+ expect(serializable_hash[:data][0][:relationships].length).to eq 3
21
+ expect(serializable_hash[:data][0][:attributes].length).to eq 2
22
+
23
+ expect(serializable_hash[:meta]).to be_instance_of(Hash)
24
+
25
+ expect(serializable_hash[:included]).to be_instance_of(Array)
26
+ expect(serializable_hash[:included][0]).to be_instance_of(Hash)
27
+ expect(serializable_hash[:included].length).to eq 3
28
+
29
+ serializable_hash = CachingMovieSerializer.new(movie).serializable_hash
30
+
31
+ expect(serializable_hash[:data]).to be_instance_of(Hash)
32
+ expect(serializable_hash[:meta]).to be nil
33
+ expect(serializable_hash[:included]).to be nil
34
+ end
35
+
36
+ it 'uses cached values for the record' do
37
+ previous_name = movie.name
38
+ previous_actors = movie.actors
39
+ CachingMovieSerializer.new(movie).serializable_hash
40
+
41
+ movie.name = 'should not match'
42
+ allow(movie).to receive(:actor_ids).and_return([99])
43
+
44
+ expect(previous_name).not_to eq(movie.name)
45
+ expect(previous_actors).not_to eq(movie.actors)
46
+ serializable_hash = CachingMovieSerializer.new(movie).serializable_hash
47
+
48
+ expect(serializable_hash[:data][:attributes][:name]).to eq(previous_name)
49
+ expect(serializable_hash[:data][:relationships][:actors][:data].length).to eq movie.actors.length
50
+ end
51
+
52
+ it 'uses cached values for has many as specified' do
53
+ previous_name = movie.name
54
+ previous_actors = movie.actors
55
+ CachingMovieWithHasManySerializer.new(movie).serializable_hash
56
+
57
+ movie.name = 'should not match'
58
+ allow(movie).to receive(:actor_ids).and_return([99])
59
+
60
+ expect(previous_name).not_to eq(movie.name)
61
+ expect(previous_actors).not_to eq(movie.actors)
62
+ serializable_hash = CachingMovieWithHasManySerializer.new(movie).serializable_hash
63
+
64
+ expect(serializable_hash[:data][:attributes][:name]).to eq(previous_name)
65
+ expect(serializable_hash[:data][:relationships][:actors][:data].length).to eq previous_actors.length
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,69 @@
1
+ require 'spec_helper'
2
+
3
+ describe FastJsonapi::ObjectSerializer do
4
+
5
+ include_context 'movie class'
6
+
7
+ context 'when testing class methods of object serializer' do
8
+
9
+ before(:example) do
10
+ MovieSerializer.relationships_to_serialize = {}
11
+ end
12
+
13
+ it 'returns correct relationship hash for a has_many relationship' do
14
+ MovieSerializer.has_many :roles
15
+ relationship = MovieSerializer.relationships_to_serialize[:roles]
16
+ expect(relationship).to be_instance_of(Hash)
17
+ expect(relationship.keys).to all(be_instance_of(Symbol))
18
+ expect(relationship[:id_method_name]).to end_with '_ids'
19
+ expect(relationship[:record_type]).to eq 'roles'.singularize.to_sym
20
+ end
21
+
22
+ it 'returns correct relationship hash for a has_many relationship with overrides' do
23
+ MovieSerializer.has_many :roles, id_method_name: :roles_only_ids, record_type: :super_role
24
+ relationship = MovieSerializer.relationships_to_serialize[:roles]
25
+ expect(relationship[:id_method_name]).to be :roles_only_ids
26
+ expect(relationship[:record_type]).to be :super_role
27
+ end
28
+
29
+ it 'returns correct relationship hash for a belongs_to relationship' do
30
+ MovieSerializer.belongs_to :area
31
+ relationship = MovieSerializer.relationships_to_serialize[:area]
32
+ expect(relationship).to be_instance_of(Hash)
33
+ expect(relationship.keys).to all(be_instance_of(Symbol))
34
+ expect(relationship[:id_method_name]).to end_with '_id'
35
+ expect(relationship[:record_type]).to eq 'area'.singularize.to_sym
36
+ end
37
+
38
+ it 'returns correct relationship hash for a belongs_to relationship with overrides' do
39
+ MovieSerializer.has_many :area, id_method_name: :blah_id, record_type: :awesome_area, serializer: :my_area
40
+ relationship = MovieSerializer.relationships_to_serialize[:area]
41
+ expect(relationship[:id_method_name]).to be :blah_id
42
+ expect(relationship[:record_type]).to be :awesome_area
43
+ expect(relationship[:serializer]).to be :MyAreaSerializer
44
+ end
45
+
46
+ it 'returns correct relationship hash for a has_one relationship' do
47
+ MovieSerializer.has_one :area
48
+ relationship = MovieSerializer.relationships_to_serialize[:area]
49
+ expect(relationship).to be_instance_of(Hash)
50
+ expect(relationship.keys).to all(be_instance_of(Symbol))
51
+ expect(relationship[:id_method_name]).to end_with '_id'
52
+ expect(relationship[:record_type]).to eq 'area'.singularize.to_sym
53
+ end
54
+
55
+ it 'returns correct relationship hash for a has_one relationship with overrides' do
56
+ MovieSerializer.has_one :area, id_method_name: :blah_id, record_type: :awesome_area
57
+ relationship = MovieSerializer.relationships_to_serialize[:area]
58
+ expect(relationship[:id_method_name]).to be :blah_id
59
+ expect(relationship[:record_type]).to be :awesome_area
60
+ end
61
+
62
+ it 'returns serializer name correctly with namespaces' do
63
+ AppName::V1::MovieSerializer.has_many :area, id_method_name: :blah_id
64
+ relationship = AppName::V1::MovieSerializer.relationships_to_serialize[:area]
65
+ expect(relationship[:serializer]).to be :'AppName::V1::AreaSerializer'
66
+ end
67
+ end
68
+
69
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+
3
+ describe FastJsonapi::ObjectSerializer do
4
+ include_context 'movie class'
5
+ include_context 'ams movie class'
6
+
7
+ context 'when using hyphens for word separation in the JSON API members' do
8
+ it 'returns correct hash when serializable_hash is called' do
9
+ serializable_hash = HyphenMovieSerializer.new([movie, movie]).serializable_hash
10
+ expect(serializable_hash[:data].length).to eq 2
11
+ expect(serializable_hash[:data][0][:relationships].length).to eq 3
12
+ expect(serializable_hash[:data][0][:relationships]).to have_key('movie-type'.to_sym)
13
+ expect(serializable_hash[:data][0][:attributes].length).to eq 2
14
+ expect(serializable_hash[:data][0][:attributes]).to have_key("release-year".to_sym)
15
+
16
+ serializable_hash = HyphenMovieSerializer.new(movie_struct).serializable_hash
17
+ expect(serializable_hash[:data][:relationships].length).to eq 3
18
+ expect(serializable_hash[:data][:relationships]).to have_key('movie-type'.to_sym)
19
+ expect(serializable_hash[:data][:attributes].length).to eq 2
20
+ expect(serializable_hash[:data][:attributes]).to have_key('release-year'.to_sym)
21
+ expect(serializable_hash[:data][:id]).to eq movie_struct.id.to_s
22
+ end
23
+
24
+ it 'returns same thing as ams' do
25
+ ams_movie = build_ams_movies(1).first
26
+ movie = build_movies(1).first
27
+ our_json = HyphenMovieSerializer.new([movie]).serialized_json
28
+ ams_json = ActiveModelSerializers::SerializableResource.new([ams_movie], key_transform: :dash).to_json
29
+ expect(our_json.length).to eq (ams_json.length)
30
+ end
31
+
32
+ it 'returns type hypenated when trying to serializing a class with multiple words' do
33
+ movie_type = MovieType.new
34
+ movie_type.id = 3
35
+ movie_type.name = "x"
36
+ serializable_hash = HyphenMovieTypeSerializer.new(movie_type).serializable_hash
37
+ expect(serializable_hash[:data][:type].to_sym).to eq 'movie-type'.to_sym
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,87 @@
1
+ require 'spec_helper'
2
+
3
+ describe FastJsonapi::ObjectSerializer do
4
+ include_context 'movie class'
5
+ include_context 'ams movie class'
6
+
7
+ context 'when testing performance of serialization' do
8
+ it 'should create a hash of 1000 records in less than 25 ms' do
9
+ movies = 1000.times.map { |_i| movie }
10
+ expect { MovieSerializer.new(movies).serializable_hash }.to perform_under(25).ms
11
+ end
12
+
13
+ it 'should serialize 1000 records to jsonapi in less than 30 ms' do
14
+ movies = 1000.times.map { |_i| movie }
15
+ expect { MovieSerializer.new(movies).serialized_json }.to perform_under(30).ms
16
+ end
17
+
18
+ it 'should create a hash of 1000 records with includes and meta in less than 75 ms' do
19
+ count = 1000
20
+ movies = count.times.map { |_i| movie }
21
+ options = {}
22
+ options[:meta] = { total: count }
23
+ options[:include] = [:actors]
24
+ expect { MovieSerializer.new(movies, options).serializable_hash }.to perform_under(75).ms
25
+ end
26
+
27
+ it 'should serialize 1000 records to jsonapi with includes and meta in less than 75 ms' do
28
+ count = 1000
29
+ movies = count.times.map { |_i| movie }
30
+ options = {}
31
+ options[:meta] = { total: count }
32
+ options[:include] = [:actors]
33
+ expect { MovieSerializer.new(movies, options).serialized_json }.to perform_under(75).ms
34
+ end
35
+ end
36
+
37
+ def print_stats(count, ams_time, our_time)
38
+ format = '%-15s %-10s %s'
39
+ puts ''
40
+ puts format(format, 'Serializer', 'Records', 'Time')
41
+ puts format(format, 'AMS serializer', count, ams_time.round(2).to_s + ' ms')
42
+ puts format(format, 'Fast serializer', count, our_time.round(2).to_s + ' ms')
43
+ end
44
+
45
+ context 'when comparing with AMS 0.10.x' do
46
+ [1, 25, 250, 1000].each do |movie_count|
47
+ speed_factor = 25
48
+ it "should serialize #{movie_count} records atleast #{speed_factor} times faster than AMS" do
49
+ ams_movies = build_ams_movies(movie_count)
50
+ movies = build_movies(movie_count)
51
+ our_json = nil
52
+ ams_json = nil
53
+ our_serializer = MovieSerializer.new(movies)
54
+ ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_movies)
55
+ our_time = Benchmark.measure { our_json = our_serializer.serialized_json }.real * 1000
56
+ ams_time = Benchmark.measure { ams_json = ams_serializer.to_json }.real * 1000
57
+ print_stats(movie_count, ams_time, our_time)
58
+ expect(our_json.length).to eq ams_json.length
59
+ expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times
60
+ end
61
+ end
62
+ end
63
+
64
+ context 'when comparing with AMS 0.10.x and with includes and meta' do
65
+ [1, 25, 250, 1000].each do |movie_count|
66
+ speed_factor = 25
67
+ it "should serialize #{movie_count} records atleast #{speed_factor} times faster than AMS" do
68
+ ams_movies = build_ams_movies(movie_count)
69
+ movies = build_movies(movie_count)
70
+ our_json = nil
71
+ ams_json = nil
72
+
73
+ options = {}
74
+ options[:meta] = { total: movie_count }
75
+ options[:include] = [:actors, :movie_type]
76
+
77
+ our_serializer = MovieSerializer.new(movies, options)
78
+ ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_movies, include: options[:include], meta: options[:meta])
79
+ our_time = Benchmark.measure { our_json = our_serializer.serialized_json }.real * 1000
80
+ ams_time = Benchmark.measure { ams_json = ams_serializer.to_json }.real * 1000
81
+ print_stats(movie_count, ams_time, our_time)
82
+ expect(our_json.length).to eq ams_json.length
83
+ expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times
84
+ end
85
+ end
86
+ end
87
+ end