axn 0.1.0.pre.alpha.2.8.1 → 0.1.0.pre.alpha.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/.cursor/commands/pr.md +36 -0
- data/.cursor/rules/axn-framework-patterns.mdc +43 -0
- data/.cursor/rules/general-coding-standards.mdc +27 -0
- data/.cursor/rules/spec/testing-patterns.mdc +40 -0
- data/CHANGELOG.md +57 -0
- data/Rakefile +114 -4
- data/docs/.vitepress/config.mjs +19 -10
- data/docs/advanced/conventions.md +3 -3
- data/docs/advanced/mountable.md +476 -0
- data/docs/advanced/profiling.md +351 -0
- data/docs/advanced/rough.md +27 -8
- data/docs/index.md +5 -3
- data/docs/intro/about.md +1 -1
- data/docs/intro/overview.md +6 -6
- data/docs/recipes/formatting-context-for-error-tracking.md +186 -0
- data/docs/recipes/memoization.md +103 -18
- data/docs/recipes/rubocop-integration.md +38 -284
- data/docs/recipes/testing.md +14 -14
- data/docs/recipes/validating-user-input.md +1 -1
- data/docs/reference/async.md +429 -0
- data/docs/reference/axn-result.md +107 -0
- data/docs/reference/class.md +225 -64
- data/docs/reference/configuration.md +366 -34
- data/docs/reference/form-object.md +252 -0
- data/docs/reference/instance.md +14 -29
- data/docs/strategies/client.md +212 -0
- data/docs/strategies/form.md +235 -0
- data/docs/strategies/index.md +21 -21
- data/docs/strategies/transaction.md +1 -1
- data/docs/usage/setup.md +16 -2
- data/docs/usage/steps.md +7 -7
- data/docs/usage/using.md +23 -12
- data/docs/usage/writing.md +191 -12
- data/lib/axn/async/adapters/active_job.rb +74 -0
- data/lib/axn/async/adapters/disabled.rb +41 -0
- data/lib/axn/async/adapters/sidekiq.rb +67 -0
- data/lib/axn/async/adapters.rb +26 -0
- data/lib/axn/async/batch_enqueue/config.rb +38 -0
- data/lib/axn/async/batch_enqueue.rb +99 -0
- data/lib/axn/async/enqueue_all_orchestrator.rb +363 -0
- data/lib/axn/async.rb +178 -0
- data/lib/axn/configuration.rb +113 -0
- data/lib/{action → axn}/context.rb +22 -4
- data/lib/axn/core/automatic_logging.rb +89 -0
- data/lib/axn/core/context/facade.rb +69 -0
- data/lib/{action → axn}/core/context/facade_inspector.rb +32 -5
- data/lib/{action → axn}/core/context/internal.rb +5 -5
- data/lib/{action → axn}/core/contract.rb +111 -73
- data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
- data/lib/{action → axn}/core/contract_validation.rb +27 -12
- data/lib/axn/core/contract_validation_for_subfields.rb +165 -0
- data/lib/axn/core/default_call.rb +63 -0
- data/lib/axn/core/field_resolvers/extract.rb +32 -0
- data/lib/axn/core/field_resolvers/model.rb +63 -0
- data/lib/axn/core/field_resolvers.rb +24 -0
- data/lib/{action → axn}/core/flow/callbacks.rb +7 -7
- data/lib/{action → axn}/core/flow/exception_execution.rb +9 -13
- data/lib/{action → axn}/core/flow/handlers/base_descriptor.rb +3 -2
- data/lib/{action → axn}/core/flow/handlers/descriptors/callback_descriptor.rb +2 -2
- data/lib/{action → axn}/core/flow/handlers/descriptors/message_descriptor.rb +23 -11
- data/lib/axn/core/flow/handlers/invoker.rb +47 -0
- data/lib/{action → axn}/core/flow/handlers/matcher.rb +9 -19
- data/lib/{action → axn}/core/flow/handlers/registry.rb +3 -1
- data/lib/{action → axn}/core/flow/handlers/resolvers/base_resolver.rb +1 -1
- data/lib/{action → axn}/core/flow/handlers/resolvers/callback_resolver.rb +2 -2
- data/lib/{action → axn}/core/flow/handlers/resolvers/message_resolver.rb +12 -3
- data/lib/axn/core/flow/handlers.rb +20 -0
- data/lib/{action → axn}/core/flow/messages.rb +8 -8
- data/lib/{action → axn}/core/flow.rb +4 -4
- data/lib/{action → axn}/core/hooks.rb +17 -5
- data/lib/axn/core/logging.rb +48 -0
- data/lib/axn/core/memoization.rb +53 -0
- data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
- data/lib/{action → axn}/core/timing.rb +1 -1
- data/lib/axn/core/tracing.rb +90 -0
- data/lib/axn/core/use_strategy.rb +29 -0
- data/lib/{action → axn}/core/validation/fields.rb +26 -2
- data/lib/{action → axn}/core/validation/subfields.rb +14 -12
- data/lib/axn/core/validation/validators/model_validator.rb +36 -0
- data/lib/axn/core/validation/validators/type_validator.rb +80 -0
- data/lib/{action → axn}/core/validation/validators/validate_validator.rb +12 -2
- data/lib/{action → axn}/core.rb +55 -55
- data/lib/{action → axn}/exceptions.rb +12 -2
- data/lib/axn/extras/strategies/client.rb +150 -0
- data/lib/axn/extras/strategies/vernier.rb +121 -0
- data/lib/axn/extras.rb +4 -0
- data/lib/axn/factory.rb +122 -34
- data/lib/axn/form_object.rb +90 -0
- data/lib/axn/internal/logging.rb +30 -0
- data/lib/axn/internal/registry.rb +87 -0
- data/lib/axn/mountable/descriptor.rb +76 -0
- data/lib/axn/mountable/helpers/class_builder.rb +193 -0
- data/lib/axn/mountable/helpers/mounter.rb +33 -0
- data/lib/axn/mountable/helpers/namespace_manager.rb +38 -0
- data/lib/axn/mountable/helpers/validator.rb +112 -0
- data/lib/axn/mountable/inherit_profiles.rb +72 -0
- data/lib/axn/mountable/mounting_strategies/_base.rb +87 -0
- data/lib/axn/mountable/mounting_strategies/axn.rb +48 -0
- data/lib/axn/mountable/mounting_strategies/method.rb +95 -0
- data/lib/axn/mountable/mounting_strategies/step.rb +69 -0
- data/lib/axn/mountable/mounting_strategies.rb +32 -0
- data/lib/axn/mountable.rb +119 -0
- data/lib/axn/rails/engine.rb +51 -0
- data/lib/axn/rails/generators/axn_generator.rb +86 -0
- data/lib/axn/rails/generators/templates/action.rb.erb +17 -0
- data/lib/axn/rails/generators/templates/action_spec.rb.erb +25 -0
- data/lib/{action → axn}/result.rb +32 -13
- data/lib/axn/strategies/form.rb +98 -0
- data/lib/axn/strategies/transaction.rb +26 -0
- data/lib/axn/strategies.rb +20 -0
- data/lib/axn/testing/spec_helpers.rb +6 -8
- data/lib/axn/util/callable.rb +120 -0
- data/lib/axn/util/contract_error_handling.rb +32 -0
- data/lib/axn/util/execution_context.rb +34 -0
- data/lib/axn/util/global_id_serialization.rb +52 -0
- data/lib/axn/util/logging.rb +87 -0
- data/lib/axn/util/memoization.rb +20 -0
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +26 -16
- data/lib/rubocop/cop/axn/README.md +23 -23
- data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
- metadata +106 -64
- data/.rspec +0 -3
- data/.rubocop.yml +0 -76
- data/.tool-versions +0 -1
- data/docs/reference/action-result.md +0 -37
- data/lib/action/attachable/base.rb +0 -43
- data/lib/action/attachable/steps.rb +0 -63
- data/lib/action/attachable/subactions.rb +0 -70
- data/lib/action/attachable.rb +0 -17
- data/lib/action/configuration.rb +0 -55
- data/lib/action/core/automatic_logging.rb +0 -93
- data/lib/action/core/context/facade.rb +0 -48
- data/lib/action/core/flow/handlers/invoker.rb +0 -73
- data/lib/action/core/flow/handlers.rb +0 -20
- data/lib/action/core/logging.rb +0 -37
- data/lib/action/core/tracing.rb +0 -17
- data/lib/action/core/use_strategy.rb +0 -30
- data/lib/action/core/validation/validators/model_validator.rb +0 -34
- data/lib/action/core/validation/validators/type_validator.rb +0 -30
- data/lib/action/enqueueable/via_sidekiq.rb +0 -76
- data/lib/action/enqueueable.rb +0 -13
- data/lib/action/strategies/transaction.rb +0 -19
- data/lib/action/strategies.rb +0 -48
- data/lib/axn/util.rb +0 -24
- data/package.json +0 -10
- data/yarn.lock +0 -1166
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Axn
|
|
4
4
|
class Context
|
|
5
|
-
attr_accessor :provided_data, :exposed_data
|
|
6
|
-
|
|
7
5
|
def initialize(**provided_data)
|
|
8
6
|
@provided_data = provided_data
|
|
9
7
|
@exposed_data = {}
|
|
@@ -17,18 +15,38 @@ module Action
|
|
|
17
15
|
# Framework state methods
|
|
18
16
|
def ok? = !@failure
|
|
19
17
|
def failed? = @failure || false
|
|
18
|
+
def finalized? = @finalized || false
|
|
20
19
|
|
|
21
20
|
# Framework field accessors
|
|
22
|
-
attr_accessor :elapsed_time
|
|
21
|
+
attr_accessor :provided_data, :exposed_data, :elapsed_time
|
|
23
22
|
attr_reader :exception
|
|
24
23
|
private :elapsed_time=
|
|
25
24
|
|
|
25
|
+
#
|
|
26
|
+
# Here down intended for internal use only
|
|
27
|
+
#
|
|
28
|
+
|
|
26
29
|
# INTERNAL: base for further filtering (for logging) or providing user with usage hints
|
|
27
30
|
def __combined_data = @provided_data.merge(@exposed_data)
|
|
28
31
|
|
|
32
|
+
def __early_completion? = @early_completion || false
|
|
33
|
+
|
|
29
34
|
def __record_exception(e)
|
|
30
35
|
@exception = e
|
|
31
36
|
@failure = true
|
|
37
|
+
@finalized = true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def __record_early_completion(message)
|
|
41
|
+
@early_completion_message = message unless message == Axn::Internal::EarlyCompletion.new.message
|
|
42
|
+
@early_completion = true
|
|
43
|
+
@finalized = true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def __early_completion_message = @early_completion_message.presence
|
|
47
|
+
|
|
48
|
+
def __finalize!
|
|
49
|
+
@finalized = true
|
|
32
50
|
end
|
|
33
51
|
end
|
|
34
52
|
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Axn
|
|
4
|
+
module Core
|
|
5
|
+
module AutomaticLogging
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.class_eval do
|
|
8
|
+
extend ClassMethods
|
|
9
|
+
include InstanceMethods
|
|
10
|
+
|
|
11
|
+
# Single class_attribute - nil means disabled, any level means enabled
|
|
12
|
+
class_attribute :log_calls_level, default: Axn.config.log_level
|
|
13
|
+
class_attribute :log_errors_level, default: nil
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module ClassMethods
|
|
18
|
+
def log_calls(level)
|
|
19
|
+
self.log_calls_level = level.presence
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def log_errors(level)
|
|
23
|
+
self.log_errors_level = level.presence
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module InstanceMethods
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def _with_logging
|
|
31
|
+
_log_before if self.class.log_calls_level
|
|
32
|
+
yield
|
|
33
|
+
ensure
|
|
34
|
+
_log_after if self.class.log_calls_level || self.class.log_errors_level
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def _log_before
|
|
38
|
+
Axn::Util::Logging.log_at_level(
|
|
39
|
+
self.class,
|
|
40
|
+
level: self.class.log_calls_level,
|
|
41
|
+
message_parts: ["About to execute"],
|
|
42
|
+
join_string: " with: ",
|
|
43
|
+
before: _top_level_separator,
|
|
44
|
+
error_context: "logging before hook",
|
|
45
|
+
context_direction: :inbound,
|
|
46
|
+
context_instance: self,
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def _log_after
|
|
51
|
+
# Check log_calls_level first (logs all outcomes)
|
|
52
|
+
if self.class.log_calls_level
|
|
53
|
+
_log_after_at_level(self.class.log_calls_level)
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Check log_errors_level (only logs when result.ok? is false)
|
|
58
|
+
return unless self.class.log_errors_level && !result.ok?
|
|
59
|
+
|
|
60
|
+
_log_after_at_level(self.class.log_errors_level)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def _log_after_at_level(level)
|
|
64
|
+
Axn::Util::Logging.log_at_level(
|
|
65
|
+
self.class,
|
|
66
|
+
level:,
|
|
67
|
+
message_parts: [
|
|
68
|
+
"Execution completed (with outcome: #{result.outcome}) in #{result.elapsed_time} milliseconds",
|
|
69
|
+
],
|
|
70
|
+
join_string: ". Set: ",
|
|
71
|
+
after: _top_level_separator,
|
|
72
|
+
error_context: "logging after hook",
|
|
73
|
+
context_direction: :outbound,
|
|
74
|
+
context_instance: self,
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def _top_level_separator
|
|
79
|
+
return if Axn.config.env.production?
|
|
80
|
+
return if Axn::Util::ExecutionContext.background?
|
|
81
|
+
return if Axn::Util::ExecutionContext.console?
|
|
82
|
+
return if NestingTracking._current_axn_stack.size > 1
|
|
83
|
+
|
|
84
|
+
"\n------\n"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/parameter_filter"
|
|
4
|
+
|
|
5
|
+
module Axn
|
|
6
|
+
class ContextFacade
|
|
7
|
+
def initialize(action:, context:, declared_fields:, implicitly_allowed_fields: nil)
|
|
8
|
+
if self.class.name == "Axn::ContextFacade" # rubocop:disable Style/ClassEqualityComparison
|
|
9
|
+
raise "Axn::ContextFacade is an abstract class and should not be instantiated directly"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
@context = context
|
|
13
|
+
@action = action
|
|
14
|
+
@declared_fields = declared_fields
|
|
15
|
+
|
|
16
|
+
(@declared_fields + Array(implicitly_allowed_fields)).each do |field|
|
|
17
|
+
if _model_fields.key?(field)
|
|
18
|
+
_define_model_field_method(field, _model_fields[field])
|
|
19
|
+
else
|
|
20
|
+
singleton_class.define_method(field) do
|
|
21
|
+
_context_data_source[field]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attr_reader :declared_fields
|
|
28
|
+
|
|
29
|
+
def inspect = ContextFacadeInspector.new(facade: self, action:, context:).call
|
|
30
|
+
|
|
31
|
+
def fail!(...)
|
|
32
|
+
raise Axn::ContractViolation::MethodNotAllowed, "Call fail! directly rather than on the context"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
attr_reader :action, :context
|
|
38
|
+
|
|
39
|
+
def _model_fields
|
|
40
|
+
action.internal_field_configs.each_with_object({}) do |config, hash|
|
|
41
|
+
hash[config.field] = config.validations[:model] if config.validations.key?(:model)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def action_name = @action.class.name.presence || "The action"
|
|
46
|
+
|
|
47
|
+
def _define_model_field_method(field, options)
|
|
48
|
+
Axn::Util::Memoization.define_memoized_reader_method(singleton_class, field) do
|
|
49
|
+
Axn::Core::FieldResolvers.resolve(
|
|
50
|
+
type: :model,
|
|
51
|
+
field:,
|
|
52
|
+
options:,
|
|
53
|
+
provided_data: _context_data_source,
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def _context_data_source = raise NotImplementedError
|
|
59
|
+
|
|
60
|
+
def _msg_resolver(event_type, exception:)
|
|
61
|
+
Axn::Core::Flow::Handlers::Resolvers::MessageResolver.new(
|
|
62
|
+
action._messages_registry,
|
|
63
|
+
event_type,
|
|
64
|
+
action:,
|
|
65
|
+
exception:,
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Axn
|
|
4
4
|
class ContextFacadeInspector
|
|
5
5
|
def initialize(action:, facade:, context:)
|
|
6
6
|
@action = action
|
|
@@ -19,12 +19,12 @@ module Action
|
|
|
19
19
|
attr_reader :action, :facade, :context
|
|
20
20
|
|
|
21
21
|
def status
|
|
22
|
-
return unless facade.is_a?(
|
|
22
|
+
return unless facade.is_a?(Axn::Result)
|
|
23
23
|
|
|
24
24
|
return "[OK]" if context.ok?
|
|
25
25
|
|
|
26
|
-
if
|
|
27
|
-
return context.exception.
|
|
26
|
+
if facade.outcome.failure?
|
|
27
|
+
return context.exception.default_message? ? "[failed]" : "[failed with '#{context.exception.message}']"
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
%([failed with #{context.exception.class.name}: '#{context.exception.message}'])
|
|
@@ -56,9 +56,36 @@ module Action
|
|
|
56
56
|
value.inspect
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
# Handle subfield filtering for hash values
|
|
60
|
+
if value.is_a?(Hash) && sensitive_subfields?(field)
|
|
61
|
+
filtered_value = filter_subfields(field, value)
|
|
62
|
+
return filtered_value.inspect
|
|
63
|
+
end
|
|
64
|
+
|
|
59
65
|
inspection_filter.filter_param(field, inspected_value)
|
|
60
66
|
end
|
|
61
67
|
|
|
62
|
-
def inspection_filter = action.
|
|
68
|
+
def inspection_filter = action.class.inspection_filter
|
|
69
|
+
|
|
70
|
+
def sensitive_subfields?(field)
|
|
71
|
+
action.subfield_configs.any? { |config| config.on == field && config.sensitive }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def filter_subfields(field, value)
|
|
75
|
+
# Build a nested structure with subfield paths for filtering
|
|
76
|
+
nested_data = { field => value }
|
|
77
|
+
|
|
78
|
+
# Create a filter with the subfield paths
|
|
79
|
+
sensitive_subfield_paths = action.subfield_configs
|
|
80
|
+
.select { |config| config.on == field && config.sensitive }
|
|
81
|
+
.map { |config| "#{field}.#{config.field}" }
|
|
82
|
+
|
|
83
|
+
return value if sensitive_subfield_paths.empty?
|
|
84
|
+
|
|
85
|
+
subfield_filter = ActiveSupport::ParameterFilter.new(sensitive_subfield_paths)
|
|
86
|
+
filtered_data = subfield_filter.filter(nested_data)
|
|
87
|
+
|
|
88
|
+
filtered_data[field]
|
|
89
|
+
end
|
|
63
90
|
end
|
|
64
91
|
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "axn/core/context/facade"
|
|
4
4
|
|
|
5
|
-
module
|
|
5
|
+
module Axn
|
|
6
6
|
# Inbound / Internal ContextFacade
|
|
7
7
|
class InternalContext < ContextFacade
|
|
8
|
-
def default_error = _msg_resolver(:error, exception:
|
|
8
|
+
def default_error = _msg_resolver(:error, exception: Axn::Failure.new).resolve_default_message
|
|
9
9
|
def default_success = _msg_resolver(:success, exception: nil).resolve_default_message
|
|
10
10
|
|
|
11
11
|
private
|
|
@@ -15,13 +15,13 @@ module Action
|
|
|
15
15
|
def method_missing(method_name, ...) # rubocop:disable Style/MissingRespondToMissing (because we're not actually responding to anything additional)
|
|
16
16
|
if @context.__combined_data.key?(method_name.to_sym)
|
|
17
17
|
msg = <<~MSG
|
|
18
|
-
Method ##{method_name} is not available on
|
|
18
|
+
Method ##{method_name} is not available on Axn::InternalContext!
|
|
19
19
|
|
|
20
20
|
#{action_name} may be missing a line like:
|
|
21
21
|
expects :#{method_name}
|
|
22
22
|
MSG
|
|
23
23
|
|
|
24
|
-
raise
|
|
24
|
+
raise Axn::ContractViolation::MethodNotAllowed, msg
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
super
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
require "active_support/core_ext/enumerable"
|
|
4
4
|
require "active_support/core_ext/module/delegation"
|
|
5
5
|
|
|
6
|
-
require "
|
|
7
|
-
require "
|
|
8
|
-
require "
|
|
6
|
+
require "axn/core/validation/fields"
|
|
7
|
+
require "axn/result"
|
|
8
|
+
require "axn/core/context/internal"
|
|
9
9
|
|
|
10
|
-
module
|
|
10
|
+
module Axn
|
|
11
11
|
module Core
|
|
12
12
|
module Contract
|
|
13
13
|
def self.included(base)
|
|
@@ -27,20 +27,21 @@ module Action
|
|
|
27
27
|
on: nil,
|
|
28
28
|
allow_blank: false,
|
|
29
29
|
allow_nil: false,
|
|
30
|
+
optional: false,
|
|
30
31
|
default: nil,
|
|
31
32
|
preprocess: nil,
|
|
32
33
|
sensitive: false,
|
|
33
34
|
**validations
|
|
34
35
|
)
|
|
35
|
-
return _expects_subfields(*fields, on:, allow_blank:, allow_nil:, default:, preprocess:, sensitive:, **validations) if on.present?
|
|
36
|
-
|
|
37
36
|
fields.each do |field|
|
|
38
37
|
raise ContractViolation::ReservedAttributeError, field if RESERVED_FIELD_NAMES_FOR_EXPECTATIONS.include?(field.to_s)
|
|
39
38
|
end
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
return _expects_subfields(*fields, on:, allow_blank:, allow_nil:, optional:, default:, preprocess:, sensitive:, **validations) if on.present?
|
|
41
|
+
|
|
42
|
+
_parse_field_configs(*fields, allow_blank:, allow_nil:, optional:, default:, preprocess:, sensitive:, **validations).tap do |configs|
|
|
42
43
|
duplicated = internal_field_configs.map(&:field) & configs.map(&:field)
|
|
43
|
-
raise
|
|
44
|
+
raise Axn::DuplicateFieldError, "Duplicate field(s) declared: #{duplicated.join(', ')}" if duplicated.any?
|
|
44
45
|
|
|
45
46
|
# NOTE: avoid <<, which would update value for parents and children
|
|
46
47
|
self.internal_field_configs += configs
|
|
@@ -51,6 +52,7 @@ module Action
|
|
|
51
52
|
*fields,
|
|
52
53
|
allow_blank: false,
|
|
53
54
|
allow_nil: false,
|
|
55
|
+
optional: false,
|
|
54
56
|
default: nil,
|
|
55
57
|
sensitive: false,
|
|
56
58
|
**validations
|
|
@@ -59,92 +61,108 @@ module Action
|
|
|
59
61
|
raise ContractViolation::ReservedAttributeError, field if RESERVED_FIELD_NAMES_FOR_EXPOSURES.include?(field.to_s)
|
|
60
62
|
end
|
|
61
63
|
|
|
62
|
-
_parse_field_configs(*fields, allow_blank:, allow_nil:, default:, preprocess: nil, sensitive:, **validations).tap do |configs|
|
|
64
|
+
_parse_field_configs(*fields, allow_blank:, allow_nil:, optional:, default:, preprocess: nil, sensitive:, **validations).tap do |configs|
|
|
63
65
|
duplicated = external_field_configs.map(&:field) & configs.map(&:field)
|
|
64
|
-
raise
|
|
66
|
+
raise Axn::DuplicateFieldError, "Duplicate field(s) declared: #{duplicated.join(', ')}" if duplicated.any?
|
|
65
67
|
|
|
66
68
|
# NOTE: avoid <<, which would update value for parents and children
|
|
67
69
|
self.external_field_configs += configs
|
|
68
70
|
end
|
|
69
71
|
end
|
|
70
72
|
|
|
73
|
+
def inspection_filter
|
|
74
|
+
@inspection_filter ||= ActiveSupport::ParameterFilter.new(sensitive_fields)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def sensitive_fields
|
|
78
|
+
(internal_field_configs + external_field_configs + subfield_configs).select(&:sensitive).map(&:field)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def _declared_fields(direction)
|
|
82
|
+
raise ArgumentError, "Invalid direction: #{direction}" unless direction.nil? || %i[inbound outbound].include?(direction)
|
|
83
|
+
|
|
84
|
+
configs = case direction
|
|
85
|
+
when :inbound then internal_field_configs
|
|
86
|
+
when :outbound then external_field_configs
|
|
87
|
+
else (internal_field_configs + external_field_configs)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
configs.map(&:field)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def context_for_logging(data:, direction: nil)
|
|
94
|
+
inspection_filter.filter(data.slice(*_declared_fields(direction)))
|
|
95
|
+
end
|
|
96
|
+
|
|
71
97
|
private
|
|
72
98
|
|
|
73
99
|
RESERVED_FIELD_NAMES_FOR_EXPECTATIONS = %w[
|
|
74
100
|
fail! ok?
|
|
75
101
|
inspect default_error
|
|
76
102
|
each_pair
|
|
103
|
+
default_success
|
|
104
|
+
action_name
|
|
77
105
|
].freeze
|
|
78
106
|
|
|
79
107
|
RESERVED_FIELD_NAMES_FOR_EXPOSURES = %w[
|
|
80
108
|
fail! ok?
|
|
81
109
|
inspect each_pair default_error
|
|
82
110
|
ok error success message
|
|
111
|
+
result
|
|
112
|
+
outcome
|
|
113
|
+
exception
|
|
114
|
+
elapsed_time
|
|
115
|
+
finalized?
|
|
116
|
+
__action__
|
|
83
117
|
].freeze
|
|
84
118
|
|
|
85
119
|
def _parse_field_configs(
|
|
86
120
|
*fields,
|
|
87
121
|
allow_blank: false,
|
|
88
122
|
allow_nil: false,
|
|
123
|
+
optional: false,
|
|
89
124
|
default: nil,
|
|
90
125
|
preprocess: nil,
|
|
91
126
|
sensitive: false,
|
|
92
127
|
**validations
|
|
93
128
|
)
|
|
129
|
+
# Handle optional: true by setting allow_blank: true
|
|
130
|
+
allow_blank ||= optional
|
|
131
|
+
|
|
94
132
|
_parse_field_validations(*fields, allow_nil:, allow_blank:, **validations).map do |field, parsed_validations|
|
|
95
133
|
_define_field_reader(field)
|
|
96
|
-
_define_model_reader(field, parsed_validations[:model]) if parsed_validations.key?(:model)
|
|
97
134
|
FieldConfig.new(field:, validations: parsed_validations, default:, preprocess:, sensitive:)
|
|
98
135
|
end
|
|
99
136
|
end
|
|
100
137
|
|
|
101
|
-
def define_memoized_reader_method(field, &block)
|
|
102
|
-
define_method(field) do
|
|
103
|
-
ivar = :"@_memoized_reader_#{field}"
|
|
104
|
-
cached_val = instance_variable_get(ivar)
|
|
105
|
-
return cached_val if cached_val.present?
|
|
106
|
-
|
|
107
|
-
value = instance_exec(&block)
|
|
108
|
-
instance_variable_set(ivar, value)
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
138
|
def _define_field_reader(field)
|
|
113
139
|
# Allow local access to explicitly-expected fields -- even externally-expected needs to be available locally
|
|
114
140
|
# (e.g. to allow success message callable to reference exposed fields)
|
|
115
141
|
define_method(field) { internal_context.public_send(field) }
|
|
116
142
|
end
|
|
117
143
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
raise ArgumentError, "Model validation expects to be given a field ending in _id (given: #{field})" unless field.to_s.end_with?("_id")
|
|
121
|
-
raise ArgumentError, "Failed to define model reader - #{name} is already defined" if method_defined?(name)
|
|
122
|
-
|
|
123
|
-
id_extractor ||= -> { public_send(field) }
|
|
124
|
-
|
|
125
|
-
define_memoized_reader_method(name) do
|
|
126
|
-
Validators::ModelValidator.instance_for(field:, klass:, id: instance_exec(&id_extractor))
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
|
|
144
|
+
# This method applies any top-level options to each of the individual validations given.
|
|
145
|
+
# It also allows our custom validators to accept a direct value rather than a hash of options.
|
|
130
146
|
def _parse_field_validations(
|
|
131
147
|
*fields,
|
|
132
148
|
allow_nil: false,
|
|
133
149
|
allow_blank: false,
|
|
134
150
|
**validations
|
|
135
151
|
)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
152
|
+
# Apply syntactic sugar for our custom validators (convert shorthand to full hash of options)
|
|
153
|
+
validations[:type] = Axn::Validators::TypeValidator.apply_syntactic_sugar(validations[:type], fields) if validations.key?(:type)
|
|
154
|
+
validations[:model] = Axn::Validators::ModelValidator.apply_syntactic_sugar(validations[:model], fields) if validations.key?(:model)
|
|
155
|
+
validations[:validate] = Axn::Validators::ValidateValidator.apply_syntactic_sugar(validations[:validate], fields) if validations.key?(:validate)
|
|
156
|
+
|
|
157
|
+
# Push allow_blank and allow_nil to the individual validations
|
|
158
|
+
if allow_blank || allow_nil
|
|
142
159
|
validations.transform_values! do |v|
|
|
143
|
-
|
|
144
|
-
{ allow_nil: true }.merge(v)
|
|
160
|
+
{ allow_blank:, allow_nil: }.merge(v)
|
|
145
161
|
end
|
|
146
162
|
else
|
|
147
|
-
|
|
163
|
+
# Apply default presence validation (unless the type is boolean or params)
|
|
164
|
+
type_values = Array(validations.dig(:type, :klass))
|
|
165
|
+
validations[:presence] = true unless validations.key?(:presence) || type_values.include?(:boolean) || type_values.include?(:params)
|
|
148
166
|
end
|
|
149
167
|
|
|
150
168
|
fields.map { |field| [field, validations] }
|
|
@@ -169,59 +187,79 @@ module Action
|
|
|
169
187
|
end
|
|
170
188
|
|
|
171
189
|
kwargs.each do |key, value|
|
|
172
|
-
raise
|
|
190
|
+
raise Axn::ContractViolation::UnknownExposure, key unless result.respond_to?(key)
|
|
173
191
|
|
|
174
192
|
@__context.exposed_data[key] = value
|
|
175
193
|
end
|
|
176
194
|
end
|
|
177
195
|
|
|
196
|
+
# Set additional context to be included in exception logging
|
|
197
|
+
# This context is only used when exceptions occur, not in normal pre/post logging
|
|
198
|
+
def set_logging_context(**kwargs)
|
|
199
|
+
@__additional_logging_context ||= {}
|
|
200
|
+
@__additional_logging_context.merge!(kwargs)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Clear any previously set additional logging context
|
|
204
|
+
def clear_logging_context
|
|
205
|
+
@__additional_logging_context = nil
|
|
206
|
+
end
|
|
207
|
+
|
|
178
208
|
def context_for_logging(direction = nil)
|
|
179
|
-
|
|
209
|
+
base_context = self.class.context_for_logging(
|
|
210
|
+
data: @__context.__combined_data,
|
|
211
|
+
direction:,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Only merge additional context for exception logging (direction is nil)
|
|
215
|
+
# Pre/post logging don't need additional context since they only log inputs/outputs
|
|
216
|
+
return base_context if direction
|
|
217
|
+
|
|
218
|
+
# Merge both explicit setter context and hook method context (if both exist)
|
|
219
|
+
explicit_context = @__additional_logging_context || {}
|
|
220
|
+
hook_context = respond_to?(:additional_logging_context, true) ? additional_logging_context : {}
|
|
221
|
+
base_context.merge(explicit_context).merge(hook_context)
|
|
180
222
|
end
|
|
181
223
|
|
|
182
224
|
private
|
|
183
225
|
|
|
184
|
-
def
|
|
185
|
-
|
|
186
|
-
|
|
226
|
+
def _handle_early_completion_if_raised
|
|
227
|
+
yield
|
|
228
|
+
nil
|
|
229
|
+
rescue Axn::Internal::EarlyCompletion => e
|
|
230
|
+
@__context.__record_early_completion(e.message)
|
|
231
|
+
_trigger_on_success
|
|
232
|
+
true
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def _with_contract(&)
|
|
236
|
+
return if _handle_early_completion_if_raised { _apply_inbound_preprocessing! }
|
|
237
|
+
return if _handle_early_completion_if_raised { _apply_defaults!(:inbound) }
|
|
238
|
+
|
|
187
239
|
_validate_contract!(:inbound)
|
|
188
240
|
|
|
189
|
-
|
|
241
|
+
if _handle_early_completion_if_raised(&)
|
|
242
|
+
# Even with early completion, we need to validate outbound and apply defaults
|
|
243
|
+
_apply_defaults!(:outbound)
|
|
244
|
+
_validate_contract!(:outbound)
|
|
245
|
+
return
|
|
246
|
+
end
|
|
190
247
|
|
|
191
248
|
_apply_defaults!(:outbound)
|
|
192
249
|
_validate_contract!(:outbound)
|
|
193
250
|
|
|
194
251
|
# TODO: improve location of this triggering
|
|
195
|
-
|
|
252
|
+
@__context.__finalize! # Mark result as finalized
|
|
253
|
+
_trigger_on_success
|
|
196
254
|
end
|
|
197
255
|
|
|
198
256
|
def _build_context_facade(direction)
|
|
199
257
|
raise ArgumentError, "Invalid direction: #{direction}" unless %i[inbound outbound].include?(direction)
|
|
200
258
|
|
|
201
|
-
klass = direction == :inbound ?
|
|
202
|
-
implicitly_allowed_fields = direction == :inbound ? _declared_fields(:outbound) : []
|
|
203
|
-
|
|
204
|
-
klass.new(action: self, context: @__context, declared_fields: _declared_fields(direction), implicitly_allowed_fields:)
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def inspection_filter
|
|
208
|
-
@inspection_filter ||= ActiveSupport::ParameterFilter.new(sensitive_fields)
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def sensitive_fields
|
|
212
|
-
(internal_field_configs + external_field_configs).select(&:sensitive).map(&:field)
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
def _declared_fields(direction)
|
|
216
|
-
raise ArgumentError, "Invalid direction: #{direction}" unless direction.nil? || %i[inbound outbound].include?(direction)
|
|
217
|
-
|
|
218
|
-
configs = case direction
|
|
219
|
-
when :inbound then internal_field_configs
|
|
220
|
-
when :outbound then external_field_configs
|
|
221
|
-
else (internal_field_configs + external_field_configs)
|
|
222
|
-
end
|
|
259
|
+
klass = direction == :inbound ? Axn::InternalContext : Axn::Result
|
|
260
|
+
implicitly_allowed_fields = direction == :inbound ? self.class._declared_fields(:outbound) : []
|
|
223
261
|
|
|
224
|
-
|
|
262
|
+
klass.new(action: self, context: @__context, declared_fields: self.class._declared_fields(direction), implicitly_allowed_fields:)
|
|
225
263
|
end
|
|
226
264
|
end
|
|
227
265
|
end
|