axn 0.1.0.pre.alpha.2.8 → 0.1.0.pre.alpha.3

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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/rules/axn-framework-patterns.mdc +43 -0
  3. data/.cursor/rules/general-coding-standards.mdc +27 -0
  4. data/.cursor/rules/spec/testing-patterns.mdc +40 -0
  5. data/CHANGELOG.md +47 -0
  6. data/Rakefile +12 -2
  7. data/docs/.vitepress/config.mjs +8 -3
  8. data/docs/advanced/conventions.md +2 -2
  9. data/docs/advanced/mountable.md +562 -0
  10. data/docs/advanced/profiling.md +355 -0
  11. data/docs/advanced/rough.md +1 -1
  12. data/docs/index.md +5 -3
  13. data/docs/intro/about.md +1 -1
  14. data/docs/intro/overview.md +5 -5
  15. data/docs/recipes/memoization.md +2 -2
  16. data/docs/recipes/rubocop-integration.md +38 -284
  17. data/docs/recipes/testing.md +14 -14
  18. data/docs/recipes/validating-user-input.md +1 -1
  19. data/docs/reference/async.md +160 -0
  20. data/docs/reference/axn-result.md +107 -0
  21. data/docs/reference/class.md +123 -25
  22. data/docs/reference/configuration.md +191 -10
  23. data/docs/reference/instance.md +14 -29
  24. data/docs/strategies/index.md +21 -21
  25. data/docs/strategies/transaction.md +1 -1
  26. data/docs/usage/setup.md +14 -0
  27. data/docs/usage/steps.md +7 -7
  28. data/docs/usage/using.md +23 -12
  29. data/docs/usage/writing.md +92 -11
  30. data/lib/axn/async/adapters/active_job.rb +65 -0
  31. data/lib/axn/async/adapters/disabled.rb +26 -0
  32. data/lib/axn/async/adapters/sidekiq.rb +74 -0
  33. data/lib/axn/async/adapters.rb +26 -0
  34. data/lib/axn/async.rb +61 -0
  35. data/lib/{action → axn}/configuration.rb +21 -3
  36. data/lib/{action → axn}/context.rb +21 -4
  37. data/lib/{action → axn}/core/automatic_logging.rb +6 -6
  38. data/lib/axn/core/context/facade.rb +69 -0
  39. data/lib/{action → axn}/core/context/facade_inspector.rb +31 -4
  40. data/lib/{action → axn}/core/context/internal.rb +5 -5
  41. data/lib/{action → axn}/core/contract.rb +43 -46
  42. data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
  43. data/lib/{action → axn}/core/contract_validation.rb +16 -6
  44. data/lib/axn/core/contract_validation_for_subfields.rb +158 -0
  45. data/lib/axn/core/field_resolvers/extract.rb +32 -0
  46. data/lib/axn/core/field_resolvers/model.rb +63 -0
  47. data/lib/axn/core/field_resolvers.rb +24 -0
  48. data/lib/{action → axn}/core/flow/callbacks.rb +7 -7
  49. data/lib/{action → axn}/core/flow/exception_execution.rb +4 -13
  50. data/lib/{action → axn}/core/flow/handlers/base_descriptor.rb +3 -2
  51. data/lib/{action → axn}/core/flow/handlers/descriptors/callback_descriptor.rb +2 -2
  52. data/lib/{action → axn}/core/flow/handlers/descriptors/message_descriptor.rb +6 -6
  53. data/lib/{action → axn}/core/flow/handlers/invoker.rb +6 -6
  54. data/lib/{action → axn}/core/flow/handlers/matcher.rb +5 -5
  55. data/lib/{action → axn}/core/flow/handlers/registry.rb +3 -1
  56. data/lib/{action → axn}/core/flow/handlers/resolvers/base_resolver.rb +1 -1
  57. data/lib/{action → axn}/core/flow/handlers/resolvers/callback_resolver.rb +2 -2
  58. data/lib/{action → axn}/core/flow/handlers/resolvers/message_resolver.rb +12 -3
  59. data/lib/axn/core/flow/handlers.rb +20 -0
  60. data/lib/{action → axn}/core/flow/messages.rb +7 -7
  61. data/lib/{action → axn}/core/flow.rb +4 -4
  62. data/lib/{action → axn}/core/hooks.rb +16 -5
  63. data/lib/{action → axn}/core/logging.rb +3 -3
  64. data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
  65. data/lib/axn/core/profiling.rb +124 -0
  66. data/lib/{action → axn}/core/timing.rb +1 -1
  67. data/lib/axn/core/tracing.rb +17 -0
  68. data/lib/axn/core/use_strategy.rb +29 -0
  69. data/lib/{action → axn}/core/validation/fields.rb +26 -2
  70. data/lib/{action → axn}/core/validation/subfields.rb +14 -12
  71. data/lib/axn/core/validation/validators/model_validator.rb +36 -0
  72. data/lib/axn/core/validation/validators/type_validator.rb +80 -0
  73. data/lib/{action → axn}/core/validation/validators/validate_validator.rb +12 -2
  74. data/lib/axn/core.rb +123 -0
  75. data/lib/{action → axn}/exceptions.rb +12 -2
  76. data/lib/axn/factory.rb +102 -34
  77. data/lib/axn/internal/logging.rb +26 -0
  78. data/lib/axn/internal/registry.rb +87 -0
  79. data/lib/axn/mountable/descriptor.rb +76 -0
  80. data/lib/axn/mountable/helpers/class_builder.rb +162 -0
  81. data/lib/axn/mountable/helpers/mounter.rb +33 -0
  82. data/lib/axn/mountable/helpers/namespace_manager.rb +66 -0
  83. data/lib/axn/mountable/helpers/validator.rb +112 -0
  84. data/lib/axn/mountable/inherit_profiles.rb +72 -0
  85. data/lib/axn/mountable/mounting_strategies/_base.rb +83 -0
  86. data/lib/axn/mountable/mounting_strategies/axn.rb +48 -0
  87. data/lib/axn/mountable/mounting_strategies/enqueue_all.rb +55 -0
  88. data/lib/axn/mountable/mounting_strategies/method.rb +95 -0
  89. data/lib/axn/mountable/mounting_strategies/step.rb +69 -0
  90. data/lib/axn/mountable/mounting_strategies.rb +32 -0
  91. data/lib/axn/mountable.rb +85 -0
  92. data/lib/axn/rails/engine.rb +51 -0
  93. data/lib/axn/rails/generators/axn_generator.rb +68 -0
  94. data/lib/axn/rails/generators/templates/action.rb.erb +17 -0
  95. data/lib/axn/rails/generators/templates/action_spec.rb.erb +25 -0
  96. data/lib/{action → axn}/result.rb +30 -11
  97. data/lib/{action → axn}/strategies/transaction.rb +1 -1
  98. data/lib/axn/strategies.rb +20 -0
  99. data/lib/axn/testing/spec_helpers.rb +6 -8
  100. data/lib/axn/util/memoization.rb +20 -0
  101. data/lib/axn/version.rb +1 -1
  102. data/lib/axn.rb +17 -16
  103. data/lib/rubocop/cop/axn/README.md +23 -23
  104. data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
  105. metadata +88 -64
  106. data/.rspec +0 -3
  107. data/.rubocop.yml +0 -76
  108. data/.tool-versions +0 -1
  109. data/docs/reference/action-result.md +0 -37
  110. data/lib/action/attachable/base.rb +0 -43
  111. data/lib/action/attachable/steps.rb +0 -63
  112. data/lib/action/attachable/subactions.rb +0 -70
  113. data/lib/action/attachable.rb +0 -17
  114. data/lib/action/core/context/facade.rb +0 -48
  115. data/lib/action/core/flow/handlers.rb +0 -20
  116. data/lib/action/core/tracing.rb +0 -17
  117. data/lib/action/core/use_strategy.rb +0 -30
  118. data/lib/action/core/validation/validators/model_validator.rb +0 -34
  119. data/lib/action/core/validation/validators/type_validator.rb +0 -30
  120. data/lib/action/core.rb +0 -108
  121. data/lib/action/enqueueable/via_sidekiq.rb +0 -76
  122. data/lib/action/enqueueable.rb +0 -13
  123. data/lib/action/strategies.rb +0 -48
  124. data/lib/axn/util.rb +0 -24
  125. data/package.json +0 -10
  126. data/yarn.lock +0 -1166
