miscellany 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 592322f721c82c3a296438d483a372ac5dc4b06d2da56e1b4d614999ed7f1cb4
4
- data.tar.gz: affc6540ac3f73d4096a544ab45d67e617da481aa97334e399bea704c5055c1e
3
+ metadata.gz: 70a91bb987b7c99319cbe607cdfba6c32e9637a5a3fb8bec3ec38725339ea3c6
4
+ data.tar.gz: f9607f408e91b1abadb51e73c4238b1a1d9f378fe294461efe11f3172783d9e2
5
5
  SHA512:
6
- metadata.gz: e7c5dd2f97d9998656c7eae1a1ef69c7091c09fdc0d34f7b98ccce3d18067751d67ee0b6790fa2c355149703becfe124a32eb6f65fc9b620cd5425ea66e8f6e4
7
- data.tar.gz: 2cb31549f3dda188c255e0e3fb11d2770039eda248f63612e88bcfea13422e26228efc451369e69f2e688d5a0627ccbb815457d73b7b443fc19bec1b312d357e
6
+ metadata.gz: f8c1ba0f079635d38ed0d3a29d3cb865899fb66112593625a2ae159061c5fd48f63cfa2300a825f671bdbd04e4e2317d6ccedd80702391bdadd7329c0a2deae9
7
+ data.tar.gz: a73fd773078016b440b08243c4c6dc5477ff26cdbacb054f57d8b99b77c15305af7a1741d0c94b02399c3b59af7553254a23be5441d1d30948f4f51cb53b6ccf
@@ -4,16 +4,6 @@
4
4
  # The values of the Hash may be an array of [Symbol, Relation] or another (filtered) Relation.
5
5
  # Objects are queried from an existing Association on the model. This Association is detemrined
6
6
  # by either the Symbol when an array is passed, or by finding an Assoication for the passed Relation's model
7
- #
8
- # NOTICE: This implementation is NOT COMPLETE by itself - it depends on Goldiloader
9
- # to detect the use of the virtual associations and prevent N+1s. We were already using
10
- # Goldiloader, so this made sense. If this module is ever needed stand-alone,
11
- # the following options have been identified:
12
- # 1. Extend ActiveRecordRelationPatch#exec_queries to execute an ActiveRecord::Associations::Preloader
13
- # that will load the related objects
14
- # 2. Duplicates the relevant snippets from Goldiloader into this module. See Goldiloader::AutoIncludeContext
15
- # The current Goldiloader implementation uses Option 1 internally, but also makes the relations lazy - even
16
- # if you define a prefetch, it won't actually be loaded until you attempt to access it on one of the models.
17
7
  module Miscellany
18
8
  module ArbitraryPrefetch
19
9
  ACTIVE_RECORD_VERSION = ::Gem::Version.new(::ActiveRecord::VERSION::STRING).release
@@ -111,12 +101,12 @@ module Miscellany
111
101
  assert_mutability!
112
102
  @values[:prefetches] ||= {}
113
103
  kwargs.each do |attr, opts|
114
- @values[:prefetches][attr] = normalize_options(attr, opts)
104
+ @values[:prefetches][attr] = normalize_prefetch_options(attr, opts)
115
105
  end
116
106
  self
117
107
  end
118
108
 
119
- def normalize_options(attr, opts)
109
+ def normalize_prefetch_options(attr, opts)
120
110
  norm = if opts.is_a?(Array)
121
111
  { relation: opts[0], queryset: opts[1] }
122
112
  elsif opts.is_a?(ActiveRecord::Relation)
@@ -155,7 +145,7 @@ module Miscellany
155
145
  records.each do |record|
156
146
  next unless record
157
147
  reflection = record.class._reflect_on_association(association)
158
- reflection ||= record.association(association)&.reflection
148
+ reflection ||= record.association(association)&.reflection rescue nil
159
149
  next if polymorphic_parent && !reflection || !record.association(association).klass
160
150
  (h[reflection] ||= []) << record
161
151
  end
