miscellany 0.1.0 → 0.1.4
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.
- checksums.yaml +4 -4
- data/lib/miscellany/active_record/arbitrary_prefetch.rb +40 -17
- data/lib/miscellany/active_record/computed_columns.rb +107 -0
- data/lib/miscellany/controller/sliced_response.rb +13 -3
- data/lib/miscellany/param_validator.rb +133 -86
- data/lib/miscellany/version.rb +1 -1
- data/lib/miscellany.rb +12 -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 +37 -176
- data/config/initializers/01_custom_preloaders.rb +0 -2
- data/config/initializers/arbitrary_prefetch.rb +0 -1
@@ -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}"
|