@@ -1,22 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action/core/validation/subfields"
3
+ require "axn/core/validation/subfields"
4
4
 
5
- module Action
5
+ module Axn
6
6
  module Core
7
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)
8
+ SubfieldConfig = Data.define(:field, :validations, :on, :sensitive, :preprocess, :default)
11
9
 
12
10
  def self.included(base)
13
11
  base.class_eval do
14
12
  class_attribute :subfield_configs, default: []
15
13
 
16
14
  extend ClassMethods
17
- include InstanceMethods
18
-
19
- before { _validate_subfields_contract! }
20
15
  end
21
16
  end
22
17
 
@@ -27,19 +22,13 @@ module Action
27
22
  readers: true,
28
23
  allow_blank: false,
29
24
  allow_nil: false,
30
-
31
- # TODO: add support for these three options for subfields
25
+ optional: false,
32
26
  default: nil,
33
27
  preprocess: nil,
34
28
  sensitive: false,
35
29
 
36
30
  **validations
37
31
  )
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
32
  unless internal_field_configs.map(&:field).include?(on) || subfield_configs.map(&:field).include?(on)
44
33
  raise ArgumentError,
45
34
  "expects called with `on: #{on}`, but no such method exists (are you sure you've declared `expects :#{on}`?)"
@@ -47,10 +36,10 @@ module Action
47
36
 
