Class: Axn::Result

Inherits:
Core::ContextFacade
  • Object
show all
Defined in:
lib/axn/result.rb

Overview

Outbound / External ContextFacade

Constant Summary collapse

OUTCOMES =

Outcome constants for action execution results

[
  OUTCOME_SUCCESS = "success",
  OUTCOME_FAILURE = "failure",
  OUTCOME_EXCEPTION = "exception",
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeResult

Returns a new instance of Result.



9
10
11
12
# File 'lib/axn/result.rb', line 9

def initialize(...)
  super
  _define_boolean_predicate_readers
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name) ⇒ Object (private)

rubocop:disable Style/MissingRespondToMissing (because we're not actually responding to anything additional)



241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/axn/result.rb', line 241

def method_missing(method_name, ...) # rubocop:disable Style/MissingRespondToMissing (because we're not actually responding to anything additional)
  if @context.__combined_data.key?(method_name.to_sym)
    msg = <<~MSG
      Method ##{method_name} is not available on Action::Result!

      #{action_name} may be missing a line like:
        exposes :#{method_name}
    MSG

    raise Axn::ContractViolation::MethodNotAllowed, msg
  end

  super
end

Class Method Details

.error(msg = nil, **exposures, &block) ⇒ Object



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/axn/result.rb', line 26

def error(msg = nil, **exposures, &block)
  exposes = exposures.keys.to_h { |key| [key, { optional: true }] }

  Axn::Factory.build(exposes:, error: msg, auto_log: false) do
    exposures.each do |key, value|
      expose(key, value)
    end
    if block_given?
      begin
        block.call
      rescue StandardError => e
        # Set the exception directly without triggering on_exception handlers
        @__context.__record_exception(e)
      end
    else
      fail! msg, standalone: true
    end
  end.call
end

.ok(msg = nil, **exposures) ⇒ Object



16
17
18
19
20
21
22
23
24
# File 'lib/axn/result.rb', line 16

def ok(msg = nil, **exposures)
  exposes = exposures.keys.to_h { |key| [key, { optional: true }] }

  Axn::Factory.build(exposes:, success: msg, auto_log: false) do
    exposures.each do |key, value|
      expose(key, value)
    end
  end.call
end

Instance Method Details

#__action__Object

Internal accessor for the underlying action instance (used by introspection and tests). It is a reserved public field — see reserved_attribute_names_spec — so it stays public.



108
# File 'lib/axn/result.rb', line 108

def __action__ = @action

#deconstruct_keys(keys) ⇒ Object

Enable pattern matching support for Ruby 3+



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/axn/result.rb', line 111

def deconstruct_keys(keys)
  attrs = {
    ok: ok?,
    success:,
    error:,
    message:,
    outcome: outcome.to_sym,
    finalized: finalized?,
  }

  # Add all exposed data
  attrs.merge!(@context.exposed_data)

  # Return filtered attributes if keys specified
  keys ? attrs.slice(*keys) : attrs
end

#errorObject

Memoized once the context is finalized, so resolution (which can invoke user-supplied message blocks) runs a single time across the lifecycle (logging) and every caller read. A Result is the SAME object during and after the run, so we must NOT cache a pre-finalization read — e.g. a hook touching result.success/#message mid-run, where ok? is still true but a later done!/expose would change the answer. Pre-finalization reads resolve live; only a finalized result is frozen in.



55
56
57
58
59
60
61
# File 'lib/axn/result.rb', line 55

def error
  return if ok? # (!ok? implies finalized — a failure sets the finalized flag — but be explicit)
  return _resolve_error unless finalized?

  @__resolved_error = _resolve_error unless defined?(@__resolved_error)
  @__resolved_error
end

#messageObject



71
# File 'lib/axn/result.rb', line 71

def message = exception ? error : success

#outcomeObject

Deliberately NOT memoized (unlike #error/#success): outcome reflects classification state that can finalize at different points during dispatch (records #2/#3 below), so a value read early — e.g. by an ancestor's on_error before this level's context flag is set — must not be frozen in. The recompute is cheap: it short-circuits on the common paths and only allocates a StringInquirer.



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/axn/result.rb', line 84

def outcome
  label = if exception.is_a?(Axn::Failure)
            OUTCOME_FAILURE
          elsif exception
            # Three records of "this settled as a failure", in priority order:
            #   1. context flag — durable; survives after the per-execution set is cleared.
            #   2. live classification set — set as soon as ANY action (this one or a nested one,
            #      sticky) classifies the exception. Covers the window where an ancestor's `on_error`
            #      reads outcome *before* the executor sets the context flag on this level.
            #   3. `_fails_on?` — defensive recompute.
            failure = @context.__classified_as_failure? ||
                      Internal::ExceptionClassification.failure?(exception) ||
                      action.class._fails_on?(exception) ||
                      Axn::ValidationError.user_facing?(exception)
            failure ? OUTCOME_FAILURE : OUTCOME_EXCEPTION
          else
            OUTCOME_SUCCESS
          end

  ActiveSupport::StringInquirer.new(label)
end

#successObject



63
64
65
66
67
68
69
# File 'lib/axn/result.rb', line 63

def success
  return unless ok?
  return _resolve_success unless finalized?

  @__resolved_success = _resolve_success unless defined?(@__resolved_success)
  @__resolved_success
end