advanced_ar 0.1.0
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 +7 -0
- data/README.md +3 -0
- data/config/initializers/01_custom_preloaders.rb +2 -0
- data/config/initializers/arbitrary_prefetch.rb +1 -0
- data/config/initializers/cancancan.rb +34 -0
- data/lib/advanced_ar.rb +6 -0
- data/lib/advanced_ar/arbitrary_prefetch.rb +161 -0
- data/lib/advanced_ar/batch_matcher.rb +199 -0
- data/lib/advanced_ar/batch_processor.rb +41 -0
- data/lib/advanced_ar/batched_destruction.rb +94 -0
- data/lib/advanced_ar/batching_csv_processor.rb +86 -0
- data/lib/advanced_ar/custom_preloaders.rb +57 -0
- data/lib/advanced_ar/param_validator.rb +421 -0
- data/lib/advanced_ar/version.rb +3 -0
- metadata +293 -0
@@ -0,0 +1,94 @@
|
|
1
|
+
module AdvancedAR::BatchedDestruction
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
define_model_callbacks :bulk_destroy
|
6
|
+
define_model_callbacks :destroy_batch
|
7
|
+
|
8
|
+
before_destroy_batch do
|
9
|
+
# TODO Delete Dependant Relations
|
10
|
+
model_class.reflections.each do |name, reflection|
|
11
|
+
options = reflection.options
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class_methods do
|
17
|
+
def bulk_destroy(**kwargs)
|
18
|
+
return to_sql
|
19
|
+
bulk_destroy_internal(self, **kwargs)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Hook for performing the actual deletion of items, may be used to facilitate soft-deletion.
|
23
|
+
# Must not call destroy().
|
24
|
+
# Default implementation is to delete the batch using delete_all(id: batch_ids).
|
25
|
+
def destroy_bulk_batch(batch, options)
|
26
|
+
delete_ids = batch.map(&:id)
|
27
|
+
where(id: delete_ids).delete_all()
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def bulk_destroy_internal(items, **kwargs)
|
33
|
+
options = {}
|
34
|
+
options.merge!(kwargs)
|
35
|
+
ClassCallbackExector.run_callbacks(model_class, :bulk_destroy, options: options) do
|
36
|
+
if items.respond_to?(:find_in_batches)
|
37
|
+
items.find_in_batches do |batch|
|
38
|
+
_destroy_batch(batch, options)
|
39
|
+
end
|
40
|
+
else
|
41
|
+
_destroy_batch(items, options)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def _destroy_batch(batch, options)
|
47
|
+
ClassCallbackExector.run_callbacks(model_class, :destroy_batch, {
|
48
|
+
model_class: model_class,
|
49
|
+
batch: batch,
|
50
|
+
}) do
|
51
|
+
model_class.destroy_bulk_batch(batch, options)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def model_class
|
58
|
+
try(:model) || self
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def destroy(*args, legacy: false, **kwargs)
|
63
|
+
if legacy
|
64
|
+
super(*args)
|
65
|
+
else
|
66
|
+
self.class.send(:bulk_destroy_internal, [self], **kwargs)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# These classes are some Hackery to allow us to use callbacks against the Model classes instead of Model instances
|
73
|
+
class ClassCallbackExector
|
74
|
+
include ActiveSupport::Callbacks
|
75
|
+
|
76
|
+
attr_reader :callback_class
|
77
|
+
delegate :__callbacks, to: :callback_class
|
78
|
+
delegate_missing_to :callback_class
|
79
|
+
|
80
|
+
def initialize(cls, env)
|
81
|
+
@callback_class = cls
|
82
|
+
env.keys.each do |k|
|
83
|
+
define_singleton_method(k) do
|
84
|
+
env[k]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
@options = options
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.run_callbacks(cls, callback, env={}, &blk)
|
91
|
+
new(cls, env).run_callbacks(callback, &blk)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module AdvancedAR
|
2
|
+
class BatchingCsvProcessor
|
3
|
+
attr_accessor :csv, :file_name
|
4
|
+
|
5
|
+
class RowError < StandardError; end
|
6
|
+
|
7
|
+
def initialize(csv, file_name: nil)
|
8
|
+
@csv = csv
|
9
|
+
@file_name = file_name
|
10
|
+
end
|
11
|
+
|
12
|
+
def process_in_batches(&blk)
|
13
|
+
batch = BatchProcessor.new(&blk)
|
14
|
+
CSV.new(csv, headers: true, header_converters: :symbol).each.with_index do |row, line|
|
15
|
+
row[:line_number] = line + 1
|
16
|
+
next unless validate_line(row)
|
17
|
+
|
18
|
+
batch << row
|
19
|
+
end
|
20
|
+
batch.flush
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_row_errors(row); end
|
24
|
+
|
25
|
+
def find_or_init(_row)
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
def apply_row_to_model(_row, _instance)
|
30
|
+
raise NotImplementedError
|
31
|
+
end
|
32
|
+
|
33
|
+
def log_line_error(message, line_number, **kwargs)
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate_line(row)
|
38
|
+
errors = get_row_errors(row) || []
|
39
|
+
if errors.present?
|
40
|
+
log_line_error(errors[0], row[:line_number])
|
41
|
+
false
|
42
|
+
else
|
43
|
+
true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def batch_rows_to_models(rows)
|
48
|
+
rows.map { |row| build_model_from_row(row) }.reject! { |inst| inst.nil? || !inst.changed? }
|
49
|
+
end
|
50
|
+
|
51
|
+
def build_model_from_row(row)
|
52
|
+
model = find_or_init(row)
|
53
|
+
apply_row_to_model(row, model)
|
54
|
+
return nil if model.respond_to?(:discarded_at) && model.discarded_at.present? && !model.persisted?
|
55
|
+
|
56
|
+
model
|
57
|
+
rescue ActiveRecord::RecordNotFound, RowError => err
|
58
|
+
log_line_error(err.message, row[:line_number], exception: err)
|
59
|
+
nil
|
60
|
+
rescue StandardError => err
|
61
|
+
log_line_error('An Internal Error Occurred', row[:line_number], exception: err)
|
62
|
+
Raven.capture_exception(err)
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
|
66
|
+
def map_defined_columns(row, map)
|
67
|
+
newh = {}
|
68
|
+
map.each do |newk, oldk|
|
69
|
+
next unless row.include?(oldk)
|
70
|
+
|
71
|
+
newh[newk] = row[oldk]
|
72
|
+
end
|
73
|
+
newh
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.file_matches?(file)
|
77
|
+
header = file.readline.strip.split(',')
|
78
|
+
file.try(:rewind)
|
79
|
+
headers_match?(header)
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.headers_match?(headers)
|
83
|
+
(self::HEADERS - headers).empty?
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# Example Usage (Preloading items with reference to the same Polymorphic Object):
|
2
|
+
#
|
3
|
+
# has_many :related_objects, -> (self) { where(poly_type: self.poly_type, poly_id: self.poly_id) }, preloader: 'RelatedObjectPreloader'
|
4
|
+
#
|
5
|
+
# class RelatedObjectPreloader < ActiveRecord::Associations::Preloader::Association
|
6
|
+
# def run(preloader)
|
7
|
+
# @preloaded_records = []
|
8
|
+
# owners.group_by(&:poly_type).each do |type, owner_group|
|
9
|
+
# ids = owner_group.map(&:poly_id)
|
10
|
+
#
|
11
|
+
# ids_to_priors = {}
|
12
|
+
# scope = Poly.scope_for_association.where(poly_type: self.poly_type, poly_id: self.poly_id)
|
13
|
+
# scope.find_each do |pa|
|
14
|
+
# ids_to_priors[pa.poly_id] ||= []
|
15
|
+
# ids_to_priors[pa.poly_id] << pa
|
16
|
+
# @preloaded_records << pa
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# owner_group.each do |owner|
|
20
|
+
# priors = ids_to_priors[owner.poly_id] || []
|
21
|
+
#
|
22
|
+
# association = owner.association(reflection.name)
|
23
|
+
# association.loaded!
|
24
|
+
# association.target = priors
|
25
|
+
#
|
26
|
+
# # association.set_inverse_instance(record)
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
module AdvancedAR::CustomPreloaders
|
33
|
+
module AssociationBuilderExtension
|
34
|
+
def self.build(model, reflection); end
|
35
|
+
|
36
|
+
def self.valid_options
|
37
|
+
[:preloader]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module PreloaderExtension
|
42
|
+
def preloader_for(reflection, owners)
|
43
|
+
cust_preloader = reflection.options[:preloader]
|
44
|
+
if cust_preloader.present?
|
45
|
+
cust_preloader = cust_preloader.constantize if cust_preloader.is_a?(String)
|
46
|
+
cust_preloader
|
47
|
+
else
|
48
|
+
super
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.install
|
54
|
+
ActiveRecord::Associations::Builder::Association.extensions << AssociationBuilderExtension
|
55
|
+
ActiveRecord::Associations::Preloader.prepend(CustomPreloaders::PreloaderExtension)
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,421 @@
|
|
1
|
+
module AdvancedAR
|
2
|
+
class ParamValidator
|
3
|
+
attr_accessor :context, :options, :errors
|
4
|
+
|
5
|
+
delegate_missing_to :context
|
6
|
+
|
7
|
+
TIME_TYPES = [Date, DateTime, Time].freeze
|
8
|
+
|
9
|
+
CHECKS = %i[type specified present default transform in block items pattern].freeze
|
10
|
+
NON_PREFIXED = %i[default transform type message].freeze
|
11
|
+
PREFIXES = %i[all onem onep one none].freeze
|
12
|
+
PREFIX_ALIASES = { any: :onep, not: :none }.freeze
|
13
|
+
ALL_PREFIXES = (PREFIXES + PREFIX_ALIASES.keys).freeze
|
14
|
+
VALID_FLAGS = %i[present specified].freeze
|
15
|
+
|
16
|
+
def initialize(block, context, parameters = nil, options = nil)
|
17
|
+
@block = block
|
18
|
+
@context = context
|
19
|
+
@params = parameters || context.params
|
20
|
+
@subkeys = []
|
21
|
+
@options = options || {}
|
22
|
+
@errors = {}
|
23
|
+
@explicit_parameters = []
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.check(params, context: nil, &blk)
|
27
|
+
pv = new(blk, context, params)
|
28
|
+
pv.apply_checks
|
29
|
+
pv.errors
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.assert(params, context: nil, handle:, &blk)
|
33
|
+
errors = check(params, context: context, &blk)
|
34
|
+
if errors.present?
|
35
|
+
handle.call(errors)
|
36
|
+
else
|
37
|
+
params
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def apply_checks(&blk)
|
42
|
+
blk ||= @block
|
43
|
+
args = trim_arguments(blk, [params, @subkeys[-1]])
|
44
|
+
instance_exec(*args, &blk)
|
45
|
+
end
|
46
|
+
|
47
|
+
def parameter(param_keys, *args, **kwargs, &blk)
|
48
|
+
param_keys = Array(param_keys)
|
49
|
+
opts = normalize_opts(*args, **kwargs, &blk)
|
50
|
+
|
51
|
+
checks = {}
|
52
|
+
PREFIXES.each do |pfx|
|
53
|
+
pfx_keys = opts[pfx]&.keys&.select { |k| opts[pfx][k] } || []
|
54
|
+
pfx_keys.each do |k|
|
55
|
+
checks[k] = pfx # TODO: Support filters connected to multiple prefixes
|
56
|
+
end
|
57
|
+
# TODO: warn if pfx != :all && param_keys.length == 1
|
58
|
+
end
|
59
|
+
NON_PREFIXED.each do |k|
|
60
|
+
checks[k] = nil
|
61
|
+
end
|
62
|
+
|
63
|
+
all_results = {}
|
64
|
+
param_keys.each do |pk|
|
65
|
+
check_results = all_results[pk] = {}
|
66
|
+
run_check = ->(check, &blk) { exec_check(check_results, check, checks, options: opts, &blk) }
|
67
|
+
|
68
|
+
exec_check(check_results, :type) { coerce_type(params, pk, opts) } || next
|
69
|
+
|
70
|
+
run_check[:specified] { params.key?(pk) || 'must be specified' } || next
|
71
|
+
run_check[:present] { params[pk].present? || 'must be present' } || next
|
72
|
+
|
73
|
+
# Set Default
|
74
|
+
if params[pk].nil? && !opts[:default].nil?
|
75
|
+
params[pk] ||= opts[:default].respond_to?(:call) ? opts[:default].call : opts[:default]
|
76
|
+
next # We can assume that the default value is allowed
|
77
|
+
end
|
78
|
+
|
79
|
+
# Apply Transform
|
80
|
+
params[pk] = opts[:transform].to_proc.call(params[pk]) if params.include?(pk) && opts[:transform]
|
81
|
+
|
82
|
+
next if params[pk].nil?
|
83
|
+
|
84
|
+
run_check[:pattern] do |pattern|
|
85
|
+
return true if params[pk].to_s.match?(pattern)
|
86
|
+
|
87
|
+
"must match pattern: #{pattern.inspect}"
|
88
|
+
end
|
89
|
+
|
90
|
+
run_check[:in] do |one_of|
|
91
|
+
next true if one_of.include?(params[pk])
|
92
|
+
|
93
|
+
if one_of.is_a?(Range)
|
94
|
+
"must be between: #{one_of.begin}..#{one_of.end}"
|
95
|
+
else
|
96
|
+
"must be one of: #{one_of.to_a.join(', ')}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Nested check
|
101
|
+
run_check[:block] do |blk|
|
102
|
+
iterate_array = false # TODO
|
103
|
+
sub_parameter(pk) do
|
104
|
+
if params.is_a?(Array) && iterate_array
|
105
|
+
params.each_with_index do |v, i|
|
106
|
+
sub_parameter(i) { apply_checks(&blk) }
|
107
|
+
end
|
108
|
+
else
|
109
|
+
apply_checks(&blk)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Nested check
|
115
|
+
run_check[:items] do |blk|
|
116
|
+
sub_parameter(pk) do
|
117
|
+
if params.is_a?(Array)
|
118
|
+
params.each_with_index do |v, i|
|
119
|
+
sub_parameter(i) { apply_checks(&blk) }
|
120
|
+
end
|
121
|
+
else
|
122
|
+
raise "items: validator can only be used with Arrays"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
final_errors = {}
|
129
|
+
checks.each do |check, check_prefix|
|
130
|
+
if check_prefix == :all || check_prefix == nil
|
131
|
+
all_results.each do |field, err_map|
|
132
|
+
errs = err_map[check]
|
133
|
+
next unless errs.present?
|
134
|
+
|
135
|
+
final_errors[field] = merge_error_hashes(final_errors[field], errs)
|
136
|
+
end
|
137
|
+
elsif check_prefix == :none
|
138
|
+
all_results.each do |field, err_map|
|
139
|
+
errs = err_map[check]
|
140
|
+
final_errors[field] = merge_error_hashes(final_errors[field], "must NOT be #{check}") unless errs.present?
|
141
|
+
end
|
142
|
+
else
|
143
|
+
counts = check_pass_count(check, all_results)
|
144
|
+
field_key = param_keys.join(', ')
|
145
|
+
string_prefixes = {
|
146
|
+
onep: 'One or more of',
|
147
|
+
onem: 'At most one of',
|
148
|
+
one: 'Exactly one of',
|
149
|
+
}
|
150
|
+
|
151
|
+
if (counts[:passed] != 1 && check_prefix == :one) ||
|
152
|
+
(counts[:passed] > 1 && check_prefix == :onem) ||
|
153
|
+
(counts[:passed] < 1 && check_prefix == :onep)
|
154
|
+
|
155
|
+
final_errors = merge_error_hashes(final_errors, "#{string_prefixes[check_prefix]} #{field_key} #{check}")
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
@errors = merge_error_hashes(@errors, final_errors)
|
161
|
+
final_errors
|
162
|
+
end
|
163
|
+
|
164
|
+
alias p parameter
|
165
|
+
|
166
|
+
protected
|
167
|
+
|
168
|
+
def check_pass_count(check, all_results)
|
169
|
+
counts = { passed: 0, failed: 0, skipped: 0 }
|
170
|
+
all_results.each do |_field, field_results|
|
171
|
+
result = field_results[check]
|
172
|
+
key = if result.nil?
|
173
|
+
:skipped
|
174
|
+
elsif result.present?
|
175
|
+
:failed
|
176
|
+
else
|
177
|
+
:passed
|
178
|
+
end
|
179
|
+
counts[key] += 1
|
180
|
+
end
|
181
|
+
counts
|
182
|
+
end
|
183
|
+
|
184
|
+
def exec_check(state, check, checks_to_run = nil, options: nil, &blk)
|
185
|
+
return true if checks_to_run && !checks_to_run[check] && !NON_PREFIXED.include?(check)
|
186
|
+
|
187
|
+
# TODO: Support Running checks of the same type for different prefixes
|
188
|
+
|
189
|
+
check_prefixes = NON_PREFIXED.include?(check) ? [nil] : Array(checks_to_run&.[](check))
|
190
|
+
return true unless check_prefixes.present?
|
191
|
+
|
192
|
+
check_prefixes.each do |check_prefix|
|
193
|
+
initial_errors = @errors
|
194
|
+
@errors = []
|
195
|
+
prefix_options = (check_prefix.nil? ? options : options&.[](check_prefix)) || {}
|
196
|
+
args = trim_arguments(blk, [prefix_options[check]])
|
197
|
+
|
198
|
+
result = yield(*args)
|
199
|
+
result = "failed validation #{check}" if result == false
|
200
|
+
|
201
|
+
if result.present? && result != true
|
202
|
+
result = options[:message] if options&.[](:message).present?
|
203
|
+
Array(result).each do |e|
|
204
|
+
@errors << e
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
state[check] = merge_error_hashes(state[check], @errors)
|
209
|
+
@errors = initial_errors
|
210
|
+
end
|
211
|
+
|
212
|
+
!state[check].present?
|
213
|
+
end
|
214
|
+
|
215
|
+
def coerce_type(params, key, opts)
|
216
|
+
value = params[key]
|
217
|
+
return nil if value.nil? || !opts[:type].present?
|
218
|
+
|
219
|
+
types = Array(opts[:type])
|
220
|
+
types.each do |t|
|
221
|
+
params[key] = coerce_single_type(value, t, opts)
|
222
|
+
return true
|
223
|
+
rescue ArgumentError, TypeError => err
|
224
|
+
end
|
225
|
+
|
226
|
+
"'#{value}' could not be cast to a #{types.join(' or a ')}"
|
227
|
+
end
|
228
|
+
|
229
|
+
def coerce_single_type(param, type, options)
|
230
|
+
return param if (param.is_a?(type) rescue false)
|
231
|
+
|
232
|
+
if (param.is_a?(Array) && type != Array) || ((param.is_a?(Hash) || param.is_a?(ActionController::Parameters)) && type != Hash)
|
233
|
+
raise ArgumentError
|
234
|
+
end
|
235
|
+
return param if (param.is_a?(ActionController::Parameters) && type == Hash rescue false)
|
236
|
+
|
237
|
+
# Primitives
|
238
|
+
return Integer(param) if type == Integer
|
239
|
+
return Float(param) if type == Float
|
240
|
+
return Float(param) if type == Numeric
|
241
|
+
return String(param) if type == String
|
242
|
+
|
243
|
+
# Date/Time
|
244
|
+
if TIME_TYPES.include? type
|
245
|
+
if options[:format].present?
|
246
|
+
return type.strptime(param, options[:format])
|
247
|
+
else
|
248
|
+
return type.parse(param)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# Arrays/Hashes
|
253
|
+
raise ArgumentError if (type == Array || type == Hash) && !param.respond_to?(:split)
|
254
|
+
return Array(param.split(options[:delimiter] || ',')) if type == Array
|
255
|
+
return Hash[param.split(options[:delimiter] || ',').map { |c| c.split(options[:separator] || ':') }] if type == Hash
|
256
|
+
|
257
|
+
# Booleans
|
258
|
+
if [TrueClass, FalseClass, :boolean, :bool].include?(type)
|
259
|
+
return false if /^(false|f|no|n|0)$/i === param.to_s
|
260
|
+
return true if /^(true|t|yes|y|1)$/i === param.to_s
|
261
|
+
|
262
|
+
raise ArgumentError
|
263
|
+
end
|
264
|
+
|
265
|
+
# BigDecimals
|
266
|
+
if type == BigDecimal
|
267
|
+
param = param.delete('$,').strip.to_f if param.is_a?(String)
|
268
|
+
return BigDecimal(param, (options[:precision] || DEFAULT_PRECISION))
|
269
|
+
end
|
270
|
+
nil
|
271
|
+
end
|
272
|
+
|
273
|
+
def normalize_opts(*args, **kwargs, &blk)
|
274
|
+
# Stage 1 - Convert args to kwargs
|
275
|
+
args, norm = convert_positional_args(args, kwargs)
|
276
|
+
if blk.present?
|
277
|
+
type = args.delete(:items) ? :all_items : :all_block
|
278
|
+
set_hash_key(norm, type, blk)
|
279
|
+
end
|
280
|
+
set_hash_key(norm, :type, args.pop(0)) if args.present?
|
281
|
+
|
282
|
+
# Stage 2
|
283
|
+
norm = convert_flags(norm)
|
284
|
+
|
285
|
+
# Stage 3
|
286
|
+
norm = convert_prefixed_keys(norm)
|
287
|
+
|
288
|
+
extra_kwargs = norm.keys - PREFIXES - NON_PREFIXED
|
289
|
+
raise ArgumentError, "Unrecognized postitional arguments: #{args.inspect}" if args.present?
|
290
|
+
raise ArgumentError, "Unrecognized keyword arguments: #{extra_kwargs.inspect}" if extra_kwargs.present?
|
291
|
+
|
292
|
+
norm
|
293
|
+
end
|
294
|
+
|
295
|
+
def convert_positional_args(args, kwargs)
|
296
|
+
dest_hash = { **kwargs }
|
297
|
+
args = args.reject do |arg|
|
298
|
+
next false unless arg.is_a?(Symbol)
|
299
|
+
|
300
|
+
flag, pfx = split_key(arg)
|
301
|
+
next false unless VALID_FLAGS.include?(flag) && (pfx.nil? || PREFIXES.include?(pfx))
|
302
|
+
|
303
|
+
set_hash_key(dest_hash, :"#{pfx || 'all'}_#{flag}", true)
|
304
|
+
true
|
305
|
+
end
|
306
|
+
[args, dest_hash]
|
307
|
+
end
|
308
|
+
|
309
|
+
def convert_flags(h)
|
310
|
+
dest_hash = {}
|
311
|
+
h.each do |k, v|
|
312
|
+
if VALID_FLAGS.include?(k) && normalize_prefix(v)
|
313
|
+
set_hash_key(dest_hash, :"#{normalize_prefix(v)}_#{k}", true)
|
314
|
+
else
|
315
|
+
dest_hash[k] = v
|
316
|
+
end
|
317
|
+
end
|
318
|
+
dest_hash
|
319
|
+
end
|
320
|
+
|
321
|
+
def convert_prefixed_keys(h)
|
322
|
+
dest_hash = {}
|
323
|
+
|
324
|
+
h.each do |k, v|
|
325
|
+
flag, pfx = split_key(k)
|
326
|
+
if !CHECKS.include?(flag)
|
327
|
+
dest_hash[k] = v
|
328
|
+
elsif NON_PREFIXED.include?(flag)
|
329
|
+
# TODO: Raise warning if pfx
|
330
|
+
dest_hash[k] = v
|
331
|
+
elsif normalize_prefix(flag)
|
332
|
+
dest_hash[flag] = merge_hashes(dest_hash[normalize_prefix(flag)] || {}, v)
|
333
|
+
elsif pfx.nil? || PREFIXES.include?(pfx)
|
334
|
+
pfx ||= :all
|
335
|
+
dest_hash[pfx] ||= {}
|
336
|
+
set_hash_key(dest_hash[pfx], flag, v)
|
337
|
+
else
|
338
|
+
dest_hash[k] = v
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
dest_hash
|
343
|
+
end
|
344
|
+
|
345
|
+
def normalize_prefix(prefix)
|
346
|
+
prefix = PREFIX_ALIASES[prefix] if PREFIX_ALIASES.include?(prefix)
|
347
|
+
return nil unless PREFIXES.include?(prefix)
|
348
|
+
|
349
|
+
prefix
|
350
|
+
end
|
351
|
+
|
352
|
+
def split_key(key)
|
353
|
+
skey = key.to_s
|
354
|
+
ALL_PREFIXES.each do |pfx|
|
355
|
+
spfx = pfx.to_s
|
356
|
+
next unless skey.starts_with?("#{spfx}_")
|
357
|
+
|
358
|
+
return [skey[(spfx.length + 1)..-1].to_sym, PREFIX_ALIASES[pfx] || pfx]
|
359
|
+
end
|
360
|
+
[key, nil]
|
361
|
+
end
|
362
|
+
|
363
|
+
def sub_parameter(k)
|
364
|
+
@subkeys.push(k)
|
365
|
+
yield
|
366
|
+
ensure
|
367
|
+
@subkeys.pop
|
368
|
+
end
|
369
|
+
|
370
|
+
def params
|
371
|
+
p = @params
|
372
|
+
@subkeys.each { |k| p = p[k] }
|
373
|
+
p
|
374
|
+
end
|
375
|
+
|
376
|
+
def merge_error_hashes(target, from)
|
377
|
+
target ||= []
|
378
|
+
if target.is_a?(Hash)
|
379
|
+
ta = []
|
380
|
+
th = target
|
381
|
+
else
|
382
|
+
ta = target
|
383
|
+
th = target[-1].is_a?(Hash) ? ta.pop : {}
|
384
|
+
end
|
385
|
+
|
386
|
+
if from.is_a?(Hash)
|
387
|
+
from.each_pair do |k, v|
|
388
|
+
th[k] = merge_error_hashes(th[k], v)
|
389
|
+
end
|
390
|
+
elsif from.is_a?(Array)
|
391
|
+
merge_error_hashes(th, from.pop) if from[-1].is_a?(Hash)
|
392
|
+
from.each { |f| ta << f }
|
393
|
+
else
|
394
|
+
ta << from
|
395
|
+
end
|
396
|
+
|
397
|
+
return th if !ta.present? && th.present?
|
398
|
+
|
399
|
+
ta << th if th.present?
|
400
|
+
ta
|
401
|
+
end
|
402
|
+
|
403
|
+
def merge_hashes(h1, h2)
|
404
|
+
h2.each do |k, v|
|
405
|
+
set_hash_key(h1, k, v)
|
406
|
+
end
|
407
|
+
h1
|
408
|
+
end
|
409
|
+
|
410
|
+
def set_hash_key(h, k, v)
|
411
|
+
k = k.to_sym
|
412
|
+
# TODO: warn if h[k].present?
|
413
|
+
h[k] = v
|
414
|
+
end
|
415
|
+
|
416
|
+
def trim_arguments(blk, args)
|
417
|
+
return args if blk.arity.negative?
|
418
|
+
args[0..(blk.arity.abs - 1)]
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|