interaktor 0.1.5 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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