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
data/.rubocop.yml DELETED
@@ -1,76 +0,0 @@
1
- # RuboCop cops are not loaded by default in this repo
2
- # Downstream consumers can enable them by adding:
3
- # require:
4
- # - axn/rubocop
5
-
6
- AllCops:
7
- TargetRubyVersion: 3.2
8
- SuggestExtensions: false
9
- NewCops: enable
10
-
11
- Style/MultilineBlockChain:
12
- Enabled: false
13
-
14
- Style/StringLiterals:
15
- Enabled: true
16
- EnforcedStyle: double_quotes
17
-
18
- Style/StringLiteralsInInterpolation:
19
- Enabled: true
20
- EnforcedStyle: double_quotes
21
-
22
- Style/Documentation:
23
- Enabled: false
24
-
25
- Style/TrailingCommaInArguments:
26
- EnforcedStyleForMultiline: comma
27
-
28
- Style/TrailingCommaInArrayLiteral:
29
- EnforcedStyleForMultiline: comma
30
-
31
- Style/TrailingCommaInHashLiteral:
32
- EnforcedStyleForMultiline: comma
33
-
34
- Style/ClassAndModuleChildren:
35
- Enabled: false
36
-
37
- Style/HashSyntax:
38
- EnforcedShorthandSyntax: always
39
-
40
- Style/DoubleNegation:
41
- Enabled: false
42
-
43
- Metrics/BlockLength:
44
- Enabled: false
45
-
46
- Metrics/ModuleLength:
47
- Enabled: false
48
-
49
- Metrics/ClassLength:
50
- Max: 150
51
-
52
- Metrics/MethodLength:
53
- Max: 70
54
-
55
- Metrics/PerceivedComplexity:
56
- Max: 20
57
-
58
- Metrics/AbcSize:
59
- Max: 60
60
-
61
- Metrics/CyclomaticComplexity:
62
- Max: 20
63
-
64
- Lint/EmptyBlock:
65
- Enabled: false
66
-
67
- Naming/MethodParameterName:
68
- AllowedNames: e, on, id, if
69
-
70
- Metrics/ParameterLists:
71
- Max: 9
72
-
73
- Layout/LineLength:
74
- Max: 160
75
-
76
-
data/.tool-versions DELETED
@@ -1 +0,0 @@
1
- ruby 3.3.6
@@ -1,37 +0,0 @@
1
- # `Action::Result`
2
-
3
- Every `call` invocation on an Action will return an `Action::Result` instance, which provides a consistent interface:
4
-
5
- | Method | Description |
6
- | -- | -- |
7
- | `ok?` | `true` if the call succeeded, `false` if not.
8
- | `error` | User-facing error message (string), if not `ok?` (else nil)
9
- | `success` | User-facing success message (string), if `ok?` (else nil)
10
- | `message` | User-facing message (string), always defined (`ok? ? success : error`)
11
- | `exception` | If not `ok?` because an exception was swallowed, will be set to the swallowed exception (note: rarely used outside development; prefer to let the library automatically handle exception handling for you)
12
- | `outcome` | The execution outcome as a string inquirer (`success?`, `failure?`, `exception?`)
13
- | `elapsed_time` | Execution time in milliseconds (Float)
14
- | any `expose`d values | guaranteed to be set if `ok?` (since they have outgoing presence validations by default; any missing would have failed the action)
15
-
16
- NOTE: `success` and `error` (and so implicitly `message`) can be configured per-action via [the `success` and `error` declarations](/reference/class#success-and-error).
17
-
18
- ### Clarification of exposed values
19
-
20
- In addition to the core interface, your Action's Result class will have methods defined to read the values of any attributes that were explicitly exposed. For example, given this action and result:
21
-
22
-
23
- ```ruby
24
- class Foo
25
- include Action
26
-
27
- exposes :bar, :baz # [!code focus]
28
-
29
- def call
30
- expose bar: 1, baz: 2
31
- end
32
- end
33
-
34
- result = Foo.call # [!code focus]
35
- ```
36
-
37
- `result` will have both `bar` and `baz` reader methods (which will return 1 and 2, respectively).
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Action
4
- module Attachable
5
- module Base
6
- extend ActiveSupport::Concern
7
-
8
- class_methods do
9
- def axn_for_attachment(
10
- attachment_type: "Action",
11
- name: nil,
12
- axn_klass: nil,
13
- superclass: nil,
14
- **kwargs,
15
- &block
16
- )
17
- raise ArgumentError, "#{attachment_type} name must be a string or symbol" unless name.is_a?(String) || name.is_a?(Symbol)
18
- raise ArgumentError, "#{attachment_type} '#{name}' must be given an existing action class or a block" if axn_klass.nil? && !block_given?
19
-
20
- if axn_klass && block_given?
21
- raise ArgumentError,
22
- "#{attachment_type} '#{name}' was given both an existing action class and a block - only one is allowed"
23
- end
24
-
25
- if axn_klass
26
- unless axn_klass.respond_to?(:<) && axn_klass < Action
27
- raise ArgumentError,
28
- "#{attachment_type} '#{name}' was given an already-existing class #{axn_klass.name} that does NOT inherit from Action as expected"
29
- end
30
-
31
- if kwargs.present?
32
- raise ArgumentError, "#{attachment_type} '#{name}' was given an existing action class and also keyword arguments - only one is allowed"
33
- end
34
-
35
- return axn_klass
36
- end
37
-
38
- Axn::Factory.build(superclass: superclass || self, name:, **kwargs, &block)
39
- end
40
- end
41
- end
42
- end
43
- end
@@ -1,63 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Action
4
- module Attachable
5
- module Steps
6
- extend ActiveSupport::Concern
7
-
8
- included do
9
- class_attribute :_axn_steps, default: []
10
- end
11
-
12
- class_methods do
13
- def steps(*steps)
14
- Array(steps).compact.each do |step|
15
- raise ArgumentError, "Step #{step} must include Action module" if step.is_a?(Class) && !step.included_modules.include?(Action) && !step < Action
16
-
17
- step("Step #{_axn_steps.length + 1}", step)
18
- end
19
- end
20
-
21
- def step(name, axn_klass = nil, error_prefix: nil, **kwargs, &block)
22
- axn_klass = axn_for_attachment(
23
- name:,
24
- axn_klass:,
25
- attachment_type: "Step",
26
- superclass: Object, # NOTE: steps skip inheriting from the wrapping class (to avoid duplicate field expectations/exposures)
27
- **kwargs,
28
- &block
29
- )
30
-
31
- # Add the step to the list of steps
32
- _axn_steps << axn_klass
33
-
34
- # Set up error handling for steps without explicit labels
35
- error_prefix ||= "#{name}: "
36
- error from: axn_klass do |e|
37
- "#{error_prefix}#{e.message}"
38
- end
39
- end
40
- end
41
-
42
- # Execute steps automatically when the action is called
43
- def call
44
- _axn_steps.each do |axn|
45
- _merge_step_exposures!(axn.call!(**_merged_context_data))
46
- end
47
- end
48
-
49
- private
50
-
51
- def _merged_context_data
52
- @__context.__combined_data
53
- end
54
-
55
- # Each step can expect the data exposed from the previous steps
56
- def _merge_step_exposures!(step_result)
57
- step_result.declared_fields.each do |field|
58
- @__context.exposed_data[field] = step_result.public_send(field)
59
- end
60
- end
61
- end
62
- end
63
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Action
4
- module Attachable
5
- module Subactions
6
- extend ActiveSupport::Concern
7
-
8
- included do
9
- class_attribute :_axnable_methods, default: {}
10
- class_attribute :_axns, default: {}
11
- end
12
-
13
- class_methods do
14
- def axnable_method(name, axn_klass = nil, **action_kwargs, &block)
15
- raise ArgumentError, "Unable to attach Axn -- '#{name}' is already taken" if respond_to?(name)
16
-
17
- self._axnable_methods = _axnable_methods.merge(name => { axn_klass:, action_kwargs:, block: })
18
-
19
- action_kwargs[:expose_return_as] ||= :value unless axn_klass
20
- axn_klass = axn_for_attachment(name:, axn_klass:, **action_kwargs, &block)
21
-
22
- define_singleton_method("#{name}_axn") do |**kwargs|
23
- axn_klass.call(**kwargs)
24
- end
25
-
26
- define_singleton_method("#{name}!") do |**kwargs|
27
- result = axn_klass.call!(**kwargs)
28
- result.public_send(action_kwargs[:expose_return_as])
29
- end
30
- end
31
-
32
- def axn(name, axn_klass = nil, **action_kwargs, &block)
33
- raise ArgumentError, "Unable to attach Axn -- '#{name}' is already taken" if respond_to?(name)
34
-
35
- self._axns = _axns.merge(name => { axn_klass:, action_kwargs:, block: })
36
-
37
- axn_klass = axn_for_attachment(name:, axn_klass:, **action_kwargs, &block)
38
-
39
- define_singleton_method(name) do |**kwargs|
40
- axn_klass.call(**kwargs)
41
- end
42
-
43
- # TODO: do we also need an instance-level version that auto-creates the appropriate `error from:` to prefix with the name?
44
-
45
- define_singleton_method("#{name}!") do |**kwargs|
46
- axn_klass.call!(**kwargs)
47
- end
48
-
49
- self._axns = _axns.merge(name => axn_klass)
50
- end
51
-
52
- def inherited(subclass)
53
- super
54
-
55
- return unless subclass.name.present? # TODO: not sure why..
56
-
57
- # Need to redefine the axnable methods on the subclass to ensure they properly reference the subclass's
58
- # helper method definitions and not the superclass's.
59
- _axnable_methods.each do |name, config|
60
- subclass.axnable_method(name, config[:axn_klass], **config[:action_kwargs], &config[:block])
61
- end
62
-
63
- _axns.each do |name, config|
64
- subclass.axn(name, config[:axn_klass], **config[:action_kwargs], &config[:block])
65
- end
66
- end
67
- end
68
- end
69
- end
70
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "action/attachable/base"
4
- require "action/attachable/steps"
5
- require "action/attachable/subactions"
6
-
7
- module Action
8
- module Attachable
9
- extend ActiveSupport::Concern
10
-
11
- included do
12
- include Base
13
- include Steps
14
- include Subactions
15
- end
16
- end
17
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_support/parameter_filter"
4
-
5
- module Action
6
- class ContextFacade
7
- def initialize(action:, context:, declared_fields:, implicitly_allowed_fields: nil)
8
- if self.class.name == "Action::ContextFacade" # rubocop:disable Style/ClassEqualityComparison
9
- raise "Action::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
- singleton_class.define_method(field) do
18
- _context_data_source[field]
19
- end
20
- end
21
- end
22
-
23
- attr_reader :declared_fields
24
-
25
- def inspect = ContextFacadeInspector.new(facade: self, action:, context:).call
26
-
27
- def fail!(...)
28
- raise Action::ContractViolation::MethodNotAllowed, "Call fail! directly rather than on the context"
29
- end
30
-
31
- private
32
-
33
- attr_reader :action, :context
34
-
35
- def action_name = @action.class.name.presence || "The action"
36
-
37
- def _context_data_source = raise NotImplementedError
38
-
39
- def _msg_resolver(event_type, exception:)
40
- Action::Core::Flow::Handlers::Resolvers::MessageResolver.new(
41
- action._messages_registry,
42
- event_type,
43
- action:,
44
- exception:,
45
- )
46
- end
47
- end
48
- end
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Action
4
- module Core
5
- module Flow
6
- module Handlers
7
- end
8
- end
9
- end
10
- end
11
-
12
- require "action/core/flow/handlers/base_descriptor"
13
- require "action/core/flow/handlers/matcher"
14
- require "action/core/flow/handlers/resolvers/base_resolver"
15
- require "action/core/flow/handlers/descriptors/message_descriptor"
16
- require "action/core/flow/handlers/descriptors/callback_descriptor"
17
- require "action/core/flow/handlers/invoker"
18
- require "action/core/flow/handlers/resolvers/callback_resolver"
19
- require "action/core/flow/handlers/registry"
20
- require "action/core/flow/handlers/resolvers/message_resolver"
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Action
4
- module Core
5
- module Tracing
6
- private
7
-
8
- def _with_tracing(&)
9
- return yield unless Action.config.wrap_with_trace
10
-
11
- Action.config.wrap_with_trace.call(self.class.name || "AnonymousClass", &)
12
- rescue StandardError => e
13
- Axn::Util.piping_error("running trace hook", action: self, exception: e)
14
- end
15
- end
16
- end
17
- end
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Action
4
- module Core
5
- module UseStrategy
6
- extend ActiveSupport::Concern
7
-
8
- class_methods do
9
- def use(strategy_name, **config, &block)
10
- strategy = Action::Strategies.all[strategy_name.to_sym]
11
- raise StrategyNotFound, "Strategy #{strategy_name} not found" if strategy.blank?
12
- raise ArgumentError, "Strategy #{strategy_name} does not support config" if config.any? && !strategy.respond_to?(:setup)
13
-
14
- # Allow dynamic setup of strategy (i.e. dynamically define module before returning)
15
- if strategy.respond_to?(:setup)
16
- configured = strategy.setup(**config, &block)
17
- raise ArgumentError, "Strategy #{strategy_name} setup method must return a module" unless configured.is_a?(Module)
18
-
19
- strategy = configured
20
- else
21
- raise ArgumentError, "Strategy #{strategy_name} does not support config (define #setup method)" if config.any?
22
- raise ArgumentError, "Strategy #{strategy_name} does not support blocks (define #setup method)" if block_given?
23
- end
24
-
25
- include strategy
26
- end
27
- end
28
- end
29
- end
30
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_model"
4
-
5
- module Action
6
- module Validators
7
- class ModelValidator < ActiveModel::EachValidator
8
- def self.model_for(field:, klass: nil)
9
- return klass if defined?(ActiveRecord::Base) && klass.is_a?(ActiveRecord::Base)
10
-
11
- field.to_s.delete_suffix("_id").classify.constantize
12
- end
13
-
14
- def self.instance_for(field:, klass:, id:)
15
- klass = model_for(field:, klass:)
16
- return unless klass.respond_to?(:find_by)
17
-
18
- klass.find_by(id:)
19
- end
20
-
21
- def validate_each(record, attribute, id)
22
- klass = self.class.model_for(field: attribute, klass: options[:with])
23
- instance = self.class.instance_for(field: attribute, klass:, id:)
24
- return if instance.present?
25
-
26
- msg = id.blank? ? "not found (given a blank ID)" : "not found for class #{klass.name} and ID #{id}"
27
- record.errors.add(attribute, msg)
28
- rescue StandardError => e
29
- Axn::Util.piping_error("applying model validation on field '#{attribute}'", exception: e)
30
- record.errors.add(attribute, "error raised while trying to find a valid #{klass.name}")
31
- end
32
- end
33
- end
34
- end
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_model"
4
-
5
- module Action
6
- module Validators
7
- class TypeValidator < ActiveModel::EachValidator
8
- def validate_each(record, attribute, value)
9
- # NOTE: the last one (:value) might be my fault from the make-it-a-hash fallback in #parse_field_configs
10
- types = options[:in].presence || Array(options[:with]).presence || Array(options[:value]).presence
11
-
12
- return if value.blank? && !types.include?(:boolean) # Handled with a separate default presence validator
13
-
14
- msg = types.size == 1 ? "is not a #{types.first}" : "is not one of #{types.join(", ")}"
15
- record.errors.add attribute, (options[:message] || msg) unless types.any? do |type|
16
- if type == :boolean
17
- [true, false].include?(value)
18
- elsif type == :uuid
19
- value.is_a?(String) && value.match?(/\A[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}\z/i)
20
- else
21
- # NOTE: allow mocks to pass type validation by default (much easier testing ergonomics)
22
- next true if Action.config.env.test? && value.class.name&.start_with?("RSpec::Mocks::")
23
-
24
- value.is_a?(type)
25
- end
26
- end
27
- end
28
- end
29
- end
30
- end
data/lib/action/core.rb DELETED
@@ -1,108 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "action/context"
4
-
5
- require "action/strategies"
6
- require "action/core/hooks"
7
- require "action/core/logging"
8
- require "action/core/flow"
9
- require "action/core/automatic_logging"
10
- require "action/core/use_strategy"
11
- require "action/core/timing"
12
- require "action/core/tracing"
13
- require "action/core/nesting_tracking"
14
-
15
- # CONSIDER: make class names match file paths?
16
- require "action/core/validation/validators/model_validator"
17
- require "action/core/validation/validators/type_validator"
18
- require "action/core/validation/validators/validate_validator"
19
-
20
- require "action/core/contract_validation"
21
- require "action/core/contract"
22
- require "action/core/contract_for_subfields"
23
-
24
- module Action
25
- module Core
26
- def self.included(base)
27
- base.class_eval do
28
- extend ClassMethods
29
- include Core::Hooks
30
- include Core::Logging
31
- include Core::AutomaticLogging
32
- include Core::Tracing
33
- include Core::Timing
34
-
35
- include Core::Flow
36
-
37
- include Core::ContractValidation
38
- include Core::Contract
39
- include Core::ContractForSubfields
40
- include Core::NestingTracking
41
-
42
- include Core::UseStrategy
43
- end
44
- end
45
-
46
- module ClassMethods
47
- def call(**)
48
- new(**).tap(&:_run).result
49
- end
50
-
51
- def call!(**)
52
- result = call(**)
53
- return result if result.ok?
54
-
55
- # When we're nested, we want to raise a failure that includes the source action to support
56
- # the error message generation's `from` filter
57
- raise Action::Failure.new(result.error, source: result.__action__), cause: result.exception if _nested_in_another_axn?
58
-
59
- raise result.exception
60
- end
61
- end
62
-
63
- def initialize(**)
64
- @__context = Action::Context.new(**)
65
- end
66
-
67
- # Main entry point for action execution
68
- def _run
69
- _tracking_nesting(self) do
70
- _with_tracing do
71
- _with_logging do
72
- _with_timing do
73
- _with_exception_handling do # Exceptions stop here; outer wrappers access result status (and must not introduce another exception layer)
74
- _with_contract do # Library internals -- any failures (e.g. contract violations) *should* fail the Action::Result
75
- _with_hooks do # User hooks -- any failures here *should* fail the Action::Result
76
- call
77
- end
78
- end
79
- end
80
- end
81
- end
82
- end
83
- end
84
- ensure
85
- _emit_metrics
86
- end
87
-
88
- # User-defined action logic - override this method in your action classes
89
- def call; end
90
-
91
- def fail!(message = nil)
92
- raise Action::Failure, message
93
- end
94
-
95
- private
96
-
97
- def _emit_metrics
98
- return unless Action.config.emit_metrics
99
-
100
- Action.config.emit_metrics.call(
101
- self.class.name || "AnonymousClass",
102
- result,
103
- )
104
- rescue StandardError => e
105
- Axn::Util.piping_error("running metrics hook", action: self, exception: e)
106
- end
107
- end
108
- end
@@ -1,76 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Action
4
- module Enqueueable
5
- module ViaSidekiq
6
- def self.included(base)
7
- base.class_eval do
8
- begin
9
- require "sidekiq"
10
- include Sidekiq::Job
11
- rescue LoadError
12
- puts "Sidekiq not available -- skipping Enqueueable"
13
- return
14
- end
15
-
16
- define_method(:perform) do |*args|
17
- context = self.class._params_from_global_id(args.first)
18
- bang = args.size > 1 ? args.last : false
19
-
20
- if bang
21
- self.class.call!(**context)
22
- else
23
- self.class.call(**context)
24
- end
25
- end
26
-
27
- def self.enqueue(context = {})
28
- perform_async(_process_context_to_sidekiq_args(context))
29
- end
30
-
31
- def self.enqueue!(context = {})
32
- perform_async(_process_context_to_sidekiq_args(context), true)
33
- end
34
-
35
- def self.queue_options(opts)
36
- opts = opts.transform_keys(&:to_s)
37
- self.sidekiq_options_hash = get_sidekiq_options.merge(opts)
38
- end
39
-
40
- private
41
-
42
- def self._process_context_to_sidekiq_args(context)
43
- client = Sidekiq::Client.new
44
-
45
- _params_to_global_id(context).tap do |args|
46
- if client.send(:json_unsafe?, args).present?
47
- raise ArgumentError,
48
- "Cannot pass non-JSON-serializable objects to Sidekiq. Make sure all expected arguments are serializable (or respond to to_global_id)."
49
- end
50
- end
51
- end
52
-
53
- def self._params_to_global_id(context)
54
- context.stringify_keys.each_with_object({}) do |(key, value), hash|
55
- if value.respond_to?(:to_global_id)
56
- hash["#{key}_as_global_id"] = value.to_global_id.to_s
57
- else
58
- hash[key] = value
59
- end
60
- end
61
- end
62
-
63
- def self._params_from_global_id(params)
64
- params.each_with_object({}) do |(key, value), hash|
65
- if key.end_with?("_as_global_id")
66
- hash[key.delete_suffix("_as_global_id")] = GlobalID::Locator.locate(value)
67
- else
68
- hash[key] = value
69
- end
70
- end.symbolize_keys
71
- end
72
- end
73
- end
74
- end
75
- end
76
- end