miscellany 0.1.0 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/miscellany/active_record/arbitrary_prefetch.rb +40 -17
- data/lib/miscellany/active_record/computed_columns.rb +107 -0
- data/lib/miscellany/controller/sliced_response.rb +13 -3
- data/lib/miscellany/param_validator.rb +133 -86
- data/lib/miscellany/version.rb +1 -1
- data/lib/miscellany.rb +12 -0
- data/miscellany.gemspec +8 -17
- data/spec/db/database.yml +3 -0
- data/spec/miscellany/arbitrary_prefetch_spec.rb +69 -0
- data/spec/miscellany/computed_columns_spec.rb +62 -0
- data/spec/miscellany/param_validator_spec.rb +295 -0
- data/spec/spec_helper.rb +42 -0
- metadata +37 -176
- data/config/initializers/01_custom_preloaders.rb +0 -2
- data/config/initializers/arbitrary_prefetch.rb +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 70a91bb987b7c99319cbe607cdfba6c32e9637a5a3fb8bec3ec38725339ea3c6
|
4
|
+
data.tar.gz: f9607f408e91b1abadb51e73c4238b1a1d9f378fe294461efe11f3172783d9e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f8c1ba0f079635d38ed0d3a29d3cb865899fb66112593625a2ae159061c5fd48f63cfa2300a825f671bdbd04e4e2317d6ccedd80702391bdadd7329c0a2deae9
|
7
|
+
data.tar.gz: a73fd773078016b440b08243c4c6dc5477ff26cdbacb054f57d8b99b77c15305af7a1741d0c94b02399c3b59af7553254a23be5441d1d30948f4f51cb53b6ccf
|
@@ -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
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require_relative './http_error_handling'
|
2
|
+
|
1
3
|
module Miscellany
|
2
4
|
module SlicedResponse
|
3
5
|
extend ActiveSupport::Concern
|
@@ -84,7 +86,7 @@ module Miscellany
|
|
84
86
|
def normalize_sort(sort, key: nil)
|
85
87
|
sort = sort.to_s if sort.is_a?(Symbol)
|
86
88
|
if sort.is_a?(String)
|
87
|
-
m = sort.match(
|
89
|
+
m = sort.match(/^([\w\.]+)(?: (ASC|DESC)(!?))?$/)
|
88
90
|
sort = { column: m[1], order: m[2], force_order: m[3].present? }.compact
|
89
91
|
elsif sort.is_a?(Proc)
|
90
92
|
sort = { column: sort }
|
@@ -213,12 +215,20 @@ module Miscellany
|
|
213
215
|
|
214
216
|
def rendered_items
|
215
217
|
ritems = sliced_items
|
216
|
-
ritems = ritems.map(&options[:item_transformer]) if options[:item_transformer]
|
218
|
+
ritems = ritems.to_a.map(&options[:item_transformer]) if options[:item_transformer]
|
217
219
|
ritems
|
218
220
|
end
|
219
221
|
|
220
222
|
def total_item_count
|
221
|
-
@total_item_count ||= options[:total_count] ||
|
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
|
222
232
|
end
|
223
233
|
|
224
234
|
def sliced_items
|
@@ -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
|
+
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
|
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
data/lib/miscellany.rb
CHANGED
@@ -1,6 +1,18 @@
|
|
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
|
5
7
|
|
8
|
+
if defined?(Rails)
|
9
|
+
class Engine < ::Rails::Engine
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
ActiveSupport.on_load(:active_record) do
|
14
|
+
Miscellany::CustomPreloaders.install
|
15
|
+
Miscellany::ArbitraryPrefetch.install
|
16
|
+
Miscellany::ComputedColumns.install
|
17
|
+
end
|
6
18
|
end
|
data/miscellany.gemspec
CHANGED
@@ -22,22 +22,13 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.test_files = Dir["spec/**/*"]
|
23
23
|
spec.require_paths = ['lib']
|
24
24
|
|
25
|
-
spec.
|
26
|
-
spec.
|
27
|
-
spec.
|
28
|
-
spec.add_development_dependency "rspec-rails"
|
29
|
-
spec.add_development_dependency "pg"
|
30
|
-
spec.add_development_dependency "factory"
|
31
|
-
spec.add_development_dependency "factory_bot"
|
32
|
-
spec.add_development_dependency "timecop"
|
33
|
-
spec.add_development_dependency "webmock"
|
34
|
-
spec.add_development_dependency "sinatra", ">= 0"
|
35
|
-
spec.add_development_dependency "shoulda-matchers"
|
36
|
-
spec.add_development_dependency "yard"
|
37
|
-
spec.add_development_dependency "pry"
|
38
|
-
spec.add_development_dependency "pry-nav"
|
39
|
-
spec.add_development_dependency "rubocop"
|
25
|
+
spec.add_dependency 'rails', '>= 5', '< 6.3'
|
26
|
+
# spec.add_dependency 'activerecord', '>= 5', '< 6.3'
|
27
|
+
# spec.add_dependency 'activesupport', '>= 5', '< 6.3'
|
40
28
|
|
41
|
-
spec.
|
42
|
-
spec.
|
29
|
+
spec.add_development_dependency 'rake'
|
30
|
+
spec.add_development_dependency 'database_cleaner', '>= 1.2'
|
31
|
+
spec.add_development_dependency 'rspec', '~> 3'
|
32
|
+
spec.add_development_dependency 'sqlite3', '~> 1.3'
|
33
|
+
spec.add_development_dependency 'with_model'
|
43
34
|
end
|