active_loaders 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'active_loaders/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "active_loaders"
8
+ spec.version = ActiveLoaders::VERSION
9
+ spec.authors = ["Jan Berdajs"]
10
+ spec.email = ["mrbrdo@gmail.com"]
11
+ spec.summary = %q{Ruby library to automatically preload data for your Active Model Serializers}
12
+ spec.homepage = "https://github.com/kundi/active_loaders"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency 'active_model_serializers', '~> 0.9'
21
+ spec.add_dependency 'datasource', '~> 0.3'
22
+ spec.add_development_dependency "bundler", "~> 1.6"
23
+ spec.add_development_dependency "rake"
24
+ spec.add_development_dependency "rspec", "~> 3.2"
25
+ spec.add_development_dependency 'sqlite3', '~> 1.3'
26
+ spec.add_development_dependency 'activerecord', '~> 4'
27
+ spec.add_development_dependency 'pry', '~> 0.9'
28
+ spec.add_development_dependency 'sequel', '~> 4.17'
29
+ spec.add_development_dependency 'database_cleaner', '~> 1.3'
30
+ end
@@ -0,0 +1,6 @@
1
+ require "active_loaders/version"
2
+ require "active_loaders/datasource_adapter"
3
+
4
+ module ActiveLoaders
5
+
6
+ end
@@ -0,0 +1,216 @@
1
+ require "active_model/serializer"
2
+ require "datasource"
3
+
4
+ module ActiveLoaders
5
+ module Adapters
6
+ module ActiveModelSerializers
7
+ module ArraySerializer
8
+ def initialize_with_loaders(objects, options = {})
9
+ datasource_class = options.delete(:datasource)
10
+ adapter = Datasource.orm_adapters.find { |a| a.is_scope?(objects) }
11
+ if adapter && !adapter.scope_loaded?(objects)
12
+ scope = begin
13
+ objects
14
+ .for_serializer(options[:serializer])
15
+ .datasource_params(*[options[:loader_params]].compact)
16
+ rescue NameError
17
+ if options[:serializer].nil?
18
+ return initialize_without_loaders(objects, options)
19
+ else
20
+ raise
21
+ end
22
+ end
23
+
24
+ if datasource_class
25
+ scope = scope.with_datasource(datasource_class)
26
+ end
27
+
28
+ records = adapter.scope_to_records(scope)
29
+
30
+ # if we are loading an association proxy, we should set the target
31
+ # especially because AMS will resolve it twice, which would do 2 queries
32
+ if objects.respond_to?(:proxy_association)
33
+ objects.proxy_association.target = records
34
+ end
35
+
36
+ initialize_without_loaders(records, options)
37
+ else
38
+ initialize_without_loaders(objects, options)
39
+ end
40
+ end
41
+ end
42
+
43
+ module_function
44
+ def get_serializer_for(klass, serializer_assoc = nil)
45
+ serializer = if serializer_assoc
46
+ if serializer_assoc.kind_of?(Hash)
47
+ serializer_assoc[:options].try(:[], :serializer)
48
+ else
49
+ serializer_assoc.options[:serializer]
50
+ end
51
+ end
52
+ serializer || "#{klass.name}Serializer".constantize
53
+ end
54
+
55
+ def to_datasource_select(result, klass, serializer = nil, serializer_assoc = nil, adapter = nil, datasource = nil)
56
+ adapter ||= Datasource::Base.default_adapter
57
+ serializer ||= get_serializer_for(klass, serializer_assoc)
58
+ if serializer._attributes.respond_to?(:keys) # AMS 0.8
59
+ result.concat(serializer._attributes.keys)
60
+ else # AMS 0.9
61
+ result.concat(serializer._attributes)
62
+ end
63
+ result.concat(serializer.loaders_context.select)
64
+ if serializer.loaders_context.skip_select.empty?
65
+ result.unshift("*")
66
+ else
67
+ datasource_class = if datasource
68
+ datasource.class
69
+ else
70
+ serializer.use_datasource || klass.default_datasource
71
+ end
72
+ result.concat(datasource_class._column_attribute_names -
73
+ serializer.loaders_context.skip_select.map(&:to_s))
74
+ end
75
+ result_assocs = serializer.loaders_context.includes.dup
76
+ result.push(result_assocs)
77
+
78
+ serializer._associations.each_pair do |name, serializer_assoc|
79
+ # TODO: what if assoc is renamed in serializer?
80
+ reflection = adapter.association_reflection(klass, name.to_sym)
81
+ assoc_class = reflection[:klass]
82
+
83
+ name = name.to_s
84
+ result_assocs[name] = []
85
+ to_datasource_select(result_assocs[name], assoc_class, nil, serializer_assoc, adapter)
86
+ end
87
+ rescue Exception => ex
88
+ if ex.is_a?(SystemStackError) || ex.is_a?(Datasource::RecursionError)
89
+ fail Datasource::RecursionError, "recursive association (involving #{klass.name})"
90
+ else
91
+ raise
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ module SerializerClassMethods
99
+ class SerializerDatasourceContext
100
+ def initialize(serializer)
101
+ @serializer = serializer
102
+ end
103
+
104
+ def select(*args)
105
+ @datasource_select ||= []
106
+ @datasource_select.concat(args)
107
+
108
+ @datasource_select
109
+ end
110
+
111
+ def skip_select(*args)
112
+ @datasource_skip_select ||= []
113
+ @datasource_skip_select.concat(args)
114
+
115
+ @datasource_skip_select
116
+ end
117
+
118
+ def includes(*args)
119
+ @datasource_includes ||= {}
120
+
121
+ args.each do |arg|
122
+ @datasource_includes.deep_merge!(datasource_includes_to_select(arg))
123
+ end
124
+
125
+ @datasource_includes
126
+ end
127
+
128
+ def use_datasource(*args)
129
+ @serializer.use_datasource(*args)
130
+ end
131
+
132
+ private
133
+ def datasource_includes_to_select(arg)
134
+ if arg.kind_of?(Hash)
135
+ arg.keys.inject({}) do |memo, key|
136
+ memo[key.to_sym] = ["*", datasource_includes_to_select(arg[key])]
137
+ memo
138
+ end
139
+ elsif arg.kind_of?(Array)
140
+ arg.inject({}) do |memo, element|
141
+ memo.deep_merge!(datasource_includes_to_select(element))
142
+ end
143
+ elsif arg.respond_to?(:to_sym)
144
+ { arg.to_sym => ["*"] }
145
+ else
146
+ fail Datasource::Error, "unknown includes value type #{arg.class}"
147
+ end
148
+ end
149
+ end
150
+
151
+ def inherited(base)
152
+ select_values = loaders_context.select.deep_dup
153
+ skip_select_values = loaders_context.skip_select.deep_dup
154
+ includes_values = loaders_context.includes.deep_dup
155
+ base.loaders do
156
+ select(*select_values)
157
+ skip_select(*skip_select_values)
158
+ includes(*includes_values)
159
+ end
160
+ base.use_datasource(use_datasource)
161
+
162
+ super
163
+ end
164
+
165
+ def loaders_context
166
+ @loaders_context ||= SerializerDatasourceContext.new(self)
167
+ end
168
+
169
+ def loaders(&block)
170
+ loaders_context.instance_eval(&block)
171
+ end
172
+
173
+ # required by datasource gem
174
+ def datasource_adapter
175
+ ActiveLoaders::Adapters::ActiveModelSerializers
176
+ end
177
+
178
+ # required by datasource gem
179
+ def use_datasource(*args)
180
+ @use_datasource = args.first unless args.empty?
181
+ @use_datasource
182
+ end
183
+ end
184
+
185
+ module SerializerInstanceMethods
186
+ def initialize(object, options={}, *args)
187
+ if object && object.respond_to?(:for_serializer)
188
+ # single record
189
+ datasource_class = options.delete(:datasource)
190
+ record = object.for_serializer(self.class, datasource_class) do |scope|
191
+ scope.datasource_params(*[options[:loader_params]].compact)
192
+ end
193
+ super(record, options, *args)
194
+ else
195
+ super
196
+ end
197
+ end
198
+ end
199
+
200
+ array_serializer_class = if defined?(ActiveModel::Serializer::ArraySerializer)
201
+ ActiveModel::Serializer::ArraySerializer
202
+ else
203
+ ActiveModel::ArraySerializer
204
+ end
205
+
206
+ array_serializer_class.class_exec do
207
+ alias_method :initialize_without_loaders, :initialize
208
+ include ActiveLoaders::Adapters::ActiveModelSerializers::ArraySerializer
209
+ def initialize(*args)
210
+ initialize_with_loaders(*args)
211
+ end
212
+ end
213
+
214
+ ActiveModel::Serializer.singleton_class.send :prepend, SerializerClassMethods
215
+ ActiveModel::Serializer.send :prepend, SerializerInstanceMethods
216
+ Datasource::Base.default_consumer_adapter ||= ActiveLoaders::Adapters::ActiveModelSerializers
@@ -0,0 +1,100 @@
1
+ require 'set'
2
+
3
+ module ActiveLoaders
4
+ module Test
5
+ Error = Class.new(StandardError)
6
+ def test_serializer_queries(serializer_klass, model_klass, ignore_columns: [], skip_columns_check: false, allow_queries_per_record: 0)
7
+ records = get_all_records(model_klass, serializer_klass)
8
+ fail "Not enough records to test #{serializer_klass}. Create at least 1 #{model_klass}." unless records.size > 0
9
+
10
+ records.each do |record|
11
+ queries = get_executed_queries do
12
+ serializer_klass.new(record).as_json
13
+ end
14
+
15
+ unless queries.size == allow_queries_per_record
16
+ fail Error, "unexpected queries\n\nRecord:\n#{record.inspect}\n\nQueries:\n#{queries.join("\n")}"
17
+ end
18
+ end
19
+
20
+ # just for good measure
21
+ queries = get_executed_queries do
22
+ ActiveModel::ArraySerializer.new(records, each_serializer: serializer_klass).as_json
23
+ end
24
+ unless queries.size == (records.size * allow_queries_per_record)
25
+ fail Error, "unexpected queries when using ArraySerializer\n\nModel:\n#{model_klass}\n\nQueries:\n#{queries.join("\n")}"
26
+ end
27
+
28
+ # select values (if supported)
29
+ # TODO: Sequel?
30
+ unless skip_columns_check
31
+ if defined?(ActiveRecord::Base) && model_klass.ancestors.include?(ActiveRecord::Base)
32
+ if records.first.respond_to?(:accessed_fields)
33
+ accessed_fields = Set.new
34
+ records.each { |record| accessed_fields.merge(record.accessed_fields) }
35
+
36
+ unaccessed_columns = model_klass.column_names - accessed_fields.to_a - ignore_columns.map(&:to_s)
37
+
38
+ unless unaccessed_columns.empty?
39
+ unaccessed_columns_str = unaccessed_columns.join(", ")
40
+ unaccessed_columns_syms = unaccessed_columns.map { |c| ":#{c}" }.join(", ")
41
+ all_unaccessed_columns_syms = (ignore_columns.map(&:to_s) + unaccessed_columns).map { |c| ":#{c}" }.join(", ")
42
+ fail Error, "unnecessary select for #{model_klass} columns: #{unaccessed_columns_str}\n\nAdd to #{serializer_klass} loaders block:\n skip_select #{unaccessed_columns_syms}\n\nOr ignore this error with:\n test_serializer_queries(#{serializer_klass}, #{model_klass}, ignore_columns: [#{all_unaccessed_columns_syms}])\n\nOr skip this columns check entirely:\n test_serializer_queries(#{serializer_klass}, #{model_klass}, skip_columns_check: true)"
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ (@active_loaders_tested_serializers ||= Set.new).add(serializer_klass)
49
+ end
50
+
51
+ def assert_all_serializers_tested(namespace = nil)
52
+ descendants =
53
+ ObjectSpace.each_object(Class)
54
+ .select { |klass| klass < ActiveModel::Serializer }
55
+ .select { |klass| (namespace.nil? && !klass.name.include?("::")) || klass.name.starts_with?("#{namespace}::") }
56
+ .reject { |klass| Array(@active_loaders_tested_serializers).include?(klass) }
57
+
58
+ unless descendants.empty?
59
+ fail Error, "serializers not tested: #{descendants.map(&:name).join(", ")}"
60
+ end
61
+ end
62
+
63
+ private
64
+ def get_all_records(model_klass, serializer_klass)
65
+ if defined?(ActiveRecord::Base) && model_klass.ancestors.include?(ActiveRecord::Base)
66
+ model_klass.for_serializer(serializer_klass).to_a
67
+ elsif defined?(Sequel::Model) && model_klass.ancestors.include?(Sequel::Model)
68
+ model_klass.for_serializer(serializer_klass).all
69
+ else
70
+ fail "Unknown model #{model_klass} of type #{model_klass.superclass}."
71
+ end
72
+ end
73
+
74
+ def get_executed_queries
75
+ logger_io = StringIO.new
76
+ logger = Logger.new(logger_io)
77
+ logger.formatter = ->(severity, datetime, progname, msg) { "#{msg}\n" }
78
+ if defined?(ActiveRecord::Base)
79
+ ar_old_logger = ActiveRecord::Base.logger
80
+ ActiveRecord::Base.logger = logger
81
+ end
82
+ if defined?(Sequel::Model)
83
+ Sequel::Model.db.loggers << logger
84
+ end
85
+
86
+ begin
87
+ yield
88
+ ensure
89
+ if defined?(ActiveRecord::Base)
90
+ ActiveRecord::Base.logger = ar_old_logger
91
+ end
92
+ if defined?(Sequel::Model)
93
+ Sequel::Model.db.loggers.delete(logger)
94
+ end
95
+ end
96
+
97
+ logger_io.string.lines.reject { |line| line.strip == "" }
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,3 @@
1
+ module ActiveLoaders
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+
3
+ module SequelSerializerSpec
4
+ describe "Serializer (Sequel)", :sequel do
5
+ class Comment < Sequel::Model
6
+ many_to_one :post
7
+ end
8
+
9
+ class Post < Sequel::Model
10
+ many_to_one :blog
11
+ one_to_many :comments
12
+
13
+ datasource_module do
14
+ query :author_name do
15
+ "posts.author_first_name || ' ' || posts.author_last_name"
16
+ end
17
+ end
18
+ end
19
+
20
+ class Blog < Sequel::Model
21
+ one_to_many :posts
22
+ end
23
+
24
+ class CommentSerializer < ActiveModel::Serializer
25
+ attributes :id, :comment
26
+ end
27
+
28
+ class PostSerializer < ActiveModel::Serializer
29
+ attributes :id, :title, :author_name
30
+ has_many :comments, each_serializer: CommentSerializer
31
+
32
+ def author_name
33
+ object.values[:author_name]
34
+ end
35
+ end
36
+
37
+ class BlogSerializer < ActiveModel::Serializer
38
+ attributes :id, :title
39
+
40
+ has_many :posts, each_serializer: PostSerializer
41
+ end
42
+
43
+ it "returns serialized hash" do
44
+ blog = Blog.create title: "Blog 1"
45
+ post = Post.create blog_id: blog.id, title: "Post 1", author_first_name: "John", author_last_name: "Doe"
46
+ Comment.create(post_id: post.id, comment: "Comment 1")
47
+ post = Post.create blog_id: blog.id, title: "Post 2", author_first_name: "Maria", author_last_name: "Doe"
48
+ Comment.create(post_id: post.id, comment: "Comment 2")
49
+ blog = Blog.create title: "Blog 2"
50
+
51
+ expected_result = [
52
+ {:id =>1, :title =>"Blog 1", :posts =>[
53
+ {:id =>1, :title =>"Post 1", :author_name =>"John Doe", comments: [{:id =>1, :comment =>"Comment 1"}]},
54
+ {:id =>2, :title =>"Post 2", :author_name =>"Maria Doe", comments: [{:id =>2, :comment =>"Comment 2"}]}
55
+ ]},
56
+ {:id =>2, :title =>"Blog 2", :posts =>[]}
57
+ ]
58
+
59
+ expect_query_count(3) do
60
+ serializer = ActiveModel::ArraySerializer.new(Blog.where, each_serializer: BlogSerializer)
61
+ expect(expected_result).to eq(serializer.as_json)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,77 @@
1
+ require 'spec_helper'
2
+
3
+ module SequelSkipSelectSpec
4
+ describe "skip_select (Sequel)", :sequel do
5
+ class Comment < Sequel::Model
6
+ many_to_one :post
7
+ end
8
+
9
+ class Post < Sequel::Model
10
+ many_to_one :blog
11
+ one_to_many :comments
12
+
13
+ datasource_module do
14
+ query :author_name do
15
+ "posts.author_first_name || ' ' || posts.author_last_name"
16
+ end
17
+ end
18
+ end
19
+
20
+ class Blog < Sequel::Model
21
+ one_to_many :posts
22
+ end
23
+
24
+ class CommentSerializer < ActiveModel::Serializer
25
+ attributes :id, :comment
26
+ end
27
+
28
+ class PostSerializer < ActiveModel::Serializer
29
+ attributes :id, :title, :author_name
30
+ has_many :comments, each_serializer: CommentSerializer
31
+
32
+ loaders do
33
+ skip_select :author_first_name, :author_last_name
34
+ end
35
+
36
+ def author_name
37
+ object.values[:author_name]
38
+ end
39
+ end
40
+
41
+ class BlogSerializer < ActiveModel::Serializer
42
+ attributes :id, :title
43
+
44
+ has_many :posts, each_serializer: PostSerializer
45
+ end
46
+
47
+ it "returns serialized hash" do
48
+ blog = Blog.create title: "Blog 1"
49
+ post = Post.create blog_id: blog.id, title: "Post 1", author_first_name: "John", author_last_name: "Doe"
50
+ Comment.create(post_id: post.id, comment: "Comment 1")
51
+ post = Post.create blog_id: blog.id, title: "Post 2", author_first_name: "Maria", author_last_name: "Doe"
52
+ Comment.create(post_id: post.id, comment: "Comment 2")
53
+ blog = Blog.create title: "Blog 2"
54
+
55
+ expected_result = [
56
+ {:id =>1, :title =>"Blog 1", :posts =>[
57
+ {:id =>1, :title =>"Post 1", :author_name =>"John Doe", comments: [{:id =>1, :comment =>"Comment 1"}]},
58
+ {:id =>2, :title =>"Post 2", :author_name =>"Maria Doe", comments: [{:id =>2, :comment =>"Comment 2"}]}
59
+ ]},
60
+ {:id =>2, :title =>"Blog 2", :posts =>[]}
61
+ ]
62
+
63
+ expect_query_count(3) do |logger|
64
+ serializer = ActiveModel::ArraySerializer.new(Blog.where, each_serializer: BlogSerializer)
65
+ expect(expected_result).to eq(serializer.as_json)
66
+ expect(logger.string.lines[0]).to include("blogs.*")
67
+ expect(logger.string.lines[1]).to_not include("posts.*")
68
+ expect(logger.string.lines[1]).to_not include("posts.author_first_name,")
69
+ expect(logger.string.lines[1]).to_not include("posts.author_last_name,")
70
+ expect(logger.string.lines[1]).to include("posts.id")
71
+ expect(logger.string.lines[1]).to include("posts.title")
72
+ expect(logger.string.lines[1]).to include("posts.blog_id")
73
+ expect(logger.string.lines[2]).to include("comments.*")
74
+ end
75
+ end
76
+ end
77
+ end