miscellany 0.1.2 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/miscellany/active_record/arbitrary_prefetch.rb +40 -17
- data/lib/miscellany/active_record/batched_destruction.rb +0 -1
- data/lib/miscellany/active_record/computed_columns.rb +107 -0
- data/lib/miscellany/batch_processor.rb +7 -3
- data/lib/miscellany/controller/sliced_response.rb +35 -8
- data/lib/miscellany/param_validator.rb +133 -86
- data/lib/miscellany/version.rb +1 -1
- data/lib/miscellany.rb +8 -0
- data/miscellany.gemspec +8 -17
- data/spec/db/database.yml +3 -0
- data/spec/miscellany/arbitrary_prefetch_spec.rb +69 -0
- data/spec/miscellany/computed_columns_spec.rb +62 -0
- data/spec/miscellany/param_validator_spec.rb +295 -0
- data/spec/spec_helper.rb +42 -0
- metadata +38 -179
- data/app/views/miscellany/spa_page.html.erb +0 -2
- data/config/initializers/01_custom_preloaders.rb +0 -2
- data/config/initializers/arbitrary_prefetch.rb +0 -1
- data/lib/miscellany/controller/spa_render.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0c92b625438d9027f73a5cd7b5f4ef168e1ed3e083b0c13924cb9f3f0e2db6bd
|
4
|
+
data.tar.gz: 0a52d0877c7eea6a0c2189efdd4c30cb65a3419abf149a8ae0ce4e63467741de
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9cbc439200d8a6c1604aa0153cbed6cfeead8636eb7330880371d2e8c6d2d412739e8a2d302c5dfd1cd9b038bec9f6de663f8fc7ddfd1fb1d8c99afe029e0306
|
7
|
+
data.tar.gz: d4bea4b5fd11c1830a8f18302c7f6c95d6f4c3fc147faf715f675850e7dee4e02d08632b05aa3e61ce4277de494a9784e6dc3ef55c8e92f99a0a22ba9a9d4fbc
|
@@ -4,18 +4,11 @@
|
|
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
|
9
|
+
ACTIVE_RECORD_VERSION = ::Gem::Version.new(::ActiveRecord::VERSION::STRING).release
|
10
|
+
PRE_RAILS_6_2 = ACTIVE_RECORD_VERSION < ::Gem::Version.new('6.2.0')
|
11
|
+
|
19
12
|
class PrefetcherContext
|
20
13
|
attr_accessor :model, :target_attribute
|
21
14
|
attr_reader :options
|
@@ -49,7 +42,7 @@ module Miscellany
|
|
49
42
|
@reflection ||= begin
|
50
43
|
queryset = @queryset
|
51
44
|
source_refl = model.reflections[@source_key.to_s]
|
52
|
-
scope = lambda {
|
45
|
+
scope = lambda {|*_args|
|
53
46
|
qs = queryset
|
54
47
|
qs = qs.merge(source_refl.scope_for(model.unscoped)) if source_refl.scope
|
55
48
|
qs
|
@@ -83,14 +76,16 @@ module Miscellany
|
|
83
76
|
return super if loaded?
|
84
77
|
|
85
78
|
records = super
|
86
|
-
preloader = nil
|
87
79
|
(@values[:prefetches] || {}).each do |_key, opts|
|
88
80
|
pfc = PrefetcherContext.new(model, opts)
|
89
81
|
pfc.link_models(records)
|
90
82
|
|
91
83
|
unless defined?(Goldiloader)
|
92
|
-
|
93
|
-
|
84
|
+
if PRE_RAILS_6_2
|
85
|
+
::ActiveRecord::Associations::Preloader.new.preload(records, [opts[:attribute]])
|
86
|
+
else
|
87
|
+
::ActiveRecord::Associations::Preloader.new(records: records, associations: [opts[:attribute]]).call
|
88
|
+
end
|
94
89
|
end
|
95
90
|
end
|
96
91
|
records
|
@@ -106,12 +101,12 @@ module Miscellany
|
|
106
101
|
assert_mutability!
|
107
102
|
@values[:prefetches] ||= {}
|
108
103
|
kwargs.each do |attr, opts|
|
109
|
-
@values[:prefetches][attr] =
|
104
|
+
@values[:prefetches][attr] = normalize_prefetch_options(attr, opts)
|
110
105
|
end
|
111
106
|
self
|
112
107
|
end
|
113
108
|
|
114
|
-
def
|
109
|
+
def normalize_prefetch_options(attr, opts)
|
115
110
|
norm = if opts.is_a?(Array)
|
116
111
|
{ relation: opts[0], queryset: opts[1] }
|
117
112
|
elsif opts.is_a?(ActiveRecord::Relation)
|
@@ -143,15 +138,43 @@ module Miscellany
|
|
143
138
|
end
|
144
139
|
end
|
145
140
|
|
141
|
+
module ActiveRecordPreloaderPatch
|
142
|
+
if ACTIVE_RECORD_VERSION >= ::Gem::Version.new('6.0.0')
|
143
|
+
def grouped_records(association, records, polymorphic_parent)
|
144
|
+
h = {}
|
145
|
+
records.each do |record|
|
146
|
+
next unless record
|
147
|
+
reflection = record.class._reflect_on_association(association)
|
148
|
+
reflection ||= record.association(association)&.reflection rescue nil
|
149
|
+
next if polymorphic_parent && !reflection || !record.association(association).klass
|
150
|
+
(h[reflection] ||= []) << record
|
151
|
+
end
|
152
|
+
h
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
module ActiveRecordReflectionPatch
|
158
|
+
def check_preloadable!
|
159
|
+
return if scope && scope.arity < 0
|
160
|
+
super
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
146
164
|
def self.install
|
147
165
|
::ActiveRecord::Base.include(ActiveRecordBasePatch)
|
166
|
+
|
148
167
|
::ActiveRecord::Relation.prepend(ActiveRecordRelationPatch)
|
149
168
|
::ActiveRecord::Relation::Merger.prepend(ActiveRecordMergerPatch)
|
150
169
|
|
170
|
+
::ActiveRecord::Associations::Preloader.prepend(ActiveRecordPreloaderPatch)
|
171
|
+
|
172
|
+
::ActiveRecord::Reflection::AssociationReflection.prepend(ActiveRecordReflectionPatch)
|
173
|
+
|
151
174
|
return unless defined? ::Goldiloader
|
152
175
|
|
153
176
|
::Goldiloader::AssociationLoader.module_eval do
|
154
|
-
def self.has_association?(model, association_name)
|
177
|
+
def self.has_association?(model, association_name)
|
155
178
|
model.association(association_name)
|
156
179
|
true
|
157
180
|
rescue ::ActiveRecord::AssociationNotFoundError => _err
|
@@ -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
|
@@ -8,12 +8,15 @@ module Miscellany
|
|
8
8
|
# enumerator_of_some_kind.each { |item| batches << item }
|
9
9
|
# batches.flush
|
10
10
|
class BatchProcessor
|
11
|
-
attr_reader :batch_size
|
11
|
+
attr_reader :batch_size, :ensure_once
|
12
12
|
|
13
|
-
def initialize(of: 1000, &blk)
|
13
|
+
def initialize(of: 1000, ensure_once: false, &blk)
|
14
14
|
@batch_size = of
|
15
15
|
@block = blk
|
16
|
+
@ensure_once = ensure_once
|
16
17
|
@current_batch = []
|
18
|
+
|
19
|
+
@flush_count = 0
|
17
20
|
end
|
18
21
|
|
19
22
|
def <<(item)
|
@@ -28,7 +31,7 @@ module Miscellany
|
|
28
31
|
end
|
29
32
|
|
30
33
|
def flush
|
31
|
-
process_batch if @current_batch.present?
|
34
|
+
process_batch if @current_batch.present? || (@flush_count.zero? && ensure_once)
|
32
35
|
end
|
33
36
|
|
34
37
|
protected
|
@@ -36,6 +39,7 @@ module Miscellany
|
|
36
39
|
def process_batch
|
37
40
|
@block.call(@current_batch)
|
38
41
|
@current_batch = []
|
42
|
+
@flush_count += 1
|
39
43
|
end
|
40
44
|
end
|
41
45
|
end
|
@@ -17,7 +17,9 @@ module Miscellany
|
|
17
17
|
default_sort: nil, valid_sorts: nil,
|
18
18
|
&blk
|
19
19
|
)
|
20
|
-
|
20
|
+
valid_sorts ||= queryset.column_names if queryset.respond_to?(:column_names)
|
21
|
+
valid_sorts ||= []
|
22
|
+
normalized_sorts = normalize_sort_options(valid_sorts, default: default_sort)
|
21
23
|
|
22
24
|
slice = Slice.build(
|
23
25
|
queryset, slice_params,
|
@@ -85,8 +87,10 @@ module Miscellany
|
|
85
87
|
|
86
88
|
def normalize_sort(sort, key: nil)
|
87
89
|
sort = sort.to_s if sort.is_a?(Symbol)
|
88
|
-
if sort.is_a?(
|
89
|
-
|
90
|
+
if sort.is_a?(Array)
|
91
|
+
sort = { **normalize_sort(sort[0]), **(sort[1] || {}) }
|
92
|
+
elsif sort.is_a?(String)
|
93
|
+
m = sort.match(/^([\w\.]+)(?: (ASC|DESC)(!?))?$/)
|
90
94
|
sort = { column: m[1], order: m[2], force_order: m[3].present? }.compact
|
91
95
|
elsif sort.is_a?(Proc)
|
92
96
|
sort = { column: sort }
|
@@ -215,18 +219,31 @@ module Miscellany
|
|
215
219
|
|
216
220
|
def rendered_items
|
217
221
|
ritems = sliced_items
|
218
|
-
ritems = ritems.map(&options[:item_transformer]) if options[:item_transformer]
|
222
|
+
ritems = ritems.to_a.map(&options[:item_transformer]) if options[:item_transformer]
|
219
223
|
ritems
|
220
224
|
end
|
221
225
|
|
222
226
|
def total_item_count
|
223
|
-
@total_item_count ||= options[:total_count] ||
|
227
|
+
@total_item_count ||= options[:total_count] || begin
|
228
|
+
if items.is_a?(ActiveRecord::Relation)
|
229
|
+
items.except(:select).count
|
230
|
+
elsif items.respond_to?(:count)
|
231
|
+
items.count
|
232
|
+
else
|
233
|
+
nil
|
234
|
+
end
|
235
|
+
end
|
224
236
|
end
|
225
237
|
|
226
238
|
def sliced_items
|
227
239
|
@sliced_items ||= begin
|
228
240
|
if items.is_a?(Array)
|
229
|
-
|
241
|
+
start, finish = slice_bounds
|
242
|
+
if start && finish
|
243
|
+
items[start...finish]
|
244
|
+
else
|
245
|
+
items
|
246
|
+
end
|
230
247
|
elsif items.is_a?(Proc)
|
231
248
|
items.call(self)
|
232
249
|
elsif items.is_a?(ActiveRecord::Relation)
|
@@ -247,10 +264,20 @@ module Miscellany
|
|
247
264
|
sorts << options[:valid_sorts][:default] if options.dig(:valid_sorts, :default).present?
|
248
265
|
|
249
266
|
sorts.reduce(qset) do |qset, sort|
|
267
|
+
order = sort[:order] || 'ASC'
|
250
268
|
if sort[:column].is_a?(Proc)
|
251
|
-
sort[:column].call(qset,
|
269
|
+
sort[:column].call(qset, order)
|
252
270
|
else
|
253
|
-
|
271
|
+
desired_nulls = (sort[:nulls] || :low).to_s.downcase.to_sym
|
272
|
+
nulls = case desired_nulls
|
273
|
+
when :last
|
274
|
+
'LAST'
|
275
|
+
when :first
|
276
|
+
'FIRST'
|
277
|
+
else
|
278
|
+
(desired_nulls == :high) == (order.to_s.upcase == 'DESC') ? 'FIRST' : 'LAST'
|
279
|
+
end
|
280
|
+
qset.order("#{sort[:column]} #{order} NULLS #{nulls}")
|
254
281
|
end
|
255
282
|
end
|
256
283
|
else
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module Miscellany
|
2
2
|
class ParamValidator
|
3
|
-
attr_accessor :context, :
|
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
|
25
|
+
def initialize(block, context, parameters = nil)
|
25
26
|
@block = block
|
26
27
|
@context = context
|
27
28
|
@params = parameters || context.params
|
28
|
-
@
|
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,
|
52
|
-
|
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
|
-
|
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
|
-
#
|
116
|
+
# Array Items check
|
123
117
|
run_check[:items] do |blk|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
+
errs = pv.errors.store_for(i)
|
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
|
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
|
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
|
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
|
169
|
-
|
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
|
-
|
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 =
|
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
|
-
|
217
|
-
|
230
|
+
store.push(result)
|
231
|
+
|
232
|
+
encountered_errors = true
|
233
|
+
end
|
218
234
|
end
|
219
235
|
|
220
|
-
!
|
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
|
-
|
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 (
|
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.
|
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
|
data/lib/miscellany/version.rb
CHANGED