axn 0.1.0.pre.alpha.2.5.3.1 → 0.1.0.pre.alpha.2.6
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/CHANGELOG.md +12 -1
- data/README.md +2 -11
- data/docs/reference/configuration.md +15 -4
- data/docs/reference/instance.md +2 -2
- data/docs/strategies/index.md +1 -1
- data/docs/usage/setup.md +1 -1
- data/docs/usage/writing.md +8 -8
- data/lib/action/attachable.rb +3 -3
- data/lib/action/{core/configuration.rb → configuration.rb} +1 -1
- data/lib/action/context.rb +28 -0
- data/lib/action/core/automatic_logging.rb +77 -0
- data/lib/action/core/context_facade.rb +1 -1
- data/lib/action/core/contract.rb +153 -214
- data/lib/action/core/contract_for_subfields.rb +84 -82
- data/lib/action/core/contract_validation.rb +51 -0
- data/lib/action/core/handle_exceptions.rb +102 -122
- data/lib/action/core/hoist_errors.rb +42 -40
- data/lib/action/core/hooks.rb +123 -0
- data/lib/action/core/logging.rb +22 -20
- data/lib/action/core/timing.rb +22 -0
- data/lib/action/core/use_strategy.rb +19 -17
- data/lib/action/core.rb +153 -0
- data/lib/action/enqueueable.rb +1 -1
- data/lib/action/{core/exceptions.rb → exceptions.rb} +1 -19
- data/lib/axn/factory.rb +0 -12
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +10 -47
- metadata +10 -19
- data/lib/action/core/top_level_around_hook.rb +0 -108
data/lib/action/core/contract.rb
CHANGED
@@ -7,264 +7,203 @@ require "action/core/validation/fields"
|
|
7
7
|
require "action/core/context_facade"
|
8
8
|
|
9
9
|
module Action
|
10
|
-
module
|
11
|
-
|
12
|
-
base
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
# Remove public context accessor
|
20
|
-
remove_method :context
|
21
|
-
|
22
|
-
around do |hooked|
|
23
|
-
_apply_inbound_preprocessing!
|
24
|
-
_apply_defaults!(:inbound)
|
25
|
-
_validate_contract!(:inbound)
|
26
|
-
hooked.call
|
27
|
-
_apply_defaults!(:outbound)
|
28
|
-
_validate_contract!(:outbound)
|
10
|
+
module Core
|
11
|
+
module Contract
|
12
|
+
def self.included(base)
|
13
|
+
base.class_eval do
|
14
|
+
class_attribute :internal_field_configs, :external_field_configs, default: []
|
15
|
+
|
16
|
+
extend ClassMethods
|
17
|
+
include InstanceMethods
|
29
18
|
end
|
30
19
|
end
|
31
|
-
end
|
32
|
-
|
33
|
-
FieldConfig = Data.define(:field, :validations, :default, :preprocess, :sensitive)
|
34
|
-
|
35
|
-
module ClassMethods
|
36
|
-
def expects(
|
37
|
-
*fields,
|
38
|
-
on: nil,
|
39
|
-
allow_blank: false,
|
40
|
-
allow_nil: false,
|
41
|
-
default: nil,
|
42
|
-
preprocess: nil,
|
43
|
-
sensitive: false,
|
44
|
-
**validations
|
45
|
-
)
|
46
|
-
return _expects_subfields(*fields, on:, allow_blank:, allow_nil:, default:, preprocess:, sensitive:, **validations) if on.present?
|
47
|
-
|
48
|
-
fields.each do |field|
|
49
|
-
raise ContractViolation::ReservedAttributeError, field if RESERVED_FIELD_NAMES_FOR_EXPECTATIONS.include?(field.to_s)
|
50
|
-
end
|
51
20
|
|
52
|
-
|
53
|
-
duplicated = internal_field_configs.map(&:field) & configs.map(&:field)
|
54
|
-
raise Action::DuplicateFieldError, "Duplicate field(s) declared: #{duplicated.join(", ")}" if duplicated.any?
|
21
|
+
FieldConfig = Data.define(:field, :validations, :default, :preprocess, :sensitive)
|
55
22
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
23
|
+
module ClassMethods
|
24
|
+
def expects(
|
25
|
+
*fields,
|
26
|
+
on: nil,
|
27
|
+
allow_blank: false,
|
28
|
+
allow_nil: false,
|
29
|
+
default: nil,
|
30
|
+
preprocess: nil,
|
31
|
+
sensitive: false,
|
32
|
+
**validations
|
33
|
+
)
|
34
|
+
return _expects_subfields(*fields, on:, allow_blank:, allow_nil:, default:, preprocess:, sensitive:, **validations) if on.present?
|
60
35
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
allow_nil: false,
|
65
|
-
default: nil,
|
66
|
-
sensitive: false,
|
67
|
-
**validations
|
68
|
-
)
|
69
|
-
fields.each do |field|
|
70
|
-
raise ContractViolation::ReservedAttributeError, field if RESERVED_FIELD_NAMES_FOR_EXPOSURES.include?(field.to_s)
|
71
|
-
end
|
36
|
+
fields.each do |field|
|
37
|
+
raise ContractViolation::ReservedAttributeError, field if RESERVED_FIELD_NAMES_FOR_EXPECTATIONS.include?(field.to_s)
|
38
|
+
end
|
72
39
|
|
73
|
-
|
74
|
-
|
75
|
-
|
40
|
+
_parse_field_configs(*fields, allow_blank:, allow_nil:, default:, preprocess:, sensitive:, **validations).tap do |configs|
|
41
|
+
duplicated = internal_field_configs.map(&:field) & configs.map(&:field)
|
42
|
+
raise Action::DuplicateFieldError, "Duplicate field(s) declared: #{duplicated.join(", ")}" if duplicated.any?
|
76
43
|
|
77
|
-
|
78
|
-
|
44
|
+
# NOTE: avoid <<, which would update value for parents and children
|
45
|
+
self.internal_field_configs += configs
|
46
|
+
end
|
79
47
|
end
|
80
|
-
end
|
81
48
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
ok error success message
|
94
|
-
].freeze
|
95
|
-
|
96
|
-
def _parse_field_configs(
|
97
|
-
*fields,
|
98
|
-
allow_blank: false,
|
99
|
-
allow_nil: false,
|
100
|
-
default: nil,
|
101
|
-
preprocess: nil,
|
102
|
-
sensitive: false,
|
103
|
-
**validations
|
104
|
-
)
|
105
|
-
_parse_field_validations(*fields, allow_nil:, allow_blank:, **validations).map do |field, parsed_validations|
|
106
|
-
_define_field_reader(field)
|
107
|
-
_define_model_reader(field, parsed_validations[:model]) if parsed_validations.key?(:model)
|
108
|
-
FieldConfig.new(field:, validations: parsed_validations, default:, preprocess:, sensitive:)
|
109
|
-
end
|
110
|
-
end
|
49
|
+
def exposes(
|
50
|
+
*fields,
|
51
|
+
allow_blank: false,
|
52
|
+
allow_nil: false,
|
53
|
+
default: nil,
|
54
|
+
sensitive: false,
|
55
|
+
**validations
|
56
|
+
)
|
57
|
+
fields.each do |field|
|
58
|
+
raise ContractViolation::ReservedAttributeError, field if RESERVED_FIELD_NAMES_FOR_EXPOSURES.include?(field.to_s)
|
59
|
+
end
|
111
60
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
cached_val = instance_variable_get(ivar)
|
116
|
-
return cached_val if cached_val.present?
|
61
|
+
_parse_field_configs(*fields, allow_blank:, allow_nil:, default:, preprocess: nil, sensitive:, **validations).tap do |configs|
|
62
|
+
duplicated = external_field_configs.map(&:field) & configs.map(&:field)
|
63
|
+
raise Action::DuplicateFieldError, "Duplicate field(s) declared: #{duplicated.join(", ")}" if duplicated.any?
|
117
64
|
|
118
|
-
|
119
|
-
|
65
|
+
# NOTE: avoid <<, which would update value for parents and children
|
66
|
+
self.external_field_configs += configs
|
67
|
+
end
|
120
68
|
end
|
121
|
-
end
|
122
|
-
|
123
|
-
def _define_field_reader(field)
|
124
|
-
# Allow local access to explicitly-expected fields -- even externally-expected needs to be available locally
|
125
|
-
# (e.g. to allow success message callable to reference exposed fields)
|
126
|
-
define_method(field) { internal_context.public_send(field) }
|
127
|
-
end
|
128
|
-
|
129
|
-
def _define_model_reader(field, klass)
|
130
|
-
name = field.to_s.delete_suffix("_id")
|
131
|
-
raise ArgumentError, "Model validation expects to be given a field ending in _id (given: #{field})" unless field.to_s.end_with?("_id")
|
132
|
-
raise ArgumentError, "Failed to define model reader - #{name} is already defined" if method_defined?(name)
|
133
69
|
|
134
|
-
|
135
|
-
|
70
|
+
private
|
71
|
+
|
72
|
+
RESERVED_FIELD_NAMES_FOR_EXPECTATIONS = %w[
|
73
|
+
fail! success? ok?
|
74
|
+
inspect default_error
|
75
|
+
each_pair
|
76
|
+
].freeze
|
77
|
+
|
78
|
+
RESERVED_FIELD_NAMES_FOR_EXPOSURES = %w[
|
79
|
+
fail! success? ok?
|
80
|
+
inspect each_pair default_error
|
81
|
+
ok error success message
|
82
|
+
].freeze
|
83
|
+
|
84
|
+
def _parse_field_configs(
|
85
|
+
*fields,
|
86
|
+
allow_blank: false,
|
87
|
+
allow_nil: false,
|
88
|
+
default: nil,
|
89
|
+
preprocess: nil,
|
90
|
+
sensitive: false,
|
91
|
+
**validations
|
92
|
+
)
|
93
|
+
_parse_field_validations(*fields, allow_nil:, allow_blank:, **validations).map do |field, parsed_validations|
|
94
|
+
_define_field_reader(field)
|
95
|
+
_define_model_reader(field, parsed_validations[:model]) if parsed_validations.key?(:model)
|
96
|
+
FieldConfig.new(field:, validations: parsed_validations, default:, preprocess:, sensitive:)
|
97
|
+
end
|
136
98
|
end
|
137
|
-
end
|
138
99
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
v = { value: v } unless v.is_a?(Hash)
|
148
|
-
{ allow_blank: true }.merge(v)
|
149
|
-
end
|
150
|
-
elsif allow_nil
|
151
|
-
validations.transform_values! do |v|
|
152
|
-
v = { value: v } unless v.is_a?(Hash)
|
153
|
-
{ allow_nil: true }.merge(v)
|
100
|
+
def define_memoized_reader_method(field, &block)
|
101
|
+
define_method(field) do
|
102
|
+
ivar = :"@_memoized_reader_#{field}"
|
103
|
+
cached_val = instance_variable_get(ivar)
|
104
|
+
return cached_val if cached_val.present?
|
105
|
+
|
106
|
+
value = instance_exec(&block)
|
107
|
+
instance_variable_set(ivar, value)
|
154
108
|
end
|
155
|
-
else
|
156
|
-
validations[:presence] = true unless validations.key?(:presence) || Array(validations[:type]).include?(:boolean)
|
157
109
|
end
|
158
110
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
def internal_context = @internal_context ||= _build_context_facade(:inbound)
|
165
|
-
def external_context = @external_context ||= _build_context_facade(:outbound)
|
111
|
+
def _define_field_reader(field)
|
112
|
+
# Allow local access to explicitly-expected fields -- even externally-expected needs to be available locally
|
113
|
+
# (e.g. to allow success message callable to reference exposed fields)
|
114
|
+
define_method(field) { internal_context.public_send(field) }
|
115
|
+
end
|
166
116
|
|
167
|
-
|
168
|
-
|
169
|
-
|
117
|
+
def _define_model_reader(field, klass)
|
118
|
+
name = field.to_s.delete_suffix("_id")
|
119
|
+
raise ArgumentError, "Model validation expects to be given a field ending in _id (given: #{field})" unless field.to_s.end_with?("_id")
|
120
|
+
raise ArgumentError, "Failed to define model reader - #{name} is already defined" if method_defined?(name)
|
170
121
|
|
171
|
-
|
172
|
-
|
173
|
-
if args.any?
|
174
|
-
if args.size != 2
|
175
|
-
raise ArgumentError,
|
176
|
-
"expose must be called with exactly two positional arguments (or a hash of key/value pairs)"
|
122
|
+
define_memoized_reader_method(name) do
|
123
|
+
Validators::ModelValidator.instance_for(field:, klass:, id: public_send(field))
|
177
124
|
end
|
178
|
-
|
179
|
-
kwargs.merge!(args.first => args.last)
|
180
125
|
end
|
181
126
|
|
182
|
-
|
183
|
-
|
127
|
+
def _parse_field_validations(
|
128
|
+
*fields,
|
129
|
+
allow_nil: false,
|
130
|
+
allow_blank: false,
|
131
|
+
**validations
|
132
|
+
)
|
133
|
+
if allow_blank
|
134
|
+
validations.transform_values! do |v|
|
135
|
+
v = { value: v } unless v.is_a?(Hash)
|
136
|
+
{ allow_blank: true }.merge(v)
|
137
|
+
end
|
138
|
+
elsif allow_nil
|
139
|
+
validations.transform_values! do |v|
|
140
|
+
v = { value: v } unless v.is_a?(Hash)
|
141
|
+
{ allow_nil: true }.merge(v)
|
142
|
+
end
|
143
|
+
else
|
144
|
+
validations[:presence] = true unless validations.key?(:presence) || Array(validations[:type]).include?(:boolean)
|
145
|
+
end
|
184
146
|
|
185
|
-
|
147
|
+
fields.map { |field| [field, validations] }
|
186
148
|
end
|
187
149
|
end
|
188
150
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
raise ArgumentError, "Invalid direction: #{direction}" unless %i[inbound outbound].include?(direction)
|
151
|
+
module InstanceMethods
|
152
|
+
def internal_context = @internal_context ||= _build_context_facade(:inbound)
|
153
|
+
def result = @result ||= _build_context_facade(:outbound)
|
193
154
|
|
194
|
-
|
195
|
-
|
155
|
+
# Accepts either two positional arguments (key, value) or a hash of key/value pairs
|
156
|
+
def expose(*args, **kwargs)
|
157
|
+
if args.any?
|
158
|
+
if args.size != 2
|
159
|
+
raise ArgumentError,
|
160
|
+
"expose must be called with exactly two positional arguments (or a hash of key/value pairs)"
|
161
|
+
end
|
196
162
|
|
197
|
-
|
198
|
-
|
199
|
-
end
|
163
|
+
kwargs.merge!(args.first => args.last)
|
164
|
+
end
|
200
165
|
|
201
|
-
|
202
|
-
|
203
|
-
internal_field_configs.each do |config|
|
204
|
-
next unless config.preprocess
|
166
|
+
kwargs.each do |key, value|
|
167
|
+
raise Action::ContractViolation::UnknownExposure, key unless result.respond_to?(key)
|
205
168
|
|
206
|
-
|
207
|
-
|
208
|
-
@context.public_send("#{config.field}=", new_value)
|
209
|
-
rescue StandardError => e
|
210
|
-
raise Action::ContractViolation::PreprocessingError, "Error preprocessing field '#{config.field}': #{e.message}"
|
169
|
+
@context.public_send("#{key}=", value)
|
170
|
+
end
|
211
171
|
end
|
212
|
-
end
|
213
|
-
|
214
|
-
def _validate_contract!(direction)
|
215
|
-
raise ArgumentError, "Invalid direction: #{direction}" unless %i[inbound outbound].include?(direction)
|
216
172
|
|
217
|
-
|
218
|
-
|
219
|
-
hash[config.field] = config.validations
|
173
|
+
def context_for_logging(direction = nil)
|
174
|
+
inspection_filter.filter(@context.to_h.slice(*_declared_fields(direction)))
|
220
175
|
end
|
221
|
-
context = direction == :inbound ? internal_context : external_context
|
222
|
-
exception_klass = direction == :inbound ? Action::InboundValidationError : Action::OutboundValidationError
|
223
176
|
|
224
|
-
|
225
|
-
end
|
177
|
+
private
|
226
178
|
|
227
|
-
|
228
|
-
|
179
|
+
def _build_context_facade(direction)
|
180
|
+
raise ArgumentError, "Invalid direction: #{direction}" unless %i[inbound outbound].include?(direction)
|
229
181
|
|
230
|
-
|
231
|
-
|
232
|
-
hash[config.field] = config.default
|
233
|
-
end.compact
|
182
|
+
klass = direction == :inbound ? Action::InternalContext : Action::Result
|
183
|
+
implicitly_allowed_fields = direction == :inbound ? _declared_fields(:outbound) : []
|
234
184
|
|
235
|
-
|
236
|
-
next if @context.public_send(field).present?
|
237
|
-
|
238
|
-
default_value = default_value_getter.respond_to?(:call) ? instance_exec(&default_value_getter) : default_value_getter
|
239
|
-
|
240
|
-
@context.public_send("#{field}=", default_value)
|
185
|
+
klass.new(action: self, context: @context, declared_fields: _declared_fields(direction), implicitly_allowed_fields:)
|
241
186
|
end
|
242
|
-
end
|
243
|
-
|
244
|
-
def context_for_logging(direction = nil)
|
245
|
-
inspection_filter.filter(@context.to_h.slice(*_declared_fields(direction)))
|
246
|
-
end
|
247
|
-
|
248
|
-
protected
|
249
187
|
|
250
|
-
|
251
|
-
|
252
|
-
|
188
|
+
def inspection_filter
|
189
|
+
@inspection_filter ||= ActiveSupport::ParameterFilter.new(sensitive_fields)
|
190
|
+
end
|
253
191
|
|
254
|
-
|
255
|
-
|
256
|
-
|
192
|
+
def sensitive_fields
|
193
|
+
(internal_field_configs + external_field_configs).select(&:sensitive).map(&:field)
|
194
|
+
end
|
257
195
|
|
258
|
-
|
259
|
-
|
196
|
+
def _declared_fields(direction)
|
197
|
+
raise ArgumentError, "Invalid direction: #{direction}" unless direction.nil? || %i[inbound outbound].include?(direction)
|
260
198
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
199
|
+
configs = case direction
|
200
|
+
when :inbound then internal_field_configs
|
201
|
+
when :outbound then external_field_configs
|
202
|
+
else (internal_field_configs + external_field_configs)
|
203
|
+
end
|
266
204
|
|
267
|
-
|
205
|
+
configs.map(&:field)
|
206
|
+
end
|
268
207
|
end
|
269
208
|
end
|
270
209
|
end
|
@@ -3,103 +3,105 @@
|
|
3
3
|
require "action/core/validation/subfields"
|
4
4
|
|
5
5
|
module Action
|
6
|
-
module
|
7
|
-
|
8
|
-
|
9
|
-
|
6
|
+
module Core
|
7
|
+
module ContractForSubfields
|
8
|
+
# TODO: add default, preprocess, sensitive options for subfields?
|
9
|
+
# SubfieldConfig = Data.define(:field, :validations, :default, :preprocess, :sensitive)
|
10
|
+
SubfieldConfig = Data.define(:field, :validations, :on)
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
12
|
+
def self.included(base)
|
13
|
+
base.class_eval do
|
14
|
+
class_attribute :subfield_configs, default: []
|
14
15
|
|
15
|
-
|
16
|
-
|
16
|
+
extend ClassMethods
|
17
|
+
include InstanceMethods
|
17
18
|
|
18
|
-
|
19
|
+
before { _validate_subfields_contract! }
|
20
|
+
end
|
19
21
|
end
|
20
|
-
end
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
23
|
+
module ClassMethods
|
24
|
+
def _expects_subfields(
|
25
|
+
*fields,
|
26
|
+
on:,
|
27
|
+
readers: true,
|
28
|
+
allow_blank: false,
|
29
|
+
allow_nil: false,
|
30
|
+
|
31
|
+
# TODO: add support for these three options for subfields
|
32
|
+
default: nil,
|
33
|
+
preprocess: nil,
|
34
|
+
sensitive: false,
|
35
|
+
|
36
|
+
**validations
|
37
|
+
)
|
38
|
+
# TODO: add support for these three options for subfields
|
39
|
+
raise ArgumentError, "expects does not support :default key when also given :on" if default.present?
|
40
|
+
raise ArgumentError, "expects does not support :preprocess key when also given :on" if preprocess.present?
|
41
|
+
raise ArgumentError, "expects does not support :sensitive key when also given :on" if sensitive.present?
|
42
|
+
|
43
|
+
unless internal_field_configs.map(&:field).include?(on) || subfield_configs.map(&:field).include?(on)
|
44
|
+
raise ArgumentError,
|
45
|
+
"expects called with `on: #{on}`, but no such method exists (are you sure you've declared `expects :#{on}`?)"
|
46
|
+
end
|
47
|
+
|
48
|
+
raise ArgumentError, "expects does not support expecting fields on nested attributes (i.e. `on` cannot contain periods)" if on.to_s.include?(".")
|
49
|
+
|
50
|
+
# TODO: consider adding support for default, preprocess, sensitive options for subfields?
|
51
|
+
_parse_subfield_configs(*fields, on:, readers:, allow_blank:, allow_nil:, **validations).tap do |configs|
|
52
|
+
duplicated = subfield_configs.map(&:field) & configs.map(&:field)
|
53
|
+
raise Action::DuplicateFieldError, "Duplicate field(s) declared: #{duplicated.join(", ")}" if duplicated.any?
|
54
|
+
|
55
|
+
# NOTE: avoid <<, which would update value for parents and children
|
56
|
+
self.subfield_configs += configs
|
57
|
+
end
|
45
58
|
end
|
46
59
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
60
|
+
private
|
61
|
+
|
62
|
+
def _parse_subfield_configs(
|
63
|
+
*fields,
|
64
|
+
on:,
|
65
|
+
readers:,
|
66
|
+
allow_blank: false,
|
67
|
+
allow_nil: false,
|
68
|
+
# default: nil,
|
69
|
+
# preprocess: nil,
|
70
|
+
# sensitive: false,
|
71
|
+
**validations
|
72
|
+
)
|
73
|
+
_parse_field_validations(*fields, allow_nil:, allow_blank:, **validations).map do |field, parsed_validations|
|
74
|
+
_define_subfield_reader(field, on:, validations: parsed_validations) if readers
|
75
|
+
SubfieldConfig.new(field:, validations: parsed_validations, on:)
|
76
|
+
end
|
56
77
|
end
|
57
|
-
end
|
58
78
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
*fields,
|
63
|
-
on:,
|
64
|
-
readers:,
|
65
|
-
allow_blank: false,
|
66
|
-
allow_nil: false,
|
67
|
-
# default: nil,
|
68
|
-
# preprocess: nil,
|
69
|
-
# sensitive: false,
|
70
|
-
**validations
|
71
|
-
)
|
72
|
-
_parse_field_validations(*fields, allow_nil:, allow_blank:, **validations).map do |field, parsed_validations|
|
73
|
-
_define_subfield_reader(field, on:, validations: parsed_validations) if readers
|
74
|
-
SubfieldConfig.new(field:, validations: parsed_validations, on:)
|
75
|
-
end
|
76
|
-
end
|
79
|
+
def _define_subfield_reader(field, on:, validations:)
|
80
|
+
# Don't create top-level readers for nested fields
|
81
|
+
return if field.to_s.include?(".")
|
77
82
|
|
78
|
-
|
79
|
-
# Don't create top-level readers for nested fields
|
80
|
-
return if field.to_s.include?(".")
|
83
|
+
raise ArgumentError, "expects does not support duplicate sub-keys (i.e. `#{field}` is already defined)" if method_defined?(field)
|
81
84
|
|
82
|
-
|
85
|
+
define_memoized_reader_method(field) do
|
86
|
+
Action::Validation::Subfields.extract(field, public_send(on))
|
87
|
+
end
|
83
88
|
|
84
|
-
|
85
|
-
Action::Validation::Subfields.extract(field, public_send(on))
|
89
|
+
_define_model_reader(field, validations[:model]) if validations.key?(:model)
|
86
90
|
end
|
87
|
-
|
88
|
-
_define_model_reader(field, validations[:model]) if validations.key?(:model)
|
89
91
|
end
|
90
|
-
end
|
91
92
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
93
|
+
module InstanceMethods
|
94
|
+
def _validate_subfields_contract!
|
95
|
+
return if subfield_configs.blank?
|
96
|
+
|
97
|
+
subfield_configs.each do |config|
|
98
|
+
Validation::Subfields.validate!(
|
99
|
+
field: config.field,
|
100
|
+
validations: config.validations,
|
101
|
+
source: public_send(config.on),
|
102
|
+
exception_klass: Action::InboundValidationError,
|
103
|
+
)
|
104
|
+
end
|
103
105
|
end
|
104
106
|
end
|
105
107
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
module Core
|
5
|
+
module ContractValidation
|
6
|
+
private
|
7
|
+
|
8
|
+
def _apply_inbound_preprocessing!
|
9
|
+
internal_field_configs.each do |config|
|
10
|
+
next unless config.preprocess
|
11
|
+
|
12
|
+
initial_value = @context.public_send(config.field)
|
13
|
+
new_value = config.preprocess.call(initial_value)
|
14
|
+
@context.public_send("#{config.field}=", new_value)
|
15
|
+
rescue StandardError => e
|
16
|
+
raise Action::ContractViolation::PreprocessingError, "Error preprocessing field '#{config.field}': #{e.message}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def _validate_contract!(direction)
|
21
|
+
raise ArgumentError, "Invalid direction: #{direction}" unless %i[inbound outbound].include?(direction)
|
22
|
+
|
23
|
+
configs = direction == :inbound ? internal_field_configs : external_field_configs
|
24
|
+
validations = configs.each_with_object({}) do |config, hash|
|
25
|
+
hash[config.field] = config.validations
|
26
|
+
end
|
27
|
+
context = direction == :inbound ? internal_context : result
|
28
|
+
exception_klass = direction == :inbound ? Action::InboundValidationError : Action::OutboundValidationError
|
29
|
+
|
30
|
+
Validation::Fields.validate!(validations:, context:, exception_klass:)
|
31
|
+
end
|
32
|
+
|
33
|
+
def _apply_defaults!(direction)
|
34
|
+
raise ArgumentError, "Invalid direction: #{direction}" unless %i[inbound outbound].include?(direction)
|
35
|
+
|
36
|
+
configs = direction == :inbound ? internal_field_configs : external_field_configs
|
37
|
+
defaults_mapping = configs.each_with_object({}) do |config, hash|
|
38
|
+
hash[config.field] = config.default
|
39
|
+
end.compact
|
40
|
+
|
41
|
+
defaults_mapping.each do |field, default_value_getter|
|
42
|
+
next if @context.public_send(field).present?
|
43
|
+
|
44
|
+
default_value = default_value_getter.respond_to?(:call) ? instance_exec(&default_value_getter) : default_value_getter
|
45
|
+
|
46
|
+
@context.public_send("#{field}=", default_value)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|