interaktor 0.3.0 → 0.5.0

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.
@@ -0,0 +1,154 @@
1
+ module Interaktor
2
+ class Interaction
3
+ attr_reader :input_args, :success_args, :failure_args
4
+
5
+ # @param interaktor [Interaktor]
6
+ # @param input [Hash, Interaction]
7
+ def initialize(interaktor, input)
8
+ @interaktor = interaktor
9
+ @executed = false
10
+ @failed = false
11
+ @rolled_back = false
12
+
13
+ @input_args = case input
14
+ when Hash
15
+ input.transform_keys(&:to_sym)
16
+ when Interaction
17
+ input
18
+ .input_args
19
+ .merge(input.success_args || {})
20
+ .slice(*(interaktor.class.input_attributes || []))
21
+ else
22
+ raise ArgumentError, "Invalid input type: #{input.class}"
23
+ end
24
+ end
25
+
26
+ # Whether the interaction is successful.
27
+ def success?
28
+ if !@executed
29
+ raise Interaktor::Error::InvalidMethodForStateError.new(
30
+ self,
31
+ "Cannot call `success?` before interaktor execution is complete"
32
+ )
33
+ end
34
+
35
+ !failure?
36
+ end
37
+
38
+ # Whether the interaction has failed.
39
+ def failure?
40
+ if !@executed
41
+ raise Interaktor::Error::InvalidMethodForStateError.new(
42
+ self,
43
+ "Cannot call `failure?` before interaktor execution is complete"
44
+ )
45
+ end
46
+
47
+ @failed
48
+ end
49
+
50
+ # @param args [Hash]
51
+ #
52
+ # @raises [Interaktor::Failure]
53
+ def fail!(args = {})
54
+ if @executed
55
+ raise Interaktor::Error::InvalidMethodForStateError.new(
56
+ self,
57
+ "Cannot call `fail!` after interaktor execution is already complete"
58
+ )
59
+ end
60
+
61
+ @executed = true
62
+ @failed = true
63
+ @failure_args = args.transform_keys(&:to_sym)
64
+ raise Interaktor::Failure, self
65
+ end
66
+
67
+ # @param args [Hash]
68
+ def success!(args = {})
69
+ if @executed
70
+ raise Interaktor::Error::InvalidMethodForStateError.new(
71
+ self,
72
+ "Cannot call `success!` after interaktor execution is already complete"
73
+ )
74
+ end
75
+
76
+ @executed = true
77
+ @success_args = args.transform_keys(&:to_sym)
78
+ early_return!
79
+ end
80
+
81
+ def allowable_success_attributes
82
+ @interaktor.class.success_attributes
83
+ end
84
+
85
+ def allowable_failure_attributes
86
+ @interaktor.class.failure_attributes
87
+ end
88
+
89
+ # Only allow access to arguments when appropriate. Input arguments should be
90
+ # accessible only during the interaction's execution, and after the
91
+ # execution is complete, either the success or failure arguments should be
92
+ # accessible, depending on the outcome.
93
+ def method_missing(method_name, *args, &block)
94
+ if !@executed && input_args.key?(method_name)
95
+ input_args[method_name]
96
+ elsif success? && allowable_success_attributes.include?(method_name)
97
+ success_args[method_name]
98
+ elsif failure? && allowable_failure_attributes.include?(method_name)
99
+ failure_args[method_name]
100
+ else
101
+ super
102
+ end
103
+ end
104
+
105
+ def respond_to_missing?(method_name, include_private = false)
106
+ input_args.key?(method_name) ||
107
+ success_args&.key?(method_name) ||
108
+ failure_args&.key?(method_name) ||
109
+ super
110
+ end
111
+
112
+ # Roll back the interaction. Successful interactions may have this method
113
+ # called to roll back their state.
114
+ #
115
+ # @return [Boolean] true if rolled back successfully, false if already
116
+ # rolled back
117
+ def rollback!
118
+ return false if @rolled_back
119
+
120
+ _called.reverse_each(&:rollback)
121
+ @rolled_back = true
122
+ end
123
+
124
+ # Track that an Interaktor has been called. The `#called!` method is used by
125
+ # the interaktor being invoked. After an interaktor is successfully called,
126
+ # the interaction is tracked for the purpose of potential future rollback.
127
+ #
128
+ # @param interaktor [Interaktor] an interaktor that has been successfully
129
+ # called
130
+ def called!(interaktor)
131
+ @executed = true
132
+ _called << interaktor
133
+ end
134
+
135
+ # An array of successfully called Interaktor instances invoked against this
136
+ # interaction instance.
137
+ #
138
+ # @return [Array<Interaktor>]
139
+ def _called
140
+ @called ||= []
141
+ end
142
+
143
+ # Trigger an early return throw.
144
+ def early_return!
145
+ @early_return = true
146
+ throw :early_return, self
147
+ end
148
+
149
+ # Whether or not the interaction has been returned from early.
150
+ def early_return?
151
+ (@early_return == true) || false
152
+ end
153
+ end
154
+ end
@@ -41,8 +41,16 @@ module Interaktor::Organizer
41
41
  def call
42
42
  check_attribute_flow_valid
43
43
 
44
+ latest_interaction = nil
45
+
44
46
  self.class.organized.each do |interaktor|
45
- catch(:early_return) { interaktor.call!(@context) }
47
+ catch(:early_return) do
48
+ latest_interaction = interaktor.call!(latest_interaction || @interaction)
49
+ end
50
+ end
51
+
52
+ if latest_interaction
53
+ @interaction.instance_variable_set(:@success_args, latest_interaction.success_args)
46
54
  end
47
55
  end
48
56
 
data/lib/interaktor.rb CHANGED
@@ -14,99 +14,87 @@ module Interaktor
14
14
  extend ClassMethods
15
15
  include Hooks
16
16
  include Callable
17
+
18
+ interaction_class = Class.new(Interaktor::Interaction) do
19
+ end
20
+
21
+ base.const_set(:Interaction, interaction_class)
17
22
  end
18
23
  end
19
24
 
20
25
  module ClassMethods
21
26
  end
22
27
 
23
- # @param context [Hash, Interaktor::Context] the context object as a hash
28
+ # @param args [Hash, Interaktor::Interaction] the context object as a hash
24
29
  # with attributes or an already-built context
25
- def initialize(context = {})
26
- @context = Interaktor::Context.build(context)
30
+ def initialize(args = {})
31
+ @interaction = self.class::Interaction.new(self, args)
27
32
  end
28
33
 
29
- # Fail the current interaktor.
30
- #
31
- # @param failure_attributes [Hash{Symbol=>Object}] the context attributes
32
- #
33
- # @return [void]
34
- def fail!(failure_attributes = {})
35
- # Silently remove any attributes that are not included in the schema
36
- allowed_keys = self.class.failure_schema.key_map.keys.map { |k| k.name.to_sym }
37
- failure_attributes.select! { |k, _| allowed_keys.include?(k.to_sym) }
38
-
39
- self.class.validate_failure_schema(failure_attributes)
34
+ # @param args [Hash{Symbol=>Object}]
35
+ def fail!(args = {})
36
+ if (disallowed_key = args.keys.find { |k| !self.class.failure_attributes.include?(k.to_sym) })
37
+ raise Interaktor::Error::UnknownAttributeError.new(self, disallowed_key)
38
+ end
40
39
 
41
- @context.fail!(failure_attributes)
40
+ self.class.validate_failure_schema(args)
41
+ @interaction.fail!(args)
42
42
  end
43
43
 
44
- # Terminate execution of the current interaktor and copy the success
45
- # attributes into the context.
46
- #
47
- # @param success_attributes [Hash{Symbol=>Object}] the context attributes
48
- #
49
- # @return [void]
50
- def success!(success_attributes = {})
51
- # Silently remove any attributes that are not included in the schema
52
- allowed_keys = self.class.success_schema.key_map.keys.map { |k| k.name.to_sym }
53
- success_attributes.select! { |k, _| allowed_keys.include?(k.to_sym) }
54
-
55
- self.class.validate_success_schema(success_attributes)
44
+ # @param args [Hash]
45
+ def success!(args = {})
46
+ if (disallowed_key = args.keys.find { |k| !self.class.success_attributes.include?(k.to_sym) })
47
+ raise Interaktor::Error::UnknownAttributeError.new(self, disallowed_key)
48
+ end
56
49
 
57
- @context.success!(success_attributes)
50
+ self.class.validate_success_schema(args)
51
+ @interaction.success!(args)
58
52
  end
59
53
 
60
54
  # Invoke an Interaktor instance without any hooks, tracking, or rollback. It
61
55
  # is expected that the `#call` instance method is overwritten for each
62
56
  # interaktor class.
63
- #
64
- # @return [void]
65
- def call; end
57
+ def call
58
+ end
66
59
 
67
60
  # Reverse prior invocation of an Interaktor instance. Any interaktor class
68
61
  # that requires undoing upon downstream failure is expected to overwrite the
69
62
  # `#rollback` instance method.
70
- #
71
- # @return [void]
72
- def rollback; end
73
-
74
- # Invoke an interaktor instance along with all defined hooks. The `run`
75
- # method is used internally by the `call` class method. After successful
76
- # invocation of the interaktor, the instance is tracked within the context.
77
- # If the context is failed or any error is raised, the context is rolled
78
- # back.
79
- #
80
- # @return [void]
63
+ def rollback
64
+ end
65
+
66
+ # Invoke an interaktor instance along with all defined hooks. The `run` method
67
+ # is used internally by the `call` class method. After successful invocation
68
+ # of the interaktor, the instance is tracked within the context. If the
69
+ # context is failed or any error is raised, the context is rolled back.
81
70
  def run
82
71
  run!
83
- rescue Interaktor::Failure # rubocop:disable Lint/SuppressedException
72
+ rescue Interaktor::Failure
84
73
  end
85
74
 
86
75
  # Invoke an Interaktor instance along with all defined hooks, typically used
87
76
  # internally by `.call!`. After successful invocation of the interaktor, the
88
- # instance is tracked within the context. If the context is failed or any
89
- # error is raised, the context is rolled back. This method behaves
90
- # identically to `#run` with one notable exception - if the context is failed
91
- # during the invocation of the interaktor, `Interaktor::Failure` is raised.
77
+ # instance is tracked within the interaction. If the interaction is failed or
78
+ # any error is raised, the interaction is rolled back. This method behaves
79
+ # identically to `#run` with one notable exception - if the interaction is
80
+ # failed during the invocation of the interaktor, `Interaktor::Failure` is
81
+ # raised.
92
82
  #
93
83
  # @raises [Interaktor::Failure]
94
- #
95
- # @return [void]
96
84
  def run!
97
85
  with_hooks do
98
86
  catch(:early_return) do
99
87
  call
100
88
  end
101
89
 
102
- if !@context.early_return? && self.class.required_success_attributes.any?
90
+ if self.class.required_success_attributes.any? && !@interaction.success_args
103
91
  raise Interaktor::Error::MissingExplicitSuccessError.new(self, self.class.required_success_attributes)
104
92
  end
105
93
 
106
- @context.called!(self)
94
+ @interaction.called!(self)
107
95
  end
108
- rescue StandardError
109
- @context.rollback!
96
+ rescue
97
+ @interaction.rollback!
110
98
  raise
111
99
  end
112
100
  end