48
37
  raise ArgumentError, "expects does not support expecting fields on nested attributes (i.e. `on` cannot contain periods)" if on.to_s.include?(".")
49
38
 
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|
39
+ _parse_subfield_configs(*fields, on:, readers:, allow_blank:, allow_nil:, optional:, preprocess:, sensitive:, default:,
40
+ **validations).tap do |configs|
52
41
  duplicated = subfield_configs.map(&:field) & configs.map(&:field)
53
- raise Action::DuplicateFieldError, "Duplicate field(s) declared: #{duplicated.join(", ")}" if duplicated.any?
42
+ raise Axn::DuplicateFieldError, "Duplicate field(s) declared: #{duplicated.join(", ")}" if duplicated.any?
54
43
 
55
44
  # NOTE: avoid <<, which would update value for parents and children
56
45
  self.subfield_configs += configs
@@ -65,14 +54,18 @@ module Action
65
54
  readers:,
66
55
  allow_blank: false,
67
56
  allow_nil: false,
68
- # default: nil,
69
- # preprocess: nil,
70
- # sensitive: false,
57
+ optional: false,
58
+ preprocess: nil,
59
+ sensitive: false,
60
+ default: nil,
71
61
  **validations
72
62
  )
63
+ # Handle optional: true by setting allow_blank: true
64
+ allow_blank ||= optional
65
+
73
66
  _parse_field_validations(*fields, allow_nil:, allow_blank:, **validations).map do |field, parsed_validations|
74
67
  _define_subfield_reader(field, on:, validations: parsed_validations) if readers
75
- SubfieldConfig.new(field:, validations: parsed_validations, on:)
68
+ SubfieldConfig.new(field:, validations: parsed_validations, on:, sensitive:, preprocess:, default:)
76
69
  end
77
70
  end
78
71
 
@@ -82,24 +75,26 @@ module Action
82
75
 
83
76
  raise ArgumentError, "expects does not support duplicate sub-keys (i.e. `#{field}` is already defined)" if method_defined?(field)
84
77
 
85
- define_memoized_reader_method(field) do
86
- Action::Validation::Subfields.extract(field, public_send(on))
78
+ Axn::Util::Memoization.define_memoized_reader_method(self, field) do
79
+ Axn::Core::FieldResolvers.resolve(type: :extract, field:, provided_data: public_send(on))
87
80
  end
88
81
 
89
- _define_model_reader(field, validations[:model]) { Action::Validation::Subfields.extract(field, public_send(on)) } if validations.key?(:model)
82
+ _define_subfield_model_reader(field, validations[:model], on:) if validations.key?(:model)
90
83
  end
91
- end
92
84
 