@@ -0,0 +1,107 @@
1
+
2
+ module Miscellany
3
+ module ComputedColumns
4
+ module ActiveRecordBasePatch
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class << self
9
+ delegate :with_computed, to: :all
10
+ end
11
+ end
12
+
13
+ class_methods do
14
+ def define_computed(key, dirblk = nil, &blk)
15
+ blk ||= dirblk
16
+ @defined_computeds ||= {}
17
+ @defined_computeds[key] = blk
18
+ end
19
+
20
+ def get_defined_computed(key)
21
+ @defined_computeds ||= {}
22
+ @defined_computeds[key] || superclass.try(:get_defined_computed, key)
23
+ end
24
+ end
25
+ end
26
+
27
+ module ActiveRecordRelationPatch
28
+ def with_computed(*args, **kwargs)
29
+ entries = { **kwargs }
30
+ args.each do |k|
31
+ entries[k] = []
32
+ end
33
+
34
+ entries.reduce(self) do |query, (k, v)|
35
+ comp = model.get_defined_computed(k)
36
+ raise "Undefined ComputedColum :#{k}" if comp.nil?
37
+
38
+ builder = ComputedBuilder.new(k, v, &comp)
39
+ builder.apply(query)
40
+ end
41
+ end
42
+ end
43
+
44
+ class ComputedBuilder
45
+ def initialize(key, args, &blk)
46
+ @key = key
47
+ @args = args.is_a?(Array) ? args : [args]
48
+ @block = blk
49
+ @compiled = {}
50
+ end
51
+
52
+ %i[select join_condition query].each do |m|
53
+ define_method(m) do |arg=:not_given, &blk|
54
+ raise "Must provide either a value or a block" if arg != :not_given && blk
55
+ raise "Must provide either a value or a block" if arg == :not_given && !blk
56
+
57
+ if arg == :not_given
58
+ arg = blk.call
59
+ end
60
+
61
+ @compiled[m] = arg
62
+ end
63
+ end
64
+
65
+ def apply(q)
66
+ instance_exec(*@args, &@block)
67
+
68
+ c = @compiled
69
+ raise "defined_computed: query must be provided" unless c[:query]
70
+
71
+ join_name = @key.to_s
72
+ base_table_name = current_table_from_scope(q)
73
+ c[:join_condition] ||= "COMPUTED.id = #{base_table_name}.id"
74
+ c[:select] ||= "#{join_name}.value AS #{@key}"
75
+
76
+ q = q.select("#{base_table_name}.*") if !q.values[:select].present?
77
+
78
+ select_statement = c[:select].gsub('COMPUTED', join_name)
79
+ join_condition = c[:join_condition].gsub('COMPUTED', join_name)
80
+ join_query = c[:query]
81
+ join_query = join_query.to_sql if join_query.respond_to?(:to_sql)
82
+
83
+ q.select(select_statement).joins("LEFT OUTER JOIN (#{join_query}) #{join_name} ON #{join_condition}")
84
+ end
85
+
86
+ protected
87
+
88
+ def current_table_from_scope(q)
89
+ current_table = q.current_scope.arel.source.left
90
+
91
+ case current_table
92
+ when Arel::Table
93
+ current_table.name
94
+ when Arel::Nodes::TableAlias
95
+ current_table.right
96
+ else
97
+ fail
98
+ end
99
+ end
100
+ end
101
+
102
+ def self.install
103
+ ::ActiveRecord::Base.include(ActiveRecordBasePatch)
104
+ ::ActiveRecord::Relation.prepend(ActiveRecordRelationPatch)
105
+ end
106
+ end
107
+ end
@@ -86,7 +86,7 @@ module Miscellany
86
86
  def normalize_sort(sort, key: nil)
87
87
  sort = sort.to_s if sort.is_a?(Symbol)
88
88
  if sort.is_a?(String)
89
- m = sort.match(/(\w+)(?: (ASC|DESC)(!?))?/)
89
+ m = sort.match(/^([\w\.]+)(?: (ASC|DESC)(!?))?$/)
90
90
  sort = { column: m[1], order: m[2], force_order: m[3].present? }.compact
91
91
  elsif sort.is_a?(Proc)
92
92
  sort = { column: sort }
@@ -215,12 +215,20 @@ module Miscellany
215
215
 
216
216
  def rendered_items
217
217
  ritems = sliced_items
