miscellany 0.1.3 → 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 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