93
- module InstanceMethods
94
- def _validate_subfields_contract!
95
- return if subfield_configs.blank?
85
+ def _define_subfield_model_reader(field, options, on:)
86
+ # Apply the same syntactic sugar processing as the main contract system
87
+ processed_options = Axn::Validators::ModelValidator.apply_syntactic_sugar(options, [field])
88
+
89
+ Axn::Util::Memoization.define_memoized_reader_method(self, field) do
90
+ # Create a data source that contains the subfield data for the resolver
91
+ subfield_data = public_send(on)
96
92
 
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,
93
+ Axn::Core::FieldResolvers.resolve(
94
+ type: :model,
95
+ field:,
96
+ options: processed_options,
97
+ provided_data: subfield_data,
103
98
  )
104
99
  end
105
100
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Action
3
+ module Axn
4
4
  module Core
5
5
  module ContractValidation
6
6
  private
@@ -13,8 +13,10 @@ module Action
13
13
  new_value = config.preprocess.call(initial_value)
14
14
  @__context.provided_data[config.field] = new_value
15
15
  rescue StandardError => e
16
- raise Action::ContractViolation::PreprocessingError, "Error preprocessing field '#{config.field}': #{e.message}", cause: e
16
+ raise Axn::ContractViolation::PreprocessingError, "Error preprocessing field '#{config.field}': #{e.message}", cause: e
17
17
  end
18
+
19
+ _apply_inbound_preprocessing_for_subfields!
18
20
  end
19
21
 
20
22
  def _validate_contract!(direction)
@@ -25,9 +27,12 @@ module Action
25
27
  hash[config.field] = config.validations
26
28
  end
27
29
  context = direction == :inbound ? internal_context : result
28
- exception_klass = direction == :inbound ? Action::InboundValidationError : Action::OutboundValidationError
30
+ exception_klass = direction == :inbound ? Axn::InboundValidationError : Axn::OutboundValidationError
29
31
 
30
32
  Validation::Fields.validate!(validations:, context:, exception_klass:)
33
+
34
+ # Validate subfields for inbound direction
35
+ _validate_subfields_contract! if direction == :inbound
31
36
  end
32
37
 
33
38
  def _apply_defaults!(direction)
@@ -37,9 +42,9 @@ module Action
37
42
  # For outbound defaults, first copy values from provided_data for fields that are both expected and exposed
38
43
  external_field_configs.each do |config|
39
44
  field = config.field
40
- next if @__context.exposed_data[field].present? # Already has a value
45
+ next if @__context.exposed_data.key?(field) # Already has a value
41
46
 
42
- @__context.exposed_data[field] = @__context.provided_data[field] if @__context.provided_data[field].present?
47
+ @__context.exposed_data[field] = @__context.provided_data[field] if @__context.provided_data.key?(field)
43
48
  end
44
49
  end
45
50
 
@@ -50,12 +55,17 @@ module Action
50
55
 
51
56
  defaults_mapping.each do |field, default_value_getter|
52
57
  data_hash = direction == :inbound ? @__context.provided_data : @__context.exposed_data
53
- next if data_hash[field].present?
58
+ next if data_hash.key?(field) && !data_hash[field].nil?
54
59
 
55
60
  default_value = default_value_getter.respond_to?(:call) ? instance_exec(&default_value_getter) : default_value_getter
56
61
 
57
62
  data_hash[field] = default_value
63
+ rescue StandardError => e
64
+ raise Axn::ContractViolation::DefaultAssignmentError, "Error applying default for field '#{field}': #{e.message}", cause: e
58
65
  end
66
+
67
+ # Apply subfield defaults for inbound direction
68
+ _apply_defaults_for_subfields! if direction == :inbound
59
69
  end
60
70
  end
61
71
  end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Axn
4
+ module Core
5
+ module ContractValidationForSubfields
6
+ private
7
+
8
+ # Applies preprocessing to all subfield configurations
9
+ def _apply_inbound_preprocessing_for_subfields!
10
+ _for_each_relevant_subfield_config(:preprocess) do |config, parent_field, subfield, parent_value|
11
+ current_subfield_value = Axn::Core::FieldResolvers.resolve(type: :extract, field: subfield, provided_data: parent_value)
12
+ preprocessed_value = config.preprocess.call(current_subfield_value)
13
+ _update_subfield_value(parent_field, subfield, preprocessed_value)
14
+ rescue StandardError => e
15
+ raise Axn::ContractViolation::PreprocessingError, "Error preprocessing subfield '#{config.field}' on '#{config.on}': #{e.message}", cause: e
16
+ end
17
+ end
18
+
19
+ # Applies default values to all subfield configurations
20
+ def _apply_defaults_for_subfields!
21
+ _for_each_relevant_subfield_config(:default) do |config, parent_field, subfield, parent_value|
22
+ next if parent_value && !Axn::Core::FieldResolvers.resolve(type: :extract, field: subfield, provided_data: parent_value).nil?
23
+
24
+ @__context.provided_data[parent_field] = {} if parent_value.nil?
25
+
26
+ default_value = config.default.respond_to?(:call) ? instance_exec(&config.default) : config.default
27
+ _update_subfield_value(parent_field, subfield, default_value)
28
+ rescue StandardError => e
29
+ raise Axn::ContractViolation::DefaultAssignmentError, "Error applying default for subfield '#{config.field}' on '#{config.on}': #{e.message}",
30
+ cause: e
31
+ end
32
+ end
33
+
34
+ # Validates all subfield configurations against their defined validations
35
+ def _validate_subfields_contract!
36
+ _for_each_relevant_subfield_config do |config, parent_field, subfield, _parent_value|
37
+ Validation::Subfields.validate!(
38
+ field: subfield,
39
+ validations: config.validations,
40
+ source: public_send(parent_field),
41
+ exception_klass: Axn::InboundValidationError,
42
+ action: self,
43
+ )
44
+ end
45
+ end
46
+
47
+ #
48
+ # Here down - helpers for the above methods
49
+ #
50
+
51
+ # Iterates over subfield configurations, optionally filtered by attribute, yielding to a block
52
+ def _for_each_relevant_subfield_config(attribute = nil)
53
+ subfield_configs.each do |config|
54
+ next if attribute && !config.public_send(attribute)
55
+
56
+ parent_field = config.on
57
+ subfield = config.field
58
+ parent_value = @__context.provided_data[parent_field]
59
+
60
+ yield(config, parent_field, subfield, parent_value)
61
+ end
62
+ end
63
+
64
+ # Updates a subfield value, handling nested paths, hash objects, and method-based setters
65
+ def _update_subfield_value(parent_field, subfield, new_value)
66
+ parent_value = @__context.provided_data[parent_field]
67
+
68
+ if _is_nested_subfield?(subfield)
69
+ _update_nested_subfield_value(parent_field, subfield, new_value)
70
+ elsif parent_value.is_a?(Hash)
71
+ _update_simple_hash_subfield(parent_field, subfield, new_value)
72
+ elsif parent_value.respond_to?("#{subfield}=")
73
+ _update_object_subfield(parent_value, subfield, new_value)
74
+ end
75
+ end
76
+
77
+ # Checks if a subfield path contains nested access (e.g., "user.profile.name")
78
+ def _is_nested_subfield?(subfield) = subfield.to_s.include?(".")
79
+
80
+ # Parses a subfield path into an array of parts
81
+ def _parse_subfield_path(subfield) = subfield.to_s.split(".")
82
+
83
+ # Updates a simple hash subfield value
84
+ def _update_simple_hash_subfield(parent_field, subfield, new_value)
85
+ parent_value = @__context.provided_data[parent_field].dup
86
+ parent_value[subfield] = new_value
87
+ @__context.provided_data[parent_field] = parent_value
88
+ end
89
+
90
+ # Updates an object subfield using method assignment
91
+ def _update_object_subfield(parent_value, subfield, new_value)
92
+ parent_value.public_send("#{subfield}=", new_value)
93
+ end
94
+
95
+ # Updates a nested subfield value by navigating the path and creating intermediate hashes
96
+ def _update_nested_subfield_value(parent_field, subfield, new_value)
97
+ parent_value = @__context.provided_data[parent_field]
98
+ path_parts = _parse_subfield_path(subfield)
99
+
100
+ target_parent = _navigate_to_parent(parent_value, path_parts)
101
+ target_parent[path_parts.last.to_sym] = new_value
102
+ end
103
+
104
+ # Navigates to the parent of the target field, creating intermediate hashes as needed
105
+ def _navigate_to_parent(parent_value, path_parts)
106
+ path_parts[0..-2].reduce(parent_value) do |current, part|
107
+ current[part.to_sym] || current[part] || (current[part.to_sym] = {})
108
+ end
109
+ end
110
+
111
+ # Checks if a subfield exists in the parent value, handling both hash and object types
112
+ def _subfield_exists?(parent_value, subfield)
113
+ if parent_value.is_a?(Hash)
114
+ _hash_subfield_exists?(parent_value, subfield)
115
+ elsif parent_value.respond_to?(subfield)
116
+ _object_subfield_exists?(parent_value, subfield)
117
+ else
118
+ false
119
+ end
120
+ end
121
+
122
+ # Checks if a subfield exists in a hash, handling both simple and nested paths
123
+ def _hash_subfield_exists?(parent_value, subfield)
124
+ if _is_nested_subfield?(subfield)
125
+ _nested_hash_subfield_exists?(parent_value, subfield)
126
+ else
127
+ _simple_hash_subfield_exists?(parent_value, subfield)
128
+ end
129
+ end
130
+
131
+ # Checks if a simple (non-nested) hash subfield exists
132
+ def _simple_hash_subfield_exists?(parent_value, subfield)
133
+ parent_value.key?(subfield.to_sym) || parent_value.key?(subfield)
134
+ end
135
+
136
+ # Checks if a nested hash subfield exists by navigating the path
137
+ def _nested_hash_subfield_exists?(parent_value, subfield)
138
+ path_parts = _parse_subfield_path(subfield)
139
+ current = parent_value
140
+
141
+ path_parts.each do |part|
142
+ return false unless current.is_a?(Hash)
143
+ return false unless current.key?(part.to_sym) || current.key?(part)
144
+
145
+ current = current[part.to_sym] || current[part]
146
+ end
147
+
148
+ true
149
+ end
150
+
151
+ # Checks if an object subfield exists (not nil)
152
+ # This ensures we apply defaults for nil values on objects
153
+ def _object_subfield_exists?(parent_value, subfield)
154
+ !parent_value.public_send(subfield).nil?
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/indifferent_access"
4
+
5
+ module Axn
6
+ module Core
7
+ module FieldResolvers
8
+ class Extract
9
+ def initialize(field:, provided_data:, options: {})
10
+ @field = field
11
+ @options = options
12
+ @provided_data = provided_data
13
+ end
14
+
15
+ def call
16
+ # Handle method calls if the source responds to the field
17
+ return provided_data.public_send(field) if provided_data.respond_to?(field)
18
+
19
+ # For hash-like objects, use digging with indifferent access
20
+ raise "Unclear how to extract #{field} from #{provided_data.inspect}" unless provided_data.respond_to?(:dig)
21
+
22
+ base = provided_data.respond_to?(:with_indifferent_access) ? provided_data.with_indifferent_access : provided_data
23
+ base.dig(*field.to_s.split("."))
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :field, :options, :provided_data
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Axn
4
+ module Core
5
+ module FieldResolvers
6
+ class Model
7
+ def initialize(field:, options:, provided_data:)
8
+ @field = field
9
+ @options = options
10
+ @provided_data = provided_data
11
+ end
12
+
13
+ def call
14
+ provided_value.presence || derive_value
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :field, :options, :provided_data
20
+
21
+ def provided_value
22
+ @provided_value ||= provided_data[field]
23
+ end
24
+
25
+ def derive_value
26
+ return nil if id_value.blank?
27
+
28
+ # Handle different finder types
29
+ if finder.is_a?(Method)
30
+ # Method object - call it directly
31
+ finder.call(id_value)
32
+ elsif klass.respond_to?(finder)
33
+ # Symbol/string method name on the klass
34
+ klass.public_send(finder, id_value)
35
+ else
36
+ raise "Unknown finder: #{finder}"
37
+ end
38
+ rescue StandardError => e
39
+ # Log the exception but don't re-raise
40
+ finder_name = finder.is_a?(Method) ? finder.name : finder
41
+ Axn::Internal::Logging.piping_error("finding #{field} with #{finder_name}", exception: e)
42
+ nil
43
+ end
44
+
45
+ def klass
46
+ @klass ||= options[:klass]
47
+ end
48
+
49
+ def finder
50
+ @finder ||= options[:finder]
51
+ end
52
+
53
+ def id_field
54
+ @id_field ||= :"#{field}_id"
55
+ end
56
+
57
+ def id_value
58
+ @id_value ||= provided_data[id_field]
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "axn/core/field_resolvers/model"
4
+ require "axn/core/field_resolvers/extract"
5
+
6
+ module Axn
7
+ module Core
8
+ module FieldResolvers
9
+ # Registry for field resolvers
10
+ # This allows us to easily add new field types in the future
11
+ RESOLVERS = {
12
+ model: FieldResolvers::Model,
13
+ extract: FieldResolvers::Extract,
14
+ }.freeze
15
+
16
+ def self.resolve(type:, field:, provided_data:, options: {})
17
+ resolver_class = RESOLVERS[type]
18
+ raise ArgumentError, "Unknown field resolver type: #{type}" unless resolver_class
19
+
20
+ resolver_class.new(field:, options:, provided_data:).call
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action/core/flow/handlers"
4
- require "action/core/flow/handlers/resolvers/callback_resolver"
3
+ require "axn/core/flow/handlers"
4
+ require "axn/core/flow/handlers/resolvers/callback_resolver"
5
5
 
6
- module Action
6
+ module Axn
7
7
  module Core
8
8
  module Flow
9
9
  module Callbacks
10
10
  def self.included(base)
11
11
  base.class_eval do
12
- class_attribute :_callbacks_registry, default: Action::Core::Flow::Handlers::Registry.empty
12
+ class_attribute :_callbacks_registry, default: Axn::Core::Flow::Handlers::Registry.empty
13
13
 
14
14
  extend ClassMethods
15
15
  end
@@ -18,7 +18,7 @@ module Action
18
18
  module ClassMethods
19
19
  # Internal dispatcher
20
20
  def _dispatch_callbacks(event_type, action:, exception: nil)
21
- resolver = Action::Core::Flow::Handlers::Resolvers::CallbackResolver.new(
21
+ resolver = Axn::Core::Flow::Handlers::Resolvers::CallbackResolver.new(
22
22
  _callbacks_registry,
23
23
  event_type,
24
24
  action:,
@@ -48,12 +48,12 @@ module Action
48
48
  raise ArgumentError, "on_#{event_type} must be called with a block or symbol" unless block || handler
49
49
 
50
50
  # If handler is already a descriptor, use it directly
51
- entry = if handler.is_a?(Action::Core::Flow::Handlers::Descriptors::CallbackDescriptor)
51
+ entry = if handler.is_a?(Axn::Core::Flow::Handlers::Descriptors::CallbackDescriptor)
52
52
  raise ArgumentError, "Cannot pass additional configuration with prebuilt descriptor" if kwargs.any? || block
53
53
 
54
54
  handler
55
55
  else
56
- Action::Core::Flow::Handlers::Descriptors::CallbackDescriptor.build(
56
+ Axn::Core::Flow::Handlers::Descriptors::CallbackDescriptor.build(
57
57
  handler: handler || block,
58
58
  **kwargs,
59
59
  )
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Action
3
+ module Axn
4
4
  module Core
5
5
  module Flow
6
6
  module ExceptionExecution
@@ -13,11 +13,11 @@ module Action
13
13
  self.class._dispatch_callbacks(:exception, action: self, exception:)
14
14
 
15
15
  # Call any global handlers
16
- Action.config.on_exception(exception, action: self, context: context_for_logging)
16
+ Axn.config.on_exception(exception, action: self, context: context_for_logging)
17
17
  rescue StandardError => e
18
18
  # No action needed -- downstream #on_exception implementation should ideally log any internal failures, but
19
19
  # we don't want exception *handling* failures to cascade and overwrite the original exception.
20
- Axn::Util.piping_error("executing on_exception hooks", action: self, exception: e)
20
+ Axn::Internal::Logging.piping_error("executing on_exception hooks", action: self, exception: e)
21
21
  end
22
22
 
23
23
  def _trigger_on_success
@@ -39,22 +39,13 @@ module Action
39
39
  self.class._dispatch_callbacks(:error, action: self, exception: e)
40
40
 
41
41
  # on_failure handlers run ONLY for fail!
42
- if e.is_a?(Action::Failure)
42
+ if e.is_a?(Axn::Failure)
43
43
  self.class._dispatch_callbacks(:failure, action: self, exception: e)
44
44
  else
45
45
  # on_exception handlers run for ONLY for unhandled exceptions.
46
46
  _trigger_on_exception(e)
47
47
  end
48
48
  end
49
-
50
- def try
51
- yield
52
- rescue Action::Failure => e
53
- # NOTE: re-raising so we can still fail! from inside the block
54
- raise e
55
- rescue StandardError => e
56
- _trigger_on_exception(e)
57
- end
58
49
  end
59
50
  end
60
51
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action/core/flow/handlers/matcher"
3
+ require "axn/core/flow/handlers/matcher"
4
4
 
5
- module Action
5
+ module Axn
6
6
  module Core
7
7
  module Flow
8
8
  # "Handlers" doesn't feel like *quite* the right name for this, but basically things in this namespace
@@ -12,6 +12,7 @@ module Action
12
12
  def initialize(matcher: nil, handler: nil)
13
13
  @matcher = matcher
14
14
  @handler = handler
15
+ freeze
15
16
  end
16
17
 
17
18
  attr_reader :handler, :matcher
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action/core/flow/handlers/base_descriptor"
3
+ require "axn/core/flow/handlers/base_descriptor"
4
4
 
5
- module Action
5
+ module Axn
6
6
  module Core
7
7
  module Flow
8
8
  module Handlers
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action/core/flow/handlers/base_descriptor"
3
+ require "axn/core/flow/handlers/base_descriptor"
4
4
 
5
- module Action
5
+ module Axn
6
6
  module Core
7
7
  module Flow
8
8
  module Handlers
@@ -12,8 +12,8 @@ module Action
12
12
  attr_reader :prefix
13
13
 
14
14
  def initialize(matcher:, handler:, prefix: nil)
15
- super(matcher:, handler:)
16
15
  @prefix = prefix
16
+ super(matcher:, handler:)
17
17
  end
18
18
 
19
19
  def self.build(handler: nil, if: nil, unless: nil, prefix: nil, from: nil, **)
@@ -31,7 +31,7 @@ module Action
31
31
  _build_rule_for_from_condition(from),
32
32
  ].compact
33
33
 
34
- Action::Core::Flow::Handlers::Matcher.new(rules, invert: !!binding.local_variable_get(:unless))
34
+ Axn::Core::Flow::Handlers::Matcher.new(rules, invert: !!binding.local_variable_get(:unless))
35
35
  end
36
36
 
37
37
  def self._build_rule_for_from_condition(from_class)
@@ -39,10 +39,10 @@ module Action
39
39
 
40
40
  if from_class.is_a?(String)
41
41
  lambda { |exception:, **|
42
- exception.is_a?(Action::Failure) && exception.source&.class&.name == from_class
42
+ exception.is_a?(Axn::Failure) && exception.source&.class&.name == from_class
43
43
  }
44
44
  else
45
- ->(exception:, **) { exception.is_a?(Action::Failure) && exception.source.is_a?(from_class) }
45
+ ->(exception:, **) { exception.is_a?(Axn::Failure) && exception.source.is_a?(from_class) }
46
46
  end
47
47
  end
48
48
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Action
3
+ module Axn
4
4
  module Core
5
5
  module Flow
6
6
  module Handlers
@@ -14,7 +14,7 @@ module Action
14
14
 
15
15
  literal_value(handler)
16
16
  rescue StandardError => e
17
- Axn::Util.piping_error(operation, action:, exception: e)
17
+ Axn::Internal::Logging.piping_error(operation, action:, exception: e)
18
18
  end
19
19
 
20
20
  # Shared introspection helpers
@@ -40,18 +40,18 @@ module Action
40
40
  def callable?(value) = value.respond_to?(:arity)
41
41
 
42
42
  def call_symbol_handler(action:, symbol:, exception: nil)
43
- unless action.respond_to?(symbol)
43
+ unless action.respond_to?(symbol, true)
44
44
  action.warn("Ignoring apparently-invalid symbol #{symbol.inspect} -- action does not respond to method")
45
45
  return nil
46
46
  end
47
47
 
48
48
  method = action.method(symbol)
49
49
  if exception && accepts_exception_keyword?(method)
50
- action.public_send(symbol, exception:)
50
+ action.send(symbol, exception:)
51
51
  elsif exception && accepts_positional_exception?(method)
52
- action.public_send(symbol, exception)
52
+ action.send(symbol, exception)
53
53
  else
54
- action.public_send(symbol)
54
+ action.send(symbol)
55
55
  end
56
56
  end
57
57