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.
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "interaktor"
3
- spec.version = "0.1.5"
3
+ spec.version = "0.2.0"
4
4
 
5
5
  spec.author = "Taylor Thurlow"
6
6
  spec.email = "taylorthurlow@me.com"
@@ -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.failure_attributes
36
+ missing_attrs = self.class
37
+ .failure_attributes
219
38
  .reject { |failure_attr| failure_attributes.key?(failure_attr) }
220
- raise "Missing failure attrs: #{missing_attrs.join(", ")}" if missing_attrs.any?
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.success_attributes
52
+ missing_attrs = self.class
53
+ .success_attributes
234
54
  .reject { |success_attr| success_attributes.key?(success_attr) }
235
- raise "Missing success attrs: #{missing_attrs.join(", ")}" if missing_attrs.any?
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 "Unknown success attrs: #{unknown_attrs.join(", ")}" if unknown_attrs.any?
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