fluxo 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.tool-versions +1 -1
- data/Gemfile.lock +1 -1
- data/README.md +20 -48
- data/ci/Gemfile.activemodel-4.lock +1 -1
- data/ci/Gemfile.activemodel-5.lock +1 -1
- data/ci/Gemfile.activemodel-6.lock +1 -1
- data/ci/Gemfile.activemodel-7.lock +1 -1
- data/lib/fluxo/active_model_extension.rb +7 -4
- data/lib/fluxo/config.rb +14 -11
- data/lib/fluxo/errors.rb +11 -8
- data/lib/fluxo/operation/attributes.rb +15 -40
- data/lib/fluxo/operation.rb +30 -67
- data/lib/fluxo/result.rb +7 -2
- data/lib/fluxo/rspec.rb +172 -0
- data/lib/fluxo/version.rb +1 -1
- metadata +7 -7
- data/lib/fluxo/operation/constructor.rb +0 -31
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9411907ecd9869558ef765adeb057b14d382d0da5576e5461d7ded3f27dc151e
|
4
|
+
data.tar.gz: 8815e173af9d860f67296aea9db1880202c0c30a56b8b00e65ce820de4bad6e7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e67f568a96a0afef091d6865276f07b9be67cfac978795d53520b3c103e7b9dadfe552f019b470c9333c201ab1241707aacc5adcf1b18024a275b50a87368c7d
|
7
|
+
data.tar.gz: c599c6723cba2de6c27699a2708299424b951c1a12204f2cb285f71b245d4afb0be2fa0dfbdb02212e8925fb2497769922d7056861da0ba7c37e8ab848db6645
|
data/.tool-versions
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby 2.7.
|
1
|
+
ruby 2.7.5
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -30,27 +30,16 @@ end
|
|
30
30
|
```
|
31
31
|
|
32
32
|
And then just use the opperation by calling:
|
33
|
-
```
|
33
|
+
```ruby
|
34
34
|
result = MyOperation.call
|
35
35
|
result.success? # => true
|
36
36
|
result.value # => :ok
|
37
37
|
```
|
38
38
|
|
39
|
-
In order to execute an operation with parameters, you
|
39
|
+
In order to execute an operation with parameters, you just need to pass them to the call method:
|
40
40
|
|
41
41
|
```ruby
|
42
42
|
class MyOperation < Fluxo::Operation
|
43
|
-
attributes :param1, :param2
|
44
|
-
|
45
|
-
def call!(param1:, param2:)
|
46
|
-
Success(:ok)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
```
|
50
|
-
|
51
|
-
or use the shortcut for defining attributes:
|
52
|
-
```ruby
|
53
|
-
class MyOperation < Fluxo::Operation(:param1, :param2)
|
54
43
|
def call!(param1:, param2:)
|
55
44
|
Success(:ok)
|
56
45
|
end
|
@@ -67,7 +56,9 @@ The execution result of an operation is a `Fluxo::Result` object. There are thre
|
|
67
56
|
Use the `Success` and `Failure` methods to create results accordingly.
|
68
57
|
|
69
58
|
```ruby
|
70
|
-
class AgeCheckOperation < Fluxo::Operation
|
59
|
+
class AgeCheckOperation < Fluxo::Operation
|
60
|
+
self.strict = false # By default, operations are strict. You must it to catch errors and use on_error hook.
|
61
|
+
|
71
62
|
def call!(age:)
|
72
63
|
age >= 18 ? Success('ok') : Failure('too young')
|
73
64
|
end
|
@@ -97,7 +88,7 @@ AgeCheckOperation.call(age: 18)
|
|
97
88
|
You can also define multiple callbacks for the opportunity result. The callbacks are executed in the order they were defined. You can filter which callbacks are executed by specifying an identifier to the `Success(id) { }` or `Failure(id) { }` methods along with its value as a block.
|
98
89
|
|
99
90
|
```ruby
|
100
|
-
class AgeCategoriesOperation < Fluxo::Operation
|
91
|
+
class AgeCategoriesOperation < Fluxo::Operation
|
101
92
|
def call!(age:)
|
102
93
|
case age
|
103
94
|
when 0..14
|
@@ -128,7 +119,7 @@ AgeCategoriesOperation.call(age: 18) \
|
|
128
119
|
Once things become more complex, you can use can define a `flow` with a list of steps to be executed:
|
129
120
|
|
130
121
|
```ruby
|
131
|
-
class ArithmeticOperation < Fluxo::Operation
|
122
|
+
class ArithmeticOperation < Fluxo::Operation
|
132
123
|
flow :normalize, :plus_one, :double, :square, :wrap
|
133
124
|
|
134
125
|
def normalize(num:)
|
@@ -159,12 +150,10 @@ ArithmeticOperation.call(num: 1) \
|
|
159
150
|
# Result: 16
|
160
151
|
```
|
161
152
|
|
162
|
-
Notice that the value of each step is passed to the next step as an argument. And the last step is always the result of the operation.
|
163
|
-
|
164
|
-
By default you can only pass defined attributes to the steps. You may want to pass transient attributes to the steps. You can do this by specifying a `transient_attributes` option to the operation class:
|
153
|
+
Notice that the value of each step is passed to the next step as an argument. You can include more transient attributes during the flow execution. Step result with object different of a Hash will be ignored. And the last step is always the result of the operation.
|
165
154
|
|
166
155
|
```ruby
|
167
|
-
class CreateUserOperation < Fluxo::Operation
|
156
|
+
class CreateUserOperation < Fluxo::Operation
|
168
157
|
flow :build, :save
|
169
158
|
|
170
159
|
def build(name:, age:)
|
@@ -180,32 +169,12 @@ class CreateUserOperation < Fluxo::Operation(:name, :age)
|
|
180
169
|
end
|
181
170
|
```
|
182
171
|
|
183
|
-
This is useful to make the flow data transparent to the operation. But you can also disable this by setting the `strict_transient_attributes` option to `false` under the Operation class or the global configuration.
|
184
|
-
|
185
|
-
```ruby
|
186
|
-
class CreateUserOperation < Fluxo::Operation(:name, :age)
|
187
|
-
self.strict_transient_attributes = false
|
188
|
-
# ...
|
189
|
-
end
|
190
|
-
# or globally
|
191
|
-
Fluxo.config do |config|
|
192
|
-
config.strict_attributes = false
|
193
|
-
config.strict_transient_attributes = false
|
194
|
-
end
|
195
|
-
# or even
|
196
|
-
Fluxo.config.strict_transient_attributes = false
|
197
|
-
```
|
198
|
-
|
199
172
|
### Operation Groups
|
200
173
|
|
201
174
|
Another very useful feature of Fluxo is the ability to group operations steps. Imagine that you want to execute a bunch of operations in a single transaction. You can do this by defining a the group method and specifying the steps to be executed in the group.
|
202
175
|
|
203
176
|
```ruby
|
204
|
-
class
|
205
|
-
transient_attributes :user, :profile
|
206
|
-
|
207
|
-
flow :build, {transaction: %i[save_user save_profile]}, :enqueue_job
|
208
|
-
|
177
|
+
class ApplicationOperation < Fluxo::Operation
|
209
178
|
private
|
210
179
|
|
211
180
|
def transaction(**kwargs, &block)
|
@@ -215,6 +184,10 @@ class CreateUserOperation < Fluxo::Operation(:name, :email)
|
|
215
184
|
end
|
216
185
|
result
|
217
186
|
end
|
187
|
+
end
|
188
|
+
|
189
|
+
class CreateUserOperation < ApplicationOperation
|
190
|
+
flow :build, {transaction: %i[save_user save_profile]}, :enqueue_job
|
218
191
|
|
219
192
|
def build(name:, email:)
|
220
193
|
user = User.new(name: name, email: email)
|
@@ -245,7 +218,7 @@ end
|
|
245
218
|
If you have the `ActiveModel` gem installed, you can use the `validations` method to define validations on the operation.
|
246
219
|
|
247
220
|
```ruby
|
248
|
-
class SubscribeOperation < Fluxo::Operation
|
221
|
+
class SubscribeOperation < Fluxo::Operation
|
249
222
|
validations do
|
250
223
|
validates :name, presence: true
|
251
224
|
validates :email, presence: true, format: { with: /\A[^@]+@[^@]+\z/ }
|
@@ -262,19 +235,19 @@ end
|
|
262
235
|
To promote single responsibility principle, Fluxo allows compose a complex operation flow by combining other operations.
|
263
236
|
|
264
237
|
```ruby
|
265
|
-
class DoubleOperation < Fluxo::Operation
|
238
|
+
class DoubleOperation < Fluxo::Operation
|
266
239
|
def call!(num:)
|
267
240
|
Success(num: num * 2)
|
268
241
|
end
|
269
242
|
end
|
270
243
|
|
271
|
-
class SquareOperation < Fluxo::Operation
|
244
|
+
class SquareOperation < Fluxo::Operation
|
272
245
|
def call!(num:)
|
273
246
|
Success(num: num * 2)
|
274
247
|
end
|
275
248
|
end
|
276
249
|
|
277
|
-
class ArithmeticOperation < Fluxo::Operation
|
250
|
+
class ArithmeticOperation < Fluxo::Operation
|
278
251
|
flow :normalize, :double, :square
|
279
252
|
|
280
253
|
def normalize(num:)
|
@@ -294,11 +267,10 @@ end
|
|
294
267
|
### Configuration
|
295
268
|
|
296
269
|
```ruby
|
297
|
-
Fluxo.
|
270
|
+
Fluxo.configure do |config|
|
298
271
|
config.wrap_falsey_result = false
|
299
272
|
config.wrap_truthy_result = false
|
300
|
-
config.
|
301
|
-
config.strict_transient_attributes = true
|
273
|
+
config.strict = true
|
302
274
|
config.error_handlers << ->(result) { Honeybadger.notify(result.value) }
|
303
275
|
end
|
304
276
|
```
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'ostruct'
|
1
2
|
|
2
3
|
module Fluxo
|
3
4
|
module ActiveModelExtension
|
@@ -20,7 +21,7 @@ module Fluxo
|
|
20
21
|
private
|
21
22
|
|
22
23
|
def build_validations_proxy!
|
23
|
-
validator = Class.new do
|
24
|
+
validator = Class.new(OpenStruct) do
|
24
25
|
include ActiveModel::Validations
|
25
26
|
|
26
27
|
def self.validate!(operation_instance, **attrs)
|
@@ -44,10 +45,12 @@ module Fluxo
|
|
44
45
|
end
|
45
46
|
|
46
47
|
validator.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
47
|
-
|
48
|
+
def self.name
|
49
|
+
"#{name || 'Fluxo::Operation'}::Validations"
|
50
|
+
end
|
48
51
|
|
49
|
-
def self.
|
50
|
-
|
52
|
+
def self.to_s
|
53
|
+
name
|
51
54
|
end
|
52
55
|
RUBY
|
53
56
|
|
data/lib/fluxo/config.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Fluxo
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
class << self
|
5
|
+
def config
|
6
|
+
@config ||= Config.new
|
7
|
+
yield(@config) if block_given?
|
8
|
+
@config
|
9
|
+
end
|
10
|
+
alias_method :configure, :config
|
8
11
|
end
|
9
12
|
|
10
13
|
class Config
|
@@ -16,18 +19,18 @@ module Fluxo
|
|
16
19
|
# When set to true, the result of a truthy operation will be wrapped in a Success.
|
17
20
|
attr_accessor :wrap_truthy_result
|
18
21
|
|
19
|
-
#
|
20
|
-
|
21
|
-
|
22
|
-
# When set to true, the operation will not validate attributes definition before calling the operation.
|
23
|
-
attr_accessor :strict_attributes
|
22
|
+
# Auto handle errors/exceptions.
|
23
|
+
attr_writer :strict
|
24
24
|
|
25
25
|
def initialize
|
26
26
|
@error_handlers = []
|
27
27
|
@wrap_falsey_result = false
|
28
28
|
@wrap_truthy_result = false
|
29
|
-
@
|
30
|
-
|
29
|
+
@strict = false
|
30
|
+
end
|
31
|
+
|
32
|
+
def strict?
|
33
|
+
!!@strict
|
31
34
|
end
|
32
35
|
end
|
33
36
|
end
|
data/lib/fluxo/errors.rb
CHANGED
@@ -1,17 +1,20 @@
|
|
1
1
|
module Fluxo
|
2
|
-
|
3
|
-
|
2
|
+
module Errors
|
3
|
+
# @param result [Fluxo::Result] The result to be checked
|
4
|
+
def self.raise_operation_error!(result)
|
5
|
+
raise result if result.is_a?(Exception)
|
6
|
+
raise result.value if result.operation.class.strict?
|
4
7
|
|
5
|
-
|
8
|
+
[SyntaxError, ArgumentError, NoMethodError, Fluxo::Error].each do |exception|
|
9
|
+
raise result.value if result.value.is_a?(exception)
|
10
|
+
end
|
11
|
+
end
|
6
12
|
end
|
7
13
|
|
8
|
-
class
|
9
|
-
end
|
10
|
-
|
11
|
-
class NotDefinedAttributeError < AttributeError
|
14
|
+
class Error < StandardError
|
12
15
|
end
|
13
16
|
|
14
|
-
class
|
17
|
+
class InvalidResultError < Error
|
15
18
|
end
|
16
19
|
|
17
20
|
class ValidationDefinitionError < Error
|
@@ -8,60 +8,35 @@ module Fluxo
|
|
8
8
|
end
|
9
9
|
|
10
10
|
module ClassMethods
|
11
|
+
# This variable is used only when ActiveModel is available.
|
11
12
|
attr_reader :validations_proxy
|
12
13
|
|
13
|
-
#
|
14
|
-
attr_writer :
|
14
|
+
# Auto handle errors/exceptions.
|
15
|
+
attr_writer :strict
|
15
16
|
|
16
|
-
|
17
|
-
|
17
|
+
def strict?
|
18
|
+
return @strict if defined? @strict
|
18
19
|
|
19
|
-
|
20
|
-
return @strict_attributes if defined?(@strict_attributes)
|
21
|
-
|
22
|
-
Fluxo.config.strict_attributes
|
23
|
-
end
|
24
|
-
|
25
|
-
def strict_transient_attributes?
|
26
|
-
return @strict_transient_attributes if defined?(@strict_transient_attributes)
|
27
|
-
|
28
|
-
Fluxo.config.strict_transient_attributes
|
20
|
+
@strict = Fluxo.config.strict?
|
29
21
|
end
|
30
22
|
|
31
23
|
def validations
|
32
24
|
raise NotImplementedError, "ActiveModel is not defined to use validations."
|
33
25
|
end
|
34
26
|
|
35
|
-
def
|
36
|
-
@
|
37
|
-
end
|
38
|
-
|
39
|
-
def transient_attribute_names
|
40
|
-
@transient_attribute_names ||= []
|
27
|
+
def required_attributes
|
28
|
+
@required_attributes ||= []
|
41
29
|
end
|
42
30
|
|
43
|
-
def
|
44
|
-
@
|
45
|
-
|
46
|
-
@attribute_names.push(*names)
|
31
|
+
def validate_attributes(*attributes)
|
32
|
+
@required_attributes ||= []
|
33
|
+
@required_attributes |= attributes
|
47
34
|
end
|
35
|
+
alias_method :require_attribute, :validate_attributes
|
36
|
+
alias_method :attributes, :validate_attributes
|
48
37
|
|
49
|
-
def transient_attributes(*
|
50
|
-
|
51
|
-
names = names.map(&:to_sym) - @transient_attribute_names
|
52
|
-
@transient_attribute_names.push(*names)
|
53
|
-
end
|
54
|
-
|
55
|
-
def attribute?(key)
|
56
|
-
return false unless key
|
57
|
-
|
58
|
-
attribute_names.include?(key.to_sym)
|
59
|
-
end
|
60
|
-
|
61
|
-
def transient_attribute?(key)
|
62
|
-
return false unless key
|
63
|
-
|
64
|
-
transient_attribute_names.include?(key.to_sym)
|
38
|
+
def transient_attributes(*)
|
39
|
+
puts "DEPRECATED: #{__method__} is deprecated. Operation runs on sloppy mode by allowing any transient attribute."
|
65
40
|
end
|
66
41
|
end
|
67
42
|
end
|
data/lib/fluxo/operation.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "operation/constructor"
|
4
3
|
require_relative "operation/attributes"
|
5
4
|
|
6
5
|
module Fluxo
|
@@ -8,9 +7,6 @@ module Fluxo
|
|
8
7
|
# conflict with the Operation step methods that are going to inherit this class.
|
9
8
|
class Operation
|
10
9
|
include Attributes
|
11
|
-
include Constructor
|
12
|
-
|
13
|
-
def_Operation(::Fluxo)
|
14
10
|
|
15
11
|
class << self
|
16
12
|
def flow(*methods)
|
@@ -22,12 +18,11 @@ module Fluxo
|
|
22
18
|
|
23
19
|
begin
|
24
20
|
instance.__execute_flow__(steps: [:call!], attributes: attrs)
|
25
|
-
rescue InvalidResultError, AttributeError, ValidationDefinitionError => e
|
26
|
-
raise e
|
27
21
|
rescue => e
|
28
|
-
Fluxo::Result.new(type: :exception, value: e, operation: instance, ids: %i[error])
|
29
|
-
|
30
|
-
|
22
|
+
result = Fluxo::Result.new(type: :exception, value: e, operation: instance, ids: %i[error])
|
23
|
+
Fluxo.config.error_handlers.each { |handler| handler.call(result) }
|
24
|
+
Fluxo::Errors.raise_operation_error!(result)
|
25
|
+
result
|
31
26
|
end
|
32
27
|
end
|
33
28
|
end
|
@@ -41,19 +36,21 @@ module Fluxo
|
|
41
36
|
|
42
37
|
# Calls step-method by step-method always passing the value to the next step
|
43
38
|
# If one of the methods is a failure stop the execution and return a result.
|
44
|
-
def __execute_flow__(steps: [], attributes: {})
|
39
|
+
def __execute_flow__(steps: [], attributes: {}, validate: true)
|
45
40
|
transient_attributes, transient_ids = attributes.dup, Hash.new { |h, k| h[k] = [] }
|
46
|
-
__validate_attributes__(first_step: steps.first, attributes: transient_attributes)
|
47
41
|
|
48
42
|
result = nil
|
49
|
-
steps.unshift(:
|
43
|
+
steps.unshift(:__validate_required_attributes__) if self.class.required_attributes.any? && validate
|
44
|
+
steps.unshift(:__validate__) if self.class.validations_proxy && validate
|
50
45
|
steps.each_with_index do |step, idx|
|
51
46
|
if step.is_a?(Hash)
|
52
|
-
step.each do |group_method, group_steps|
|
47
|
+
group_result = step.each do |group_method, group_steps|
|
53
48
|
send(group_method, **transient_attributes) do |group_attrs|
|
54
|
-
result = __execute_flow__(steps: group_steps, attributes: (group_attrs || transient_attributes))
|
49
|
+
result = __execute_flow__(validate: false, steps: group_steps, attributes: (group_attrs || transient_attributes))
|
55
50
|
end
|
51
|
+
result = group_result if group_result.is_a?(Fluxo::Result)
|
56
52
|
break unless result.success?
|
53
|
+
transient_attributes = result.transient_attributes # Update transient attributes with the group result in case of value is not a Hash
|
57
54
|
end
|
58
55
|
else
|
59
56
|
result = __wrap_result__(send(step, **transient_attributes))
|
@@ -69,6 +66,7 @@ module Fluxo
|
|
69
66
|
next_step: steps[idx + 1]
|
70
67
|
)
|
71
68
|
end
|
69
|
+
result.mutate(transient_attributes: transient_attributes)
|
72
70
|
end
|
73
71
|
result.mutate(ids: transient_ids[result.type].uniq, operation: self)
|
74
72
|
end
|
@@ -103,28 +101,6 @@ module Fluxo
|
|
103
101
|
|
104
102
|
private
|
105
103
|
|
106
|
-
# Validates the operation was called with all the required keyword arguments.
|
107
|
-
# @param first_step [Symbol, Hash] The first step method
|
108
|
-
# @param attributes [Hash] The attributes to validate
|
109
|
-
# @return [void]
|
110
|
-
# @raise [MissingAttributeError] When a required attribute is missing
|
111
|
-
def __validate_attributes__(attributes:, first_step:)
|
112
|
-
if self.class.strict_attributes? && (extra = attributes.keys - self.class.attribute_names).any?
|
113
|
-
raise NotDefinedAttributeError, <<~ERROR
|
114
|
-
The following attributes are not defined: #{extra.join(", ")}
|
115
|
-
|
116
|
-
You can use the #{self.class.name}.attributes method to specify list of allowed attributes.
|
117
|
-
Or you can disable strict attributes mode by setting the strict_attributes to true.
|
118
|
-
ERROR
|
119
|
-
end
|
120
|
-
|
121
|
-
__expand_step_method__(first_step).each do |step|
|
122
|
-
method(step).parameters.select { |type, _| type == :keyreq }.each do |(_type, name)|
|
123
|
-
raise(MissingAttributeError, "Missing :#{name} attribute on #{self.class.name}#{step} step method.") unless attributes.key?(name)
|
124
|
-
end
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
104
|
# Merge the result attributes with the new attributes. Also checks if the upcomming step
|
129
105
|
# has the required attributes and transient attributes to a valid execution.
|
130
106
|
# @param new_attributes [Hash] The new attributes
|
@@ -133,37 +109,7 @@ module Fluxo
|
|
133
109
|
def __merge_result_attributes__(new_attributes:, old_attributes:, next_step:)
|
134
110
|
return old_attributes unless new_attributes.is_a?(Hash)
|
135
111
|
|
136
|
-
|
137
|
-
allowed_attrs = self.class.attribute_names + self.class.transient_attribute_names
|
138
|
-
if self.class.strict_transient_attributes? &&
|
139
|
-
(extra = attributes.keys - allowed_attrs).any?
|
140
|
-
raise NotDefinedAttributeError, <<~ERROR
|
141
|
-
The following transient attributes are not defined: #{extra.join(", ")}
|
142
|
-
|
143
|
-
You can use the #{self.class.name}.transient_attributes method to specify list of allowed attributes.
|
144
|
-
Or you can disable strict transient attributes mode by setting the strict_transient_attributes to true.
|
145
|
-
ERROR
|
146
|
-
end
|
147
|
-
|
148
|
-
__expand_step_method__(next_step).each do |step|
|
149
|
-
method(step).parameters.select { |type, _| type == :keyreq }.each do |(_type, name)|
|
150
|
-
raise(MissingAttributeError, "Missing :#{name} transient attribute on #{self.class.name}##{step} step method.") unless attributes.key?(name)
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
attributes
|
155
|
-
end
|
156
|
-
|
157
|
-
# Return the step method as an array. When it's a hash it suppose to be a
|
158
|
-
# be a step group. In this case return its first key and its first value as
|
159
|
-
# the array of step methods.
|
160
|
-
#
|
161
|
-
# @param step [Symbol, Hash] The step method name
|
162
|
-
def __expand_step_method__(step)
|
163
|
-
return [step] unless step.is_a?(Hash)
|
164
|
-
|
165
|
-
key, value = step.first
|
166
|
-
[key, Array(value).first].compact
|
112
|
+
old_attributes.merge(new_attributes.select { |k, _| k.is_a?(Symbol) })
|
167
113
|
end
|
168
114
|
|
169
115
|
# Execute active_model validation as a flow step.
|
@@ -173,6 +119,23 @@ module Fluxo
|
|
173
119
|
self.class.validations_proxy.validate!(self, **attributes)
|
174
120
|
end
|
175
121
|
|
122
|
+
# Validates the operation was called with all the required keyword arguments.
|
123
|
+
# @param attributes [Hash] The attributes to validate
|
124
|
+
# @return [Fluxo::Result] The result of the validation
|
125
|
+
# @raise [ArgumentError] When a required attribute is missing
|
126
|
+
def __validate_required_attributes__(**attributes)
|
127
|
+
missing = self.class.required_attributes - attributes.keys
|
128
|
+
return Success(:required_attributes) { nil } if missing.none?
|
129
|
+
|
130
|
+
if self.class.strict?
|
131
|
+
raise ArgumentError, "Missing required attributes: #{missing.join(", ")}"
|
132
|
+
else
|
133
|
+
Failure(:required_attributes) do
|
134
|
+
{error: "Missing required attributes: #{missing.join(", ")}"}
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
176
139
|
# Wrap the step method result in a Fluxo::Result object.
|
177
140
|
#
|
178
141
|
# @param result [Fluxo::Result, *Object] The object to wrap
|
data/lib/fluxo/result.rb
CHANGED
@@ -2,7 +2,8 @@
|
|
2
2
|
|
3
3
|
module Fluxo
|
4
4
|
class Result
|
5
|
-
|
5
|
+
ATTRIBUTES = %i[operation type value ids transient_attributes].freeze
|
6
|
+
attr_reader(*ATTRIBUTES)
|
6
7
|
|
7
8
|
# @param options [Hash]
|
8
9
|
# @option options [Fluxo::Operation] :operation The operation instance that gererated this result
|
@@ -14,10 +15,14 @@ module Fluxo
|
|
14
15
|
@value = value
|
15
16
|
@type = type
|
16
17
|
@ids = Array(ids)
|
18
|
+
@transient_attributes = {}
|
17
19
|
end
|
18
20
|
|
19
21
|
def mutate(**attrs)
|
20
|
-
|
22
|
+
attrs.each do |key, value|
|
23
|
+
instance_variable_set("@#{key}", value) if ATTRIBUTES.include?(key)
|
24
|
+
end
|
25
|
+
self
|
21
26
|
end
|
22
27
|
|
23
28
|
def ==(other)
|
data/lib/fluxo/rspec.rb
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
require "rspec/expectations"
|
2
|
+
|
3
|
+
module Fluxo
|
4
|
+
module Rspec
|
5
|
+
def self.included(klass)
|
6
|
+
klass.send(:include, OperationResultMatchers)
|
7
|
+
klass.send(:include, InstanceMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module OperationResultMatchers
|
11
|
+
extend RSpec::Matchers::DSL
|
12
|
+
|
13
|
+
matcher :result_succeed do
|
14
|
+
match do |actual|
|
15
|
+
return false unless actual.is_a?(Fluxo::Result)
|
16
|
+
|
17
|
+
actual.success? && (@expected_value.nil? || values_match?(@expected_value, actual.value))
|
18
|
+
end
|
19
|
+
|
20
|
+
chain :with_value do |value|
|
21
|
+
@expected_value = value
|
22
|
+
end
|
23
|
+
|
24
|
+
def differ
|
25
|
+
RSpec::Support::Differ.new(
|
26
|
+
object_preparer: ->(object) { RSpec::Matchers::Composable.surface_descriptions_in(object) },
|
27
|
+
color: RSpec::Matchers.configuration.color?
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
failure_message do |actual|
|
32
|
+
return "expected that operation succeed, but got invalid result #{actual.inspect}" unless actual.is_a?(Fluxo::Result)
|
33
|
+
|
34
|
+
msg = "expected that operation succeed, "
|
35
|
+
msg += if actual.error?
|
36
|
+
format("but the operation errored.\nException:\n %<e>p\n%<t>s", e: actual.value, t: actual.value.backtrace[0..5].join("\n"))
|
37
|
+
elsif actual.failure?
|
38
|
+
"but the operation failed."
|
39
|
+
else
|
40
|
+
"but got success with incorrect value"
|
41
|
+
end
|
42
|
+
if defined?(ActiveModel) && actual.value.is_a?(ActiveModel::Errors) && actual.value.any?
|
43
|
+
msg += "\nActive Model errors:\n"
|
44
|
+
actual.value.full_messages.each { |m| msg += "--> #{m}" }
|
45
|
+
end
|
46
|
+
msg += "\nDiff:" + differ.diff(actual.value, @expected_value) if @expected_value
|
47
|
+
msg
|
48
|
+
end
|
49
|
+
|
50
|
+
failure_message_when_negated do |actual|
|
51
|
+
return "expected that operation not to succeed, but got invalid result #{actual.inspect}" unless actual.is_a?(Fluxo::Result)
|
52
|
+
|
53
|
+
"expected that operation not to succeed, but the operation succeeded."
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
matcher :result_fail do # fail is a reserved word
|
58
|
+
match do |actual|
|
59
|
+
return false unless actual.is_a?(Fluxo::Result)
|
60
|
+
|
61
|
+
actual.failure? && (@expected_value.nil? || values_match?(@expected_value, actual.value))
|
62
|
+
end
|
63
|
+
|
64
|
+
chain :with_value do |value|
|
65
|
+
@expected_value = value
|
66
|
+
end
|
67
|
+
|
68
|
+
def differ
|
69
|
+
RSpec::Support::Differ.new(
|
70
|
+
object_preparer: ->(object) { RSpec::Matchers::Composable.surface_descriptions_in(object) },
|
71
|
+
color: RSpec::Matchers.configuration.color?
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
failure_message do |actual|
|
76
|
+
return "expected that operation fail, but got invalid result #{actual.inspect}" unless actual.is_a?(Fluxo::Result)
|
77
|
+
|
78
|
+
msg = "expected that operation fail, "
|
79
|
+
if actual.success?
|
80
|
+
msg += "but the operation succeeded."
|
81
|
+
msg += "\nDiff:" + differ.diff(actual.value, @expected_value) if @expected_value
|
82
|
+
elsif actual.error?
|
83
|
+
msg += format("but the operation errored.\nException:\n %<e>p\n%<t>s", e: actual.value, t: actual.value.backtrace[0..5].join("\n"))
|
84
|
+
else
|
85
|
+
msg += "but got failure with incorrect value"
|
86
|
+
msg += "\nDiff:" + differ.diff(actual.value, @expected_value) if @expected_value
|
87
|
+
end
|
88
|
+
if defined?(ActiveModel) && actual.value.is_a?(ActiveModel::Errors) && actual.value.any?
|
89
|
+
msg += "\nActive Model errors:\n"
|
90
|
+
actual.value.full_messages.each { |m| msg += "--> #{m}" }
|
91
|
+
end
|
92
|
+
msg
|
93
|
+
end
|
94
|
+
|
95
|
+
failure_message_when_negated do |actual|
|
96
|
+
return "expected that operation not to fail, but got invalid result #{actual.inspect}" unless actual.is_a?(Fluxo::Result)
|
97
|
+
|
98
|
+
"expected that operation not to fail, but the operation failed."
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
matcher :result_error do
|
103
|
+
match do |actual|
|
104
|
+
return false unless actual.is_a?(Fluxo::Result)
|
105
|
+
|
106
|
+
actual.error? && (@expected_value.nil? || values_match?(@expected_value, actual.value))
|
107
|
+
end
|
108
|
+
|
109
|
+
chain :with_value do |value|
|
110
|
+
@expected_value = value
|
111
|
+
end
|
112
|
+
|
113
|
+
failure_message do |actual|
|
114
|
+
return "expected that operation error, but got invalid result #{actual.inspect}" unless actual.is_a?(Fluxo::Result)
|
115
|
+
|
116
|
+
msg = "expected that operation error, "
|
117
|
+
msg += if actual.success?
|
118
|
+
"but the operation succeeded"
|
119
|
+
elsif actual.failure?
|
120
|
+
"but the operation failed"
|
121
|
+
else
|
122
|
+
"but got error with #{@expected_value&.inspect || "incorrect"} instead of #{actual.value.inspect}"
|
123
|
+
end
|
124
|
+
if defined?(ActiveModel) && actual.value.is_a?(ActiveModel::Errors) && actual.value.any?
|
125
|
+
msg += "\nActive Model errors:\n"
|
126
|
+
actual.value.full_messages.each { |m| msg += "--> #{m}" }
|
127
|
+
end
|
128
|
+
msg
|
129
|
+
end
|
130
|
+
|
131
|
+
failure_message_when_negated do |actual|
|
132
|
+
return "expected that operation not to error, but got invalid result #{actual.inspect}" unless actual.is_a?(Fluxo::Result)
|
133
|
+
|
134
|
+
"expected that operation not to error, but the operation errored."
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
module InstanceMethods
|
140
|
+
def expect_operation_result(**attrs)
|
141
|
+
expect(operation_result(**attrs))
|
142
|
+
end
|
143
|
+
|
144
|
+
def operation_result(**kwargs)
|
145
|
+
define_singleton_method(:result) { instance_variable_get(:@__result__) }
|
146
|
+
@__result__ ||= described_class.call(**kwargs)
|
147
|
+
end
|
148
|
+
|
149
|
+
def expect_step_result(step_name, **kwargs)
|
150
|
+
expect(step_result(step_name, **kwargs))
|
151
|
+
end
|
152
|
+
|
153
|
+
def step_result(step_name, **kwargs)
|
154
|
+
@__operation__ = described_class.new
|
155
|
+
define_singleton_method(:result) { instance_variable_get(:@__result__) }
|
156
|
+
|
157
|
+
begin
|
158
|
+
@__result__ = @__operation__.__execute_flow__(steps: [step_name], attributes: kwargs, validate: false)
|
159
|
+
rescue => e
|
160
|
+
@__result__ = Fluxo::Result.new(type: :exception, value: e, operation: @__operation__, ids: %i[error])
|
161
|
+
Fluxo::Errors.raise_operation_error!(result)
|
162
|
+
end
|
163
|
+
|
164
|
+
@__result__
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
::RSpec.configure do |config|
|
171
|
+
config.include Fluxo::Rspec, type: :operation
|
172
|
+
end
|
data/lib/fluxo/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fluxo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marcos G. Zimmermann
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-02-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: standard
|
@@ -89,8 +89,8 @@ files:
|
|
89
89
|
- lib/fluxo/errors.rb
|
90
90
|
- lib/fluxo/operation.rb
|
91
91
|
- lib/fluxo/operation/attributes.rb
|
92
|
-
- lib/fluxo/operation/constructor.rb
|
93
92
|
- lib/fluxo/result.rb
|
93
|
+
- lib/fluxo/rspec.rb
|
94
94
|
- lib/fluxo/version.rb
|
95
95
|
homepage: https://github.com/marcosgz/fluxo
|
96
96
|
licenses:
|
@@ -98,7 +98,7 @@ licenses:
|
|
98
98
|
metadata:
|
99
99
|
homepage_uri: https://github.com/marcosgz/fluxo
|
100
100
|
source_code_uri: https://github.com/marcosgz/fluxo
|
101
|
-
post_install_message:
|
101
|
+
post_install_message:
|
102
102
|
rdoc_options: []
|
103
103
|
require_paths:
|
104
104
|
- lib
|
@@ -113,8 +113,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
113
113
|
- !ruby/object:Gem::Version
|
114
114
|
version: '0'
|
115
115
|
requirements: []
|
116
|
-
rubygems_version: 3.1.
|
117
|
-
signing_key:
|
116
|
+
rubygems_version: 3.1.6
|
117
|
+
signing_key:
|
118
118
|
specification_version: 4
|
119
119
|
summary: Simple Ruby DSL to create operation service objects.
|
120
120
|
test_files: []
|
@@ -1,31 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Fluxo
|
4
|
-
class Operation
|
5
|
-
module Constructor
|
6
|
-
def self.included(klass)
|
7
|
-
klass.extend(ClassMethods)
|
8
|
-
end
|
9
|
-
|
10
|
-
module ClassMethods
|
11
|
-
def inherited(subclass)
|
12
|
-
subclass.instance_variable_set(:@attribute_names, @attribute_names.dup)
|
13
|
-
end
|
14
|
-
|
15
|
-
def def_Operation(op_module)
|
16
|
-
tap do |klass|
|
17
|
-
op_module.define_singleton_method(:Operation) do |*attrs|
|
18
|
-
klass.Operation(*attrs)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
def Operation(*attrs)
|
24
|
-
Class.new(self).tap do |klass|
|
25
|
-
klass.attributes(*attrs)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|