218
- ritems = ritems.map(&options[:item_transformer]) if options[:item_transformer]
218
+ ritems = ritems.to_a.map(&options[:item_transformer]) if options[:item_transformer]
219
219
  ritems
220
220
  end
221
221
 
222
222
  def total_item_count
223
- @total_item_count ||= options[:total_count] || (items.respond_to?(:count) && items.count) || nil
223
+ @total_item_count ||= options[:total_count] || begin
224
+ if items.is_a?(ActiveRecord::Relation)
225
+ items.except(:select).count
226
+ elsif items.respond_to?(:count)
227
+ items.count
228
+ else
229
+ nil
230
+ end
231
+ end
224
232
  end
225
233
 
226
234
  def sliced_items
@@ -1,6 +1,7 @@
1
1
  module Miscellany
2
2
  class ParamValidator
3
- attr_accessor :context, :options, :errors
3
+ attr_accessor :context, :errors
4
+ attr_reader :params
4
5
 
5
6
  delegate_missing_to :context
6
7
 
@@ -9,7 +10,7 @@ module Miscellany
9
10
  CHECKS = %i[type specified present default transform in block items pattern].freeze
10
11
  NON_PREFIXED = %i[default transform type message timezone].freeze
11
12
  PREFIXES = %i[all onem onep one none].freeze
12
- PREFIX_ALIASES = { any: :onep, not: :none }.freeze
13
+ PREFIX_ALIASES = { any: :onep, not: :none, one_or_less: :onem, one_or_more: :onem }.freeze
13
14
  ALL_PREFIXES = (PREFIXES + PREFIX_ALIASES.keys).freeze
14
15
  VALID_FLAGS = %i[present specified].freeze
15
16
 
@@ -21,14 +22,11 @@ module Miscellany
21
22
  end
22
23
  end
23
24
 
24
- def initialize(block, context, parameters = nil, options = nil)
25
+ def initialize(block, context, parameters = nil)
25
26
  @block = block
26
27
  @context = context
27
28
  @params = parameters || context.params
28
- @subkeys = []
29
- @options = options || {}
30
- @errors = {}
31
- @explicit_parameters = []
29
+ @errors = ErrorStore.new
32
30
  end
33
31
 
34
32
  def self.check(params, context: nil, &blk)
@@ -48,8 +46,13 @@ module Miscellany
48
46
 
49
47
  def apply_checks(&blk)
50
48
  blk ||= @block
51
- args = trim_arguments(blk, [params, @subkeys[-1]])
52
- instance_exec(*args, &blk)
49
+ args = trim_arguments(blk, [@params, :TODO])
50
+
51
+ dresult = instance_exec(*args, &blk)
52
+ dresult = "failed validation #{check}" if dresult == false
53
+ if dresult.present? && dresult != true
54
+ @errors.push(dresult)
55
+ end
53
56
  end
54
57
 
55
58
  def parameter(param_keys, *args, **kwargs, &blk)
@@ -107,45 +110,57 @@ module Miscellany
107
110
 
108
111
  # Nested check
109
112
  run_check[:block] do |blk|
110
- iterate_array = false # TODO
111
- sub_parameter(pk) do
112
- if params.is_a?(Array) && iterate_array
113
- params.each_with_index do |v, i|
114
- sub_parameter(i) { apply_checks(&blk) }
115
- end
116
- else
117
- apply_checks(&blk)
118
- end
119
- end
113
+ ParamValidator.check(params[pk], context: context, &blk)
120
114
  end
121
115
 
122
- # Nested check
116
+ # Array Items check
123
117
  run_check[:items] do |blk|
124
- sub_parameter(pk) do
125
- if params.is_a?(Array)
126
- params.each_with_index do |v, i|
127
- sub_parameter(i) { apply_checks(&blk) }
118
+ if params[pk].is_a?(Array)
119
+ error_items = 0
120
+ astore = ErrorStore.new
121
+
122
+ params[pk].each_with_index do |v, i|
123
+ errs = nil
124
+ if blk.is_a?(Hash)
125
+ pv = ParamValidator.new(nil, self.context, params[pk])
126
+ pv.parameter(i, **blk)
127
+ ers = pv.errors
128
+ else
129
+ errs = ParamValidator.check(params[pk][i], context: context, &blk)
130
+ end
131
+
132
+ if errs.present?
133
+ error_items += 1
134
+ if error_items > 5
135
+ check_results[:items]
136
+ astore.push("Too Many Errors")
137
+ break
138
+ else
139
+ astore.push_to(i, errs)
140
+ end
128
141
  end
