miscellany 0.1.2 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/miscellany/active_record/arbitrary_prefetch.rb +40 -17
- data/lib/miscellany/active_record/batched_destruction.rb +0 -1
- data/lib/miscellany/active_record/computed_columns.rb +107 -0
- data/lib/miscellany/batch_processor.rb +7 -3
- data/lib/miscellany/controller/sliced_response.rb +35 -8
- data/lib/miscellany/param_validator.rb +133 -86
- data/lib/miscellany/version.rb +1 -1
- data/lib/miscellany.rb +8 -0
- data/miscellany.gemspec +8 -17
- data/spec/db/database.yml +3 -0
- data/spec/miscellany/arbitrary_prefetch_spec.rb +69 -0
- data/spec/miscellany/computed_columns_spec.rb +62 -0
- data/spec/miscellany/param_validator_spec.rb +295 -0
- data/spec/spec_helper.rb +42 -0
- metadata +38 -179
- data/app/views/miscellany/spa_page.html.erb +0 -2
- data/config/initializers/01_custom_preloaders.rb +0 -2
- data/config/initializers/arbitrary_prefetch.rb +0 -1
- data/lib/miscellany/controller/spa_render.rb +0 -18
data/lib/miscellany.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
|
2
|
+
require "active_support/lazy_load_hooks"
|
3
|
+
|
2
4
|
Dir[File.dirname(__FILE__) + "/miscellany/**/*.rb"].each { |file| require file }
|
3
5
|
|
4
6
|
module Miscellany
|
@@ -7,4 +9,10 @@ module Miscellany
|
|
7
9
|
class Engine < ::Rails::Engine
|
8
10
|
end
|
9
11
|
end
|
12
|
+
|
13
|
+
ActiveSupport.on_load(:active_record) do
|
14
|
+
Miscellany::CustomPreloaders.install
|
15
|
+
Miscellany::ArbitraryPrefetch.install
|
16
|
+
Miscellany::ComputedColumns.install
|
17
|
+
end
|
10
18
|
end
|
data/miscellany.gemspec
CHANGED
@@ -22,22 +22,13 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.test_files = Dir["spec/**/*"]
|
23
23
|
spec.require_paths = ['lib']
|
24
24
|
|
25
|
-
spec.
|
26
|
-
spec.
|
27
|
-
spec.
|
28
|
-
spec.add_development_dependency "rspec-rails"
|
29
|
-
spec.add_development_dependency "pg"
|
30
|
-
spec.add_development_dependency "factory"
|
31
|
-
spec.add_development_dependency "factory_bot"
|
32
|
-
spec.add_development_dependency "timecop"
|
33
|
-
spec.add_development_dependency "webmock"
|
34
|
-
spec.add_development_dependency "sinatra", ">= 0"
|
35
|
-
spec.add_development_dependency "shoulda-matchers"
|
36
|
-
spec.add_development_dependency "yard"
|
37
|
-
spec.add_development_dependency "pry"
|
38
|
-
spec.add_development_dependency "pry-nav"
|
39
|
-
spec.add_development_dependency "rubocop"
|
25
|
+
spec.add_dependency 'rails', '>= 5', '< 6.3'
|
26
|
+
# spec.add_dependency 'activerecord', '>= 5', '< 6.3'
|
27
|
+
# spec.add_dependency 'activesupport', '>= 5', '< 6.3'
|
40
28
|
|
41
|
-
spec.
|
42
|
-
spec.
|
29
|
+
spec.add_development_dependency 'rake'
|
30
|
+
spec.add_development_dependency 'database_cleaner', '>= 1.2'
|
31
|
+
spec.add_development_dependency 'rspec', '~> 3'
|
32
|
+
spec.add_development_dependency 'sqlite3', '~> 1.3'
|
33
|
+
spec.add_development_dependency 'with_model'
|
43
34
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Miscellany::ArbitraryPrefetch do
|
5
|
+
with_model :Post do
|
6
|
+
table do |t|
|
7
|
+
t.string :title
|
8
|
+
t.timestamps null: false
|
9
|
+
end
|
10
|
+
|
11
|
+
model do
|
12
|
+
has_many :comments
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
with_model :Comment do
|
17
|
+
table do |t|
|
18
|
+
t.belongs_to :post
|
19
|
+
t.string :title
|
20
|
+
t.boolean :favorite
|
21
|
+
t.timestamps null: false
|
22
|
+
end
|
23
|
+
|
24
|
+
model do
|
25
|
+
belongs_to :post
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
let!(:posts) { 10.times.map{|i| Post.create!(title: "Post #{i}") } }
|
30
|
+
|
31
|
+
before :each do
|
32
|
+
posts.each do |p|
|
33
|
+
5.times{|i| p.comments.create!(title: "#{p.title} Comment #{i}") }
|
34
|
+
p.comments.last.update!(favorite: true)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'generally works' do
|
39
|
+
posts = Post.prefetch(favorite_comment: Comment.where(favorite: true))
|
40
|
+
expect(posts.count).to eq 10
|
41
|
+
expect(posts[0].favorite_comment).to be
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'prefetch is singluar' do
|
45
|
+
it 'returns a single object' do
|
46
|
+
posts = Post.prefetch(favorite_comment: Comment.where(favorite: true))
|
47
|
+
expect(posts[0].favorite_comment).to be_a Comment
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'with multiple items returns a single object' do
|
51
|
+
posts = Post.prefetch(favorite_comment: Comment.where(favorite: nil))
|
52
|
+
expect(posts[0].favorite_comment).to be_a Comment
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'prefetch is plural' do
|
57
|
+
it 'returns an Array' do
|
58
|
+
posts = Post.prefetch(non_favorite_comments: Comment.where(favorite: nil))
|
59
|
+
expect(posts[0].non_favorite_comments).to respond_to :[]
|
60
|
+
expect(posts[0].non_favorite_comments.length).to eq 4
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'with 1 item returns an Array' do
|
64
|
+
posts = Post.prefetch(non_favorite_comments: Comment.where(favorite: true))
|
65
|
+
expect(posts[0].non_favorite_comments).to respond_to :[]
|
66
|
+
expect(posts[0].non_favorite_comments.length).to eq 1
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Miscellany::ComputedColumns do
|
5
|
+
with_model :Post do
|
6
|
+
table do |t|
|
7
|
+
t.string :title
|
8
|
+
t.timestamps null: false
|
9
|
+
end
|
10
|
+
|
11
|
+
model do
|
12
|
+
has_many :comments
|
13
|
+
|
14
|
+
define_computed :favorite_comments_count, ->() {
|
15
|
+
select "COMPUTED.count AS favorite_comments_count"
|
16
|
+
|
17
|
+
query do
|
18
|
+
Comment.select(<<~SQL)
|
19
|
+
post_id AS id,
|
20
|
+
count(*) AS count
|
21
|
+
SQL
|
22
|
+
.group(:post_id)
|
23
|
+
.where(favorite: true)
|
24
|
+
end
|
25
|
+
}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
with_model :Comment do
|
30
|
+
table do |t|
|
31
|
+
t.belongs_to :post
|
32
|
+
t.string :title
|
33
|
+
t.boolean :favorite
|
34
|
+
t.timestamps null: false
|
35
|
+
end
|
36
|
+
|
37
|
+
model do
|
38
|
+
belongs_to :post
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
let!(:posts) { 3.times.map{|i| Post.create!(title: "Post #{i}") } }
|
43
|
+
|
44
|
+
before :each do
|
45
|
+
posts.each do |p|
|
46
|
+
5.times{|i| p.comments.create!(title: "#{p.title} Comment #{i}") }
|
47
|
+
p.comments.last.update!(favorite: true)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'generally works' do
|
52
|
+
ActiveRecord::Base.verbose_query_logs = true
|
53
|
+
posts = Post.with_computed(:favorite_comments_count)
|
54
|
+
expect(posts.except(:select).count).to eq 3
|
55
|
+
expect(posts[0].favorite_comments_count).to eq 1
|
56
|
+
|
57
|
+
Comment.update_all(favorite: true)
|
58
|
+
posts = Post.with_computed(:favorite_comments_count)
|
59
|
+
expect(posts.except(:select).count).to eq 3
|
60
|
+
expect(posts[0].favorite_comments_count).to eq 5
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,295 @@
|
|
1
|
+
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Miscellany::ParamValidator do
|
5
|
+
let!(:value) do
|
6
|
+
{
|
7
|
+
int_array: [1,2,3],
|
8
|
+
string_array: %w[A B C],
|
9
|
+
some_number: 2,
|
10
|
+
some_string: "Robert",
|
11
|
+
specified: nil,
|
12
|
+
array: [
|
13
|
+
{a: 2},
|
14
|
+
{a: 3},
|
15
|
+
],
|
16
|
+
hash: {
|
17
|
+
nested_hash: {
|
18
|
+
value: 1,
|
19
|
+
}
|
20
|
+
},
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
# TODO transform:
|
25
|
+
|
26
|
+
def expect_valid(&blk)
|
27
|
+
result = Miscellany::ParamValidator.check(value, &blk)
|
28
|
+
expect(result.serialize).to be_nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def expect_invalid(&blk)
|
32
|
+
result = Miscellany::ParamValidator.check(value, &blk)
|
33
|
+
expect(result.serialize).to be_present
|
34
|
+
end
|
35
|
+
|
36
|
+
def expect_coercion(raw, type, expectation)
|
37
|
+
result = Miscellany::ParamValidator.assert({ value: raw }, handle: ->(v){ raise 'Invalid' }) do
|
38
|
+
p :value, type: type
|
39
|
+
end
|
40
|
+
expect(result[:value]).to eq expectation
|
41
|
+
end
|
42
|
+
|
43
|
+
describe 'default:' do
|
44
|
+
it 'sets a default value' do
|
45
|
+
result = Miscellany::ParamValidator.assert({ }, handle: ->(v){ raise 'Invalid' }) do
|
46
|
+
p :value, default: 'HIA'
|
47
|
+
end
|
48
|
+
expect(result[:value]).to eq 'HIA'
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'works deep' do
|
52
|
+
result = Miscellany::ParamValidator.assert({ value: {} }, handle: ->(v){ raise 'Invalid' }) do
|
53
|
+
p :value do |x|
|
54
|
+
p :value, default: 'Hia'
|
55
|
+
end
|
56
|
+
end
|
57
|
+
expect(result).to eq ({ value: { value: 'Hia' } })
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe 'specified' do
|
62
|
+
it 'passes a given value' do
|
63
|
+
expect_valid do
|
64
|
+
p :some_string, :specified
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'passes a given nil' do
|
69
|
+
expect_valid do
|
70
|
+
p :specified, :specified
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'fails an unspecified key' do
|
75
|
+
expect_invalid do
|
76
|
+
p :not_specified, :specified
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe 'present' do
|
82
|
+
it 'passes a given value' do
|
83
|
+
expect_valid do
|
84
|
+
p :some_string, :present
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'fails a given nil' do
|
89
|
+
expect_invalid do
|
90
|
+
p :specified, :present
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'fails an unspecified key' do
|
95
|
+
expect_invalid do
|
96
|
+
p :not_specified, :present
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe 'type:' do
|
102
|
+
it 'passes based on type' do
|
103
|
+
expect_valid do
|
104
|
+
p :some_number, type: Numeric
|
105
|
+
p :some_string, type: String
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'fails based on type' do
|
110
|
+
expect_invalid do
|
111
|
+
p :some_number, type: String
|
112
|
+
p :some_string, type: Numeric
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe ':bool' do
|
117
|
+
it 'transforms booleans' do
|
118
|
+
expect_coercion('t', :bool, true)
|
119
|
+
expect_coercion('T', :bool, true)
|
120
|
+
expect_coercion('true', :bool, true)
|
121
|
+
expect_coercion('True', :bool, true)
|
122
|
+
expect_coercion('TRUE', :bool, true)
|
123
|
+
expect_coercion('YES', :bool, true)
|
124
|
+
expect_coercion('yes', :bool, true)
|
125
|
+
expect_coercion('Y', :bool, true)
|
126
|
+
expect_coercion('y', :bool, true)
|
127
|
+
expect_coercion('1', :bool, true)
|
128
|
+
expect_coercion(1, :bool, true)
|
129
|
+
|
130
|
+
expect_coercion('f', :bool, false)
|
131
|
+
expect_coercion('F', :bool, false)
|
132
|
+
expect_coercion('false', :bool, false)
|
133
|
+
expect_coercion('False', :bool, false)
|
134
|
+
expect_coercion('FALSE', :bool, false)
|
135
|
+
expect_coercion('NO', :bool, false)
|
136
|
+
expect_coercion('no', :bool, false)
|
137
|
+
expect_coercion('N', :bool, false)
|
138
|
+
expect_coercion('n', :bool, false)
|
139
|
+
expect_coercion('0', :bool, false)
|
140
|
+
expect_coercion(0, :bool, false)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
describe 'in:' do
|
146
|
+
it 'works' do
|
147
|
+
expect_valid do
|
148
|
+
p :some_number, in: [1, 2]
|
149
|
+
end
|
150
|
+
|
151
|
+
expect_invalid do
|
152
|
+
p :some_number, in: [1, 3]
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'works with modifiers' do
|
157
|
+
expect_valid do
|
158
|
+
p [:some_number, :some_string], one_in: [2]
|
159
|
+
p [:some_number, :some_string], one_in: ['Robert']
|
160
|
+
p [:some_number, :some_string], none_in: ['Steve', 3]
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
describe 'pattern:' do
|
166
|
+
it 'works' do
|
167
|
+
expect_valid do
|
168
|
+
p :some_string, pattern: /^Rob/
|
169
|
+
p :some_string, pattern: /^Robert$/
|
170
|
+
end
|
171
|
+
expect_invalid do
|
172
|
+
p :some_string, pattern: /^Steve$/
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
describe 'items:' do
|
178
|
+
it 'works when given a Lambda' do
|
179
|
+
expect_valid do
|
180
|
+
p :array, items: ->(*args) {
|
181
|
+
p :a, in: [2, 3]
|
182
|
+
nil
|
183
|
+
}
|
184
|
+
end
|
185
|
+
expect_invalid do
|
186
|
+
p :array, items: ->(*args) {
|
187
|
+
p :a, in: [5]
|
188
|
+
}
|
189
|
+
end
|
190
|
+
expect_invalid do
|
191
|
+
p :array, items: ->(*args) {
|
192
|
+
'bob'
|
193
|
+
}
|
194
|
+
end
|
195
|
+
end
|
196
|
+
it 'works when given a Hash' do
|
197
|
+
# TODO
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'supports a custom validator block' do
|
202
|
+
expect_valid do
|
203
|
+
p :some_string do |v|
|
204
|
+
nil
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
expect_invalid do
|
209
|
+
p :some_string do |v|
|
210
|
+
'Bad Length'
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
describe 'modifiers' do
|
216
|
+
let!(:value) do
|
217
|
+
{
|
218
|
+
a: 1,
|
219
|
+
b: 1,
|
220
|
+
c: 1,
|
221
|
+
x: nil,
|
222
|
+
y: nil,
|
223
|
+
z: nil,
|
224
|
+
}
|
225
|
+
end
|
226
|
+
|
227
|
+
def assert_modifier(modifier, keys, exp)
|
228
|
+
blk = ->(*args) {
|
229
|
+
p keys, :"#{modifier}_present"
|
230
|
+
}
|
231
|
+
exp ? expect_valid(&blk) : expect_invalid(&blk)
|
232
|
+
end
|
233
|
+
|
234
|
+
it 'the all modifier works as expected' do
|
235
|
+
assert_modifier(:all, %i[a b c], true)
|
236
|
+
assert_modifier(:all, %i[a b z], false)
|
237
|
+
assert_modifier(:all, %i[a y z], false)
|
238
|
+
assert_modifier(:all, %i[x y z], false)
|
239
|
+
end
|
240
|
+
|
241
|
+
it 'the onem modifier works as expected' do
|
242
|
+
assert_modifier(:onem, %i[a b c], false)
|
243
|
+
assert_modifier(:onem, %i[a b z], false)
|
244
|
+
assert_modifier(:onem, %i[a y z], true)
|
245
|
+
assert_modifier(:onem, %i[x y z], true)
|
246
|
+
end
|
247
|
+
|
248
|
+
it 'the onep modifier works as expected' do
|
249
|
+
assert_modifier(:onep, %i[a b c], true)
|
250
|
+
assert_modifier(:onep, %i[a b z], true)
|
251
|
+
assert_modifier(:onep, %i[a y z], true)
|
252
|
+
assert_modifier(:onep, %i[x y z], false)
|
253
|
+
end
|
254
|
+
|
255
|
+
it 'the one modifier works as expected' do
|
256
|
+
assert_modifier(:one, %i[a b c], false)
|
257
|
+
assert_modifier(:one, %i[a b z], false)
|
258
|
+
assert_modifier(:one, %i[a y z], true)
|
259
|
+
assert_modifier(:one, %i[x y z], false)
|
260
|
+
end
|
261
|
+
|
262
|
+
it 'the none modifier works as expected' do
|
263
|
+
assert_modifier(:none, %i[a b c], false)
|
264
|
+
assert_modifier(:none, %i[a b z], false)
|
265
|
+
assert_modifier(:none, %i[a y z], false)
|
266
|
+
assert_modifier(:none, %i[x y z], true)
|
267
|
+
end
|
268
|
+
|
269
|
+
it 'aliases work' do
|
270
|
+
assert_modifier(:any, %i[a b c], true)
|
271
|
+
assert_modifier(:any, %i[a b z], true)
|
272
|
+
assert_modifier(:any, %i[a y z], true)
|
273
|
+
assert_modifier(:any, %i[x y z], false)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
describe 'nesting' do
|
278
|
+
it 'works' do
|
279
|
+
expect_valid do
|
280
|
+
p :hash, :present do
|
281
|
+
p :nested_hash do
|
282
|
+
p :value, in: [1]
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
expect_invalid do
|
287
|
+
p :hash, :present do
|
288
|
+
p :nested_hash do
|
289
|
+
p :value, not_in: [1]
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'yaml'
|
5
|
+
require 'database_cleaner'
|
6
|
+
require 'with_model'
|
7
|
+
|
8
|
+
require 'miscellany'
|
9
|
+
|
10
|
+
FileUtils.makedirs('log')
|
11
|
+
|
12
|
+
ActiveRecord::Base.logger = Logger.new('log/test.log')
|
13
|
+
ActiveRecord::Base.logger.level = Logger::DEBUG
|
14
|
+
ActiveRecord::Migration.verbose = false
|
15
|
+
|
16
|
+
db_adapter = ENV.fetch('ADAPTER', 'sqlite3')
|
17
|
+
db_config = YAML.safe_load(File.read('spec/db/database.yml'))
|
18
|
+
ActiveRecord::Base.establish_connection(db_config[db_adapter])
|
19
|
+
|
20
|
+
RSpec.configure do |config|
|
21
|
+
config.extend WithModel
|
22
|
+
|
23
|
+
# config.order = 'random'
|
24
|
+
|
25
|
+
config.before(:suite) do
|
26
|
+
DatabaseCleaner.clean_with(:truncation)
|
27
|
+
end
|
28
|
+
|
29
|
+
config.before do
|
30
|
+
DatabaseCleaner.strategy = :transaction
|
31
|
+
end
|
32
|
+
|
33
|
+
config.before do
|
34
|
+
DatabaseCleaner.start
|
35
|
+
end
|
36
|
+
|
37
|
+
config.after do
|
38
|
+
DatabaseCleaner.clean
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
puts "Testing with ActiveRecord #{ActiveRecord::VERSION::STRING}"
|