miscellany 0.1.0 → 0.1.4
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/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
|