129
- else
130
- raise "items: validator can only be used with Arrays"
131
142
  end
143
+
144
+ astore
145
+ else
146
+ raise "items: validator can only be used with Arrays"
132
147
  end
133
148
  end
134
149
  end
135
150
 
136
- final_errors = {}
151
+ final_errors = ErrorStore.new
137
152
  checks.each do |check, check_prefix|
138
153
  if check_prefix == :all || check_prefix == nil
139
154
  all_results.each do |field, err_map|
140
155
  errs = err_map[check]
141
156
  next unless errs.present?
142
157
 
143
- final_errors[field] = merge_error_hashes(final_errors[field], errs)
158
+ final_errors.push_to(field, errs)
144
159
  end
145
160
  elsif check_prefix == :none
146
161
  all_results.each do |field, err_map|
147
162
  errs = err_map[check]
148
- final_errors[field] = merge_error_hashes(final_errors[field], "must NOT be #{check}") unless errs.present?
163
+ final_errors.push_to(field, "must NOT be #{check}") unless errs.present?
149
164
  end
150
165
  else
151
166
  counts = check_pass_count(check, all_results)
@@ -160,13 +175,14 @@ module Miscellany
160
175
  (counts[:passed] > 1 && check_prefix == :onem) ||
161
176
  (counts[:passed] < 1 && check_prefix == :onep)
162
177
 
163
- final_errors = merge_error_hashes(final_errors, "#{string_prefixes[check_prefix]} #{field_key} #{check}")
178
+ final_errors.push("#{string_prefixes[check_prefix]} #{field_key} must be #{check}")
164
179
  end
165
180
  end
166
181
  end
167
182
 
168
- @errors = merge_error_hashes(@errors, final_errors)
169
- final_errors
183
+ @errors.merge!(final_errors)
184
+
185
+ nil
170
186
  end
171
187
 
172
188
  alias p parameter
@@ -197,27 +213,27 @@ module Miscellany
197
213
  check_prefixes = NON_PREFIXED.include?(check) ? [nil] : Array(checks_to_run&.[](check))
198
214
  return true unless check_prefixes.present?
199
215
 
216
+ encountered_errors = false
217
+
200
218
  check_prefixes.each do |check_prefix|
201
- initial_errors = @errors
202
- @errors = []
219
+ prefixed_check = [check_prefix, check].compact.join('_')
203
220
  prefix_options = (check_prefix.nil? ? options : options&.[](check_prefix)) || {}
204
- args = trim_arguments(blk, [prefix_options[check]])
205
221
 
206
- result = yield(*args)
222
+ result = blk.call(*trim_arguments(blk, [prefix_options[check]]))
207
223
  result = "failed validation #{check}" if result == false
208
224
 
225
+ store = state[check] ||= ErrorStore.new
226
+
209
227
  if result.present? && result != true
210
228
  result = options[:message] if options&.[](:message).present?
211
- Array(result).each do |e|
212
- @errors << e
213
- end
214
- end
215
229
 
216
- state[check] = merge_error_hashes(state[check], @errors)
217
- @errors = initial_errors
230
+ store.push(result)
231
+
232
+ encountered_errors = true
233
+ end
218
234
  end
219
235
 
220
- !state[check].present?
236
+ !encountered_errors
221
237
  end
222
238
 
223
239
  def coerce_type(params, key, opts)
@@ -243,10 +259,11 @@ module Miscellany
243
259
 
244
260
  return type.call(param, options) if type.is_a?(Proc)
245
261
 
246
- if (param.is_a?(Array) && type != Array) || ((param.is_a?(Hash) || param.is_a?(ActionController::Parameters)) && type != Hash)
262
+ is_rails_parameters = defined?(ActionController::Parameters) && param.is_a?(ActionController::Parameters)
263
+ if (param.is_a?(Array) && type != Array) || ((param.is_a?(Hash) || is_rails_parameters) && type != Hash)
247
264
  raise ArgumentError
248
265
  end
249
- return param if (param.is_a?(ActionController::Parameters) && type == Hash rescue false)
266
+ return param if (is_rails_parameters && type == Hash rescue false)
250
267
 
251
268
  # Primitives
252
269
  return Integer(param) if type == Integer
@@ -374,53 +391,13 @@ module Miscellany
374
391
  skey = key.to_s
375
392
  ALL_PREFIXES.each do |pfx|
376
393
  spfx = pfx.to_s
377
- next unless skey.starts_with?("#{spfx}_")
394
+ next unless skey.start_with?("#{spfx}_")
378
395
 
379
396
  return [skey[(spfx.length + 1)..-1].to_sym, PREFIX_ALIASES[pfx] || pfx]
380
397
  end
381
398
  [key, nil]
382
399
  end
383
400
 
384
- def sub_parameter(k)
385
- @subkeys.push(k)
386
- yield
387
- ensure
388
- @subkeys.pop
389
- end
390
-
391
- def params
392
- p = @params
393
- @subkeys.each { |k| p = p[k] }
394
- p
395
- end
396
-
397
- def merge_error_hashes(target, from)
398
- target ||= []
399
- if target.is_a?(Hash)
400
- ta = []
401
- th = target
402
- else
403
- ta = target
404
- th = target[-1].is_a?(Hash) ? ta.pop : {}
405
- end
406
-
407
- if from.is_a?(Hash)
408
- from.each_pair do |k, v|
409
- th[k] = merge_error_hashes(th[k], v)
410
- end
411
- elsif from.is_a?(Array)
412
- merge_error_hashes(th, from.pop) if from[-1].is_a?(Hash)
413
- from.each { |f| ta << f }
414
- else
415
- ta << from
416
- end
417
-
418
- return th if !ta.present? && th.present?
419
-
420
- ta << th if th.present?
421
- ta
422
- end
423
-
424
401
  def merge_hashes(h1, h2)
425
402
  h2.each do |k, v|
426
403
  set_hash_key(h1, k, v)
@@ -438,5 +415,75 @@ module Miscellany
438
415
  return args if blk.arity.negative?
439
416
  args[0..(blk.arity.abs - 1)]
440
417
  end
418
+
419
+ class ErrorStore
420
+ attr_reader :errors, :fields
421
+
422
+ def initialize
423
+ @errors = []
424
+ @fields = {}
425
+ end
426
+
427
+ def push(error)
428
+ if error.is_a?(ErrorStore)
429
+ merge!(error)
430
+ elsif error.is_a?(Array)
431
+ error.each{|e| push(e) }
432
+ else
433
+ @errors << error
434
+ end
435
+ end
436
+
437
+ def push_to(field, error)
438
+ store_for(field).push(error)
439
+ end
440
+
441
+ def present?
442
+ @errors.present? || @fields.values.any?(&:present?)
443
+ end
444
+
445
+ def serialize
446
+ if @fields.values.any?(&:present?)
447
+ h = { }
448
+ h[:_SELF_] = @errors if @errors.present?
449
+ @fields.each do |k, v|
450
+ s = v.serialize
451
+ next unless s.present?
452
+ h[k] = s
453
+ end
454
+ h
455
+ elsif @errors.present?
456
+ @errors
457
+ else
458
+ nil
459
+ end
460
+ end
461
+
462
+ def merge!(other_store)
463
+ return self if other_store == self
464
+
465
+ @errors |= other_store.errors
466
+ other_store.fields.each do |k, v|
467
+ store_for(k).merge!(v)
468
+ end
469
+
470
+ self
471
+ end
472
+
473
+ def store_for(field)
474
+ if field.is_a?(Symbol) || field.is_a?(Numeric)
475
+ field = [field]
476
+ elsif field.is_a?(String)
477
+ field = field.split('.')
478
+ elsif field.is_a?(Array)
479
+ field = [*field]
480
+ end
481
+
482
+ sfield = field.shift
483
+ store = @fields[sfield.to_s] ||= ErrorStore.new
484
+ return store if field.count == 0
485
+ return store.store_for(field)
486
+ end
487
+ end
441
488
  end
442
489
  end
@@ -1,3 +1,3 @@
1
1
  module Miscellany
2
- VERSION = "0.1.3".freeze
2
+ VERSION = "0.1.4".freeze
3
3
  end
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.add_development_dependency "bundler", "~> 1.15"
26
- spec.add_development_dependency "rake", "~> 10.0"
27
- spec.add_development_dependency "rspec", "~> 3.0"
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.add_dependency "rails", ">= 5"
42
- spec.add_dependency "activerecord-import"
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,3 @@
1
+ sqlite3:
2
+ adapter: sqlite3
3
+ database: ":memory:"
@@ -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
@@ -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}"
metadata CHANGED
@@ -1,157 +1,37 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: miscellany
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ethan Knapp
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-17 00:00:00.000000000 Z
11
+ date: 2021-09-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: bundler
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '1.15'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '1.15'
27
- - !ruby/object:Gem::Dependency
28
- name: rake
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '10.0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '10.0'
41
- - !ruby/object:Gem::Dependency
42
- name: rspec
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '3.0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '3.0'
55
- - !ruby/object:Gem::Dependency
56
- name: rspec-rails
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: pg
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: factory
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: factory_bot
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
- - !ruby/object:Gem::Dependency
112
- name: timecop
14
+ name: rails
113
15
  requirement: !ruby/object:Gem::Requirement
114
16
  requirements:
115
17
  - - ">="
116
18
  - !ruby/object:Gem::Version
117
- version: '0'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ">="
123
- - !ruby/object:Gem::Version
124
- version: '0'
125
- - !ruby/object:Gem::Dependency
126
- name: webmock
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
19
+ version: '5'
20
+ - - "<"
130
21
  - !ruby/object:Gem::Version
131
- version: '0'
132
- type: :development
22
+ version: '6.3'
23
+ type: :runtime
133
24
  prerelease: false
134
25
  version_requirements: !ruby/object:Gem::Requirement
135
26
  requirements:
136
27
  - - ">="
137
28
  - !ruby/object:Gem::Version
138
- version: '0'
139
- - !ruby/object:Gem::Dependency
140
- name: sinatra
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - ">="
144
- - !ruby/object:Gem::Version
145
- version: '0'
146
- type: :development
147
- prerelease: false
148
- version_requirements: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - ">="
29
+ version: '5'
30
+ - - "<"
151
31
  - !ruby/object:Gem::Version
152
- version: '0'
32
+ version: '6.3'
153
33
  - !ruby/object:Gem::Dependency
154
- name: shoulda-matchers
34
+ name: rake
155
35
  requirement: !ruby/object:Gem::Requirement
156
36
  requirements:
157
37
  - - ">="
@@ -165,49 +45,49 @@ dependencies:
165
45
  - !ruby/object:Gem::Version
166
46
  version: '0'
167
47
  - !ruby/object:Gem::Dependency
168
- name: yard
48
+ name: database_cleaner
169
49
  requirement: !ruby/object:Gem::Requirement
170
50
  requirements:
171
51
  - - ">="
172
52
  - !ruby/object:Gem::Version
173
- version: '0'
53
+ version: '1.2'
174
54
  type: :development
175
55
  prerelease: false
176
56
  version_requirements: !ruby/object:Gem::Requirement
177
57
  requirements:
178
58
  - - ">="
179
59
  - !ruby/object:Gem::Version
180
- version: '0'
60
+ version: '1.2'
181
61
  - !ruby/object:Gem::Dependency
182
- name: pry
62
+ name: rspec
183
63
  requirement: !ruby/object:Gem::Requirement
184
64
  requirements:
185
- - - ">="
65
+ - - "~>"
186
66
  - !ruby/object:Gem::Version
187
- version: '0'
67
+ version: '3'
188
68
  type: :development
189
69
  prerelease: false
190
70
  version_requirements: !ruby/object:Gem::Requirement
191
71
  requirements:
192
- - - ">="
72
+ - - "~>"
193
73
  - !ruby/object:Gem::Version
194
- version: '0'
74
+ version: '3'
195
75
  - !ruby/object:Gem::Dependency
196
- name: pry-nav
76
+ name: sqlite3
197
77
  requirement: !ruby/object:Gem::Requirement
198
78
  requirements:
199
- - - ">="
79
+ - - "~>"
200
80
  - !ruby/object:Gem::Version
201
- version: '0'
81
+ version: '1.3'
202
82
  type: :development
203
83
  prerelease: false
204
84
  version_requirements: !ruby/object:Gem::Requirement
205
85
  requirements:
206
- - - ">="
86
+ - - "~>"
207
87
  - !ruby/object:Gem::Version
208
- version: '0'
88
+ version: '1.3'
209
89
  - !ruby/object:Gem::Dependency
210
- name: rubocop
90
+ name: with_model
211
91
  requirement: !ruby/object:Gem::Requirement
212
92
  requirements:
213
93
  - - ">="
@@ -220,34 +100,6 @@ dependencies:
220
100
  - - ">="
221
101
  - !ruby/object:Gem::Version
222
102
  version: '0'
223
- - !ruby/object:Gem::Dependency
224
- name: rails
225
- requirement: !ruby/object:Gem::Requirement
226
- requirements:
227
- - - ">="
228
- - !ruby/object:Gem::Version
229
- version: '5'
230
- type: :runtime
231
- prerelease: false
232
- version_requirements: !ruby/object:Gem::Requirement
233
- requirements:
234
- - - ">="
235
- - !ruby/object:Gem::Version
236
- version: '5'
237
- - !ruby/object:Gem::Dependency
238
- name: activerecord-import
239
- requirement: !ruby/object:Gem::Requirement
240
- requirements:
241
- - - ">="
242
- - !ruby/object:Gem::Version
243
- version: '0'
244
- type: :runtime
245
- prerelease: false
246
- version_requirements: !ruby/object:Gem::Requirement
247
- requirements:
248
- - - ">="
249
- - !ruby/object:Gem::Version
250
- version: '0'
251
103
  description:
252
104
  email:
253
105
  - eknapp@instructure.com
@@ -256,14 +108,12 @@ extensions: []
256
108
  extra_rdoc_files: []
257
109
  files:
258
110
  - README.md
259
- - app/views/miscellany/spa_page.html.erb
260
- - config/initializers/01_custom_preloaders.rb
261
- - config/initializers/arbitrary_prefetch.rb
262
111
  - config/initializers/cancancan.rb
263
112
  - lib/miscellany.rb
264
113
  - lib/miscellany/active_record/arbitrary_prefetch.rb
265
114
  - lib/miscellany/active_record/batch_matcher.rb
266
115
  - lib/miscellany/active_record/batched_destruction.rb
116
+ - lib/miscellany/active_record/computed_columns.rb
267
117
  - lib/miscellany/active_record/custom_preloaders.rb
268
118
  - lib/miscellany/batch_processor.rb
269
119
  - lib/miscellany/batching_csv_processor.rb
@@ -273,6 +123,11 @@ files:
273
123
  - lib/miscellany/param_validator.rb
274
124
  - lib/miscellany/version.rb
275
125
  - miscellany.gemspec
126
+ - spec/db/database.yml
127
+ - spec/miscellany/arbitrary_prefetch_spec.rb
128
+ - spec/miscellany/computed_columns_spec.rb
129
+ - spec/miscellany/param_validator_spec.rb
130
+ - spec/spec_helper.rb
276
131
  homepage: https://instructure.com
277
132
  licenses: []
278
133
  metadata: {}
@@ -295,4 +150,9 @@ rubygems_version: 3.0.3
295
150
  signing_key:
296
151
  specification_version: 4
297
152
  summary: Gem for a bunch of random, re-usable Rails Concerns & Helpers
298
- test_files: []
153
+ test_files:
154
+ - spec/db/database.yml
155
+ - spec/miscellany/arbitrary_prefetch_spec.rb
156
+ - spec/miscellany/param_validator_spec.rb
157
+ - spec/miscellany/computed_columns_spec.rb
158
+ - spec/spec_helper.rb
@@ -1,2 +0,0 @@
1
- <%= stylesheet_pack_tag pack_name %>
2
- <%= javascript_pack_tag pack_name %>
@@ -1,2 +0,0 @@
1
- # This Preloader needs to run before any that will load Models
2
- Miscellany::CustomPreloaders.install
@@ -1 +0,0 @@
1
- Miscellany::ArbitraryPrefetch.install