interaktor 0.1.5 → 0.2.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.
- checksums.yaml +4 -4
- data/.github/workflows/publish.yml +30 -0
- data/.github/workflows/tests.yml +24 -0
- data/.ruby-version +1 -0
- data/.travis.yml +1 -12
- data/Gemfile +3 -2
- data/Guardfile +42 -0
- data/README.md +87 -132
- data/interaktor.gemspec +1 -1
- data/lib/interaktor.rb +12 -188
- data/lib/interaktor/callable.rb +219 -0
- data/lib/interaktor/context.rb +16 -1
- data/lib/interaktor/error/attribute_error.rb +16 -0
- data/lib/interaktor/error/base.rb +9 -0
- data/lib/interaktor/error/disallowed_attribute_assignment_error.rb +9 -0
- data/lib/interaktor/error/missing_attribute_error.rb +5 -0
- data/lib/interaktor/error/option_error.rb +16 -0
- data/lib/interaktor/error/unknown_attribute_error.rb +5 -0
- data/lib/interaktor/error/unknown_option_error.rb +5 -0
- data/lib/interaktor/organizer.rb +7 -20
- data/spec/integration_spec.rb +18 -15
- data/spec/interactor/organizer_spec.rb +81 -13
- data/spec/spec_helper.rb +8 -3
- data/spec/support/lint.rb +289 -57
- metadata +19 -6
data/interaktor.gemspec
CHANGED
data/lib/interaktor.rb
CHANGED
@@ -13,193 +13,11 @@ module Interaktor
|
|
13
13
|
base.class_eval do
|
14
14
|
extend ClassMethods
|
15
15
|
include Hooks
|
16
|
+
include Callable
|
16
17
|
end
|
17
18
|
end
|
18
19
|
|
19
20
|
module ClassMethods
|
20
|
-
# The list of attributes which are required to be passed in when calling
|
21
|
-
# the interaktor.
|
22
|
-
#
|
23
|
-
# @return [Array<Symbol>]
|
24
|
-
def required_attributes
|
25
|
-
@required_attributes ||= []
|
26
|
-
end
|
27
|
-
|
28
|
-
# The list of attributes which are NOT required to be passed in when
|
29
|
-
# calling the interaktor.
|
30
|
-
#
|
31
|
-
# @return [Array<Symbol>]
|
32
|
-
def optional_attributes
|
33
|
-
@optional_attributes ||= []
|
34
|
-
end
|
35
|
-
|
36
|
-
# A list of optional attributes and their default values.
|
37
|
-
#
|
38
|
-
# @return [Array<Symbol>]
|
39
|
-
def optional_defaults
|
40
|
-
@optional_defaults ||= {}
|
41
|
-
end
|
42
|
-
|
43
|
-
# The list of attributes which are required to be passed in when calling
|
44
|
-
# `#fail!` from within the interaktor.
|
45
|
-
#
|
46
|
-
# @return [Array<Symbol>]
|
47
|
-
def failure_attributes
|
48
|
-
@failure_attributes ||= []
|
49
|
-
end
|
50
|
-
|
51
|
-
# The list of attributes which are required to be passed in when calling
|
52
|
-
# `#fail!` from within the interaktor.
|
53
|
-
#
|
54
|
-
# @return [Array<Symbol>]
|
55
|
-
def success_attributes
|
56
|
-
@success_attributes ||= []
|
57
|
-
end
|
58
|
-
|
59
|
-
# A DSL method for documenting required interaktor attributes.
|
60
|
-
#
|
61
|
-
# @param attributes [Symbol, Array<Symbol>] the list of attribute names
|
62
|
-
# @param options [Hash]
|
63
|
-
#
|
64
|
-
# @return [void]
|
65
|
-
def required(*attributes, **options)
|
66
|
-
required_attributes.concat attributes
|
67
|
-
|
68
|
-
attributes.each do |attribute|
|
69
|
-
# Define getter
|
70
|
-
define_method(attribute) { @context.send(attribute) }
|
71
|
-
|
72
|
-
# Define setter
|
73
|
-
define_method("#{attribute}=".to_sym) do |value|
|
74
|
-
@context.send("#{attribute}=".to_sym, value)
|
75
|
-
end
|
76
|
-
|
77
|
-
raise "Unknown option(s): #{options.keys.join(", ")}" if options.any?
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
# A DSL method for documenting optional interaktor attributes.
|
82
|
-
#
|
83
|
-
# @param attributes [Symbol, Array<Symbol>] the list of attribute names
|
84
|
-
# @param options [Hash]
|
85
|
-
#
|
86
|
-
# @return [void]
|
87
|
-
def optional(*attributes, **options)
|
88
|
-
optional_attributes.concat attributes
|
89
|
-
|
90
|
-
attributes.each do |attribute|
|
91
|
-
# Define getter
|
92
|
-
define_method(attribute) { @context.send(attribute) }
|
93
|
-
|
94
|
-
# Define setter
|
95
|
-
define_method("#{attribute}=".to_sym) do |value|
|
96
|
-
unless @context.to_h.key?(attribute)
|
97
|
-
raise <<~ERROR
|
98
|
-
You can't assign a value to an optional parameter if you
|
99
|
-
didn't initialize the interaktor with it in the first
|
100
|
-
place.
|
101
|
-
ERROR
|
102
|
-
end
|
103
|
-
|
104
|
-
@context.send("#{attribute}=".to_sym, value)
|
105
|
-
end
|
106
|
-
|
107
|
-
# Handle options
|
108
|
-
optional_defaults[attribute] = options[:default] if options[:default]
|
109
|
-
options.delete(:default)
|
110
|
-
|
111
|
-
raise "Unknown option(s): #{options.keys.join(", ")}" if options.any?
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
# A DSL method for documenting required interaktor failure attributes.
|
116
|
-
#
|
117
|
-
# @param attributes [Symbol, Array<Symbol>] the list of attribute names
|
118
|
-
#
|
119
|
-
# @return [void]
|
120
|
-
def failure(*attributes)
|
121
|
-
failure_attributes.concat attributes
|
122
|
-
end
|
123
|
-
|
124
|
-
# A DSL method for documenting required interaktor success attributes.
|
125
|
-
#
|
126
|
-
# @param attributes [Symbol, Array<Symbol>] the list of attribute names
|
127
|
-
#
|
128
|
-
# @return [void]
|
129
|
-
def success(*attributes)
|
130
|
-
success_attributes.concat attributes
|
131
|
-
end
|
132
|
-
|
133
|
-
# Invoke an Interaktor. This is the primary public API method to an
|
134
|
-
# interaktor.
|
135
|
-
#
|
136
|
-
# @param context [Hash, Interaktor::Context] the context object as a hash
|
137
|
-
# with attributes or an already-built context
|
138
|
-
#
|
139
|
-
# @return [Interaktor::Context] the context, following interaktor execution
|
140
|
-
def call(context = {})
|
141
|
-
apply_default_optional_attributes(context)
|
142
|
-
verify_attribute_presence(context)
|
143
|
-
|
144
|
-
new(context).tap(&:run).instance_variable_get(:@context)
|
145
|
-
end
|
146
|
-
|
147
|
-
# Invoke an Interaktor. This method behaves identically to `#call`, with
|
148
|
-
# one notable exception - if the context is failed during the invocation of
|
149
|
-
# the interaktor, `Interaktor::Failure` is raised.
|
150
|
-
#
|
151
|
-
# @param context [Hash, Interaktor::Context] the context object as a hash
|
152
|
-
# with attributes or an already-built context
|
153
|
-
#
|
154
|
-
# @raises [Interaktor::Failure]
|
155
|
-
#
|
156
|
-
# @return [Interaktor::Context] the context, following interaktor execution
|
157
|
-
def call!(context = {})
|
158
|
-
apply_default_optional_attributes(context)
|
159
|
-
verify_attribute_presence(context)
|
160
|
-
|
161
|
-
new(context).tap(&:run!).instance_variable_get(:@context)
|
162
|
-
end
|
163
|
-
|
164
|
-
private
|
165
|
-
|
166
|
-
# Check the provided context against the attributes defined with the DSL
|
167
|
-
# methods, and determine if there are any attributes which are required and
|
168
|
-
# have not been provided, or if there are any attributes which have been
|
169
|
-
# provided but are not listed as either required or optional.
|
170
|
-
#
|
171
|
-
# @param context [Interaktor::Context] the context to check
|
172
|
-
#
|
173
|
-
# @return [void]
|
174
|
-
def verify_attribute_presence(context)
|
175
|
-
# TODO: Add "allow_nil?" option to required attributes
|
176
|
-
missing_attrs = required_attributes.reject { |required_attr| context.to_h.key?(required_attr) }
|
177
|
-
|
178
|
-
raise <<~ERROR if missing_attrs.any?
|
179
|
-
Required attribute(s) were not provided when initializing #{name} interaktor:
|
180
|
-
#{missing_attrs.join("\n ")}
|
181
|
-
ERROR
|
182
|
-
|
183
|
-
allowed_attrs = required_attributes + optional_attributes
|
184
|
-
extra_attrs = context.to_h.keys.reject { |attr| allowed_attrs.include?(attr) }
|
185
|
-
|
186
|
-
raise <<~ERROR if extra_attrs.any?
|
187
|
-
One or more provided attributes were not recognized when initializing #{name} interaktor:
|
188
|
-
#{extra_attrs.join("\n ")}
|
189
|
-
ERROR
|
190
|
-
end
|
191
|
-
|
192
|
-
# Given the list of optional default attribute values defined by the class,
|
193
|
-
# assign those default values to the context if they were omitted.
|
194
|
-
#
|
195
|
-
# @param context [Interaktor::Context]
|
196
|
-
#
|
197
|
-
# @return [void]
|
198
|
-
def apply_default_optional_attributes(context)
|
199
|
-
optional_defaults.each do |attribute, default|
|
200
|
-
context[attribute] ||= default
|
201
|
-
end
|
202
|
-
end
|
203
21
|
end
|
204
22
|
|
205
23
|
# @param context [Hash, Interaktor::Context] the context object as a hash
|
@@ -215,9 +33,10 @@ module Interaktor
|
|
215
33
|
# @return [void]
|
216
34
|
def fail!(failure_attributes = {})
|
217
35
|
# Make sure we have all required attributes
|
218
|
-
missing_attrs = self.class
|
36
|
+
missing_attrs = self.class
|
37
|
+
.failure_attributes
|
219
38
|
.reject { |failure_attr| failure_attributes.key?(failure_attr) }
|
220
|
-
raise
|
39
|
+
raise Interaktor::Error::MissingAttributeError.new(self.class.to_s, missing_attrs) if missing_attrs.any?
|
221
40
|
|
222
41
|
@context.fail!(failure_attributes)
|
223
42
|
end
|
@@ -230,14 +49,15 @@ module Interaktor
|
|
230
49
|
# @return [void]
|
231
50
|
def success!(success_attributes = {})
|
232
51
|
# Make sure we have all required attributes
|
233
|
-
missing_attrs = self.class
|
52
|
+
missing_attrs = self.class
|
53
|
+
.success_attributes
|
234
54
|
.reject { |success_attr| success_attributes.key?(success_attr) }
|
235
|
-
raise
|
55
|
+
raise Interaktor::Error::MissingAttributeError.new(self.class.to_s, missing_attrs) if missing_attrs.any?
|
236
56
|
|
237
57
|
# Make sure we haven't provided any unknown attributes
|
238
58
|
unknown_attrs = success_attributes.keys
|
239
59
|
.reject { |success_attr| self.class.success_attributes.include?(success_attr) }
|
240
|
-
raise
|
60
|
+
raise Interaktor::Error::UnknownAttributeError.new(self.class.to_s, unknown_attrs) if unknown_attrs.any?
|
241
61
|
|
242
62
|
@context.success!(success_attributes)
|
243
63
|
end
|
@@ -284,6 +104,10 @@ module Interaktor
|
|
284
104
|
call
|
285
105
|
end
|
286
106
|
|
107
|
+
if !@context.early_return? && self.class.success_attributes.any?
|
108
|
+
raise Interaktor::Error::MissingAttributeError.new(self, self.class.success_attributes)
|
109
|
+
end
|
110
|
+
|
287
111
|
@context.called!(self)
|
288
112
|
end
|
289
113
|
rescue StandardError
|
@@ -0,0 +1,219 @@
|
|
1
|
+
module Interaktor::Callable
|
2
|
+
# When the module is included in a class, add the relevant class methods to
|
3
|
+
# that class.
|
4
|
+
#
|
5
|
+
# @param base [Class] the class which is including the module
|
6
|
+
def self.included(base)
|
7
|
+
base.class_eval { extend ClassMethods }
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
# The list of attributes which are required to be passed in when calling
|
12
|
+
# the interaktor.
|
13
|
+
#
|
14
|
+
# @return [Array<Symbol>]
|
15
|
+
def required_attributes
|
16
|
+
@required_attributes ||= []
|
17
|
+
end
|
18
|
+
|
19
|
+
# The list of attributes which are NOT required to be passed in when
|
20
|
+
# calling the interaktor.
|
21
|
+
#
|
22
|
+
# @return [Array<Symbol>]
|
23
|
+
def optional_attributes
|
24
|
+
@optional_attributes ||= []
|
25
|
+
end
|
26
|
+
|
27
|
+
# A list of optional attributes and their default values.
|
28
|
+
#
|
29
|
+
# @return [Array<Symbol>]
|
30
|
+
def optional_defaults
|
31
|
+
@optional_defaults ||= {}
|
32
|
+
end
|
33
|
+
|
34
|
+
# A list of attributes which could be passed when calling the interaktor.
|
35
|
+
#
|
36
|
+
# @return [Array<Symbol>]
|
37
|
+
def input_attributes
|
38
|
+
required_attributes + optional_attributes
|
39
|
+
end
|
40
|
+
|
41
|
+
# The list of attributes which are required to be passed in when calling
|
42
|
+
# `#fail!` from within the interaktor.
|
43
|
+
#
|
44
|
+
# @return [Array<Symbol>]
|
45
|
+
def failure_attributes
|
46
|
+
@failure_attributes ||= []
|
47
|
+
end
|
48
|
+
|
49
|
+
# The list of attributes which are required to be passed in when calling
|
50
|
+
# `#fail!` from within the interaktor.
|
51
|
+
#
|
52
|
+
# @return [Array<Symbol>]
|
53
|
+
def success_attributes
|
54
|
+
@success_attributes ||= []
|
55
|
+
end
|
56
|
+
|
57
|
+
# A DSL method for documenting required interaktor attributes.
|
58
|
+
#
|
59
|
+
# @param attributes [Symbol, Array<Symbol>] the list of attribute names
|
60
|
+
# @param options [Hash]
|
61
|
+
#
|
62
|
+
# @return [void]
|
63
|
+
def required(*attributes, **options)
|
64
|
+
required_attributes.concat attributes
|
65
|
+
|
66
|
+
attributes.each do |attribute|
|
67
|
+
# Define getter
|
68
|
+
define_method(attribute) { @context.send(attribute) }
|
69
|
+
|
70
|
+
# Define setter
|
71
|
+
define_method("#{attribute}=".to_sym) do |value|
|
72
|
+
@context.send("#{attribute}=".to_sym, value)
|
73
|
+
end
|
74
|
+
|
75
|
+
raise Interaktor::Error::UnknownOptionError.new(self.class.to_s, options) if options.any?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# A DSL method for documenting optional interaktor attributes.
|
80
|
+
#
|
81
|
+
# @param attributes [Symbol, Array<Symbol>] the list of attribute names
|
82
|
+
# @param options [Hash]
|
83
|
+
#
|
84
|
+
# @return [void]
|
85
|
+
def optional(*attributes, **options)
|
86
|
+
optional_attributes.concat attributes
|
87
|
+
|
88
|
+
attributes.each do |attribute|
|
89
|
+
# Define getter
|
90
|
+
define_method(attribute) { @context.send(attribute) }
|
91
|
+
|
92
|
+
# Define setter
|
93
|
+
define_method("#{attribute}=".to_sym) do |value|
|
94
|
+
unless @context.to_h.key?(attribute)
|
95
|
+
raise Interaktor::Error::DisallowedAttributeAssignmentError.new(self.class.to_s, [attribute])
|
96
|
+
end
|
97
|
+
|
98
|
+
@context.send("#{attribute}=".to_sym, value)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Handle options
|
102
|
+
optional_defaults[attribute] = options[:default] if options[:default]
|
103
|
+
options.delete(:default)
|
104
|
+
|
105
|
+
raise Interaktor::Error::UnknownOptionError.new(self.class.to_s, options) if options.any?
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# A DSL method for documenting required interaktor failure attributes.
|
110
|
+
#
|
111
|
+
# @param attributes [Symbol, Array<Symbol>] the list of attribute names
|
112
|
+
# @param options [Hash]
|
113
|
+
#
|
114
|
+
# @return [void]
|
115
|
+
def failure(*attributes, **options)
|
116
|
+
failure_attributes.concat attributes
|
117
|
+
|
118
|
+
attributes.each do |attribute|
|
119
|
+
# Handle options
|
120
|
+
raise Interaktor::Error::UnknownOptionError.new(self.class.to_s, options) if options.any?
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# A DSL method for documenting required interaktor success attributes.
|
125
|
+
#
|
126
|
+
# @param attributes [Symbol, Array<Symbol>] the list of attribute names
|
127
|
+
# @param options [Hash]
|
128
|
+
#
|
129
|
+
# @return [void]
|
130
|
+
def success(*attributes, **options)
|
131
|
+
success_attributes.concat attributes
|
132
|
+
|
133
|
+
attributes.each do |attribute|
|
134
|
+
# Handle options
|
135
|
+
raise Interaktor::Error::UnknownOptionError.new(self.class.to_s, options) if options.any?
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Invoke an Interaktor. This is the primary public API method to an
|
140
|
+
# interaktor. Interaktor failures will not raise an exception.
|
141
|
+
#
|
142
|
+
# @param context [Hash, Interaktor::Context] the context object as a hash
|
143
|
+
# with attributes or an already-built context
|
144
|
+
#
|
145
|
+
# @return [Interaktor::Context] the context, following interaktor execution
|
146
|
+
def call(context = {})
|
147
|
+
execute(context, false)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Invoke an Interaktor. This method behaves identically to `#call`, but if
|
151
|
+
# the interaktor is failed, `Interaktor::Failure` is raised.
|
152
|
+
#
|
153
|
+
# @param context [Hash, Interaktor::Context] the context object as a hash
|
154
|
+
# with attributes or an already-built context
|
155
|
+
#
|
156
|
+
# @raises [Interaktor::Failure]
|
157
|
+
#
|
158
|
+
# @return [Interaktor::Context] the context, following interaktor execution
|
159
|
+
def call!(context = {})
|
160
|
+
execute(context, true)
|
161
|
+
end
|
162
|
+
|
163
|
+
private
|
164
|
+
|
165
|
+
# The main execution method triggered by the public `#call` or `#call!`
|
166
|
+
# methods.
|
167
|
+
#
|
168
|
+
# @param context [Hash, Interaktor::Context] the context object as a hash
|
169
|
+
# with attributes or an already-built context
|
170
|
+
# @param raise_exception [Boolean] whether or not to raise exception on
|
171
|
+
# failure
|
172
|
+
#
|
173
|
+
# @raises [Interaktor::Failure]
|
174
|
+
#
|
175
|
+
# @return [Interaktor::Context] the context, following interaktor execution
|
176
|
+
def execute(context, raise_exception)
|
177
|
+
unless context.is_a?(Hash) || context.is_a?(Interaktor::Context)
|
178
|
+
raise ArgumentError, "Expected a hash argument when calling the interaktor, got a #{context.class} instead."
|
179
|
+
end
|
180
|
+
|
181
|
+
apply_default_optional_attributes(context)
|
182
|
+
verify_attribute_presence(context)
|
183
|
+
|
184
|
+
run_method = raise_exception ? :run! : :run
|
185
|
+
|
186
|
+
new(context).tap(&run_method).instance_variable_get(:@context)
|
187
|
+
end
|
188
|
+
|
189
|
+
# Check the provided context against the attributes defined with the DSL
|
190
|
+
# methods, and determine if there are any attributes which are required and
|
191
|
+
# have not been provided, or if there are any attributes which have been
|
192
|
+
# provided but are not listed as either required or optional.
|
193
|
+
#
|
194
|
+
# @param context [Interaktor::Context] the context to check
|
195
|
+
#
|
196
|
+
# @return [void]
|
197
|
+
def verify_attribute_presence(context)
|
198
|
+
# TODO: Add "allow_nil?" option to required attributes
|
199
|
+
missing_attrs = required_attributes.reject { |required_attr| context.to_h.key?(required_attr) }
|
200
|
+
raise Interaktor::Error::MissingAttributeError.new(self, missing_attrs) if missing_attrs.any?
|
201
|
+
|
202
|
+
allowed_attrs = required_attributes + optional_attributes
|
203
|
+
extra_attrs = context.to_h.keys.reject { |attr| allowed_attrs.include?(attr) }
|
204
|
+
raise Interaktor::Error::UnknownAttributeError.new(self, extra_attrs) if extra_attrs.any?
|
205
|
+
end
|
206
|
+
|
207
|
+
# Given the list of optional default attribute values defined by the class,
|
208
|
+
# assign those default values to the context if they were omitted.
|
209
|
+
#
|
210
|
+
# @param context [Interaktor::Context]
|
211
|
+
#
|
212
|
+
# @return [void]
|
213
|
+
def apply_default_optional_attributes(context)
|
214
|
+
optional_defaults.each do |attribute, default|
|
215
|
+
context[attribute] ||= default
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|