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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 383f612dadbf2c9b9c7cd5d8e4e09e3f140a8665405e3069e29e1c6629ff1da1
4
- data.tar.gz: d91c37a9e158ad85b039cd3fb82d09881ed8390083e60ace2f3c345549902cff
3
+ metadata.gz: 9411907ecd9869558ef765adeb057b14d382d0da5576e5461d7ded3f27dc151e
4
+ data.tar.gz: 8815e173af9d860f67296aea9db1880202c0c30a56b8b00e65ce820de4bad6e7
5
5
  SHA512:
6
- metadata.gz: b7ba58999435d17f261decb8bd64a37f2f75135027a3e3683cce67ceae08bd065145c31e0c5e9f70c394fa944faa2e20986cf15860ca90236c0feb06d54a68ab
7
- data.tar.gz: 8d2b54df3b612c2c6f20430d9086ff2a53a958ae9989a8605c05bfe12ebf2e3c93978b1add2af3919f5a6a99c294ebe5b610ab9b10f34f21ba7979bdb5e30406
6
+ metadata.gz: e67f568a96a0afef091d6865276f07b9be67cfac978795d53520b3c103e7b9dadfe552f019b470c9333c201ab1241707aacc5adcf1b18024a275b50a87368c7d
7
+ data.tar.gz: c599c6723cba2de6c27699a2708299424b951c1a12204f2cb285f71b245d4afb0be2fa0dfbdb02212e8925fb2497769922d7056861da0ba7c37e8ab848db6645
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 2.7.2
1
+ ruby 2.7.5
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fluxo (0.2.1)
4
+ fluxo (0.3.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
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 must first define list of attributes:
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(:age)
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(:age)
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(:num)
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(:name, :age)
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 CreateUserOperation < Fluxo::Operation(:name, :email)
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(:name, :email)
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(:num)
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(:num)
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(:num)
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.config do |config|
270
+ Fluxo.configure do |config|
298
271
  config.wrap_falsey_result = false
299
272
  config.wrap_truthy_result = false
300
- config.strict_attributes = true
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,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- fluxo (0.2.1)
4
+ fluxo (0.3.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- fluxo (0.2.1)
4
+ fluxo (0.3.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- fluxo (0.2.1)
4
+ fluxo (0.3.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- fluxo (0.2.1)
4
+ fluxo (0.3.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -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
- attr_accessor #{attribute_names.map(&:inspect).join(", ")}
48
+ def self.name
49
+ "#{name || 'Fluxo::Operation'}::Validations"
50
+ end
48
51
 
49
- def self.model_name
50
- ::ActiveModel::Name.new(self, nil, %|#{name || "Anonymous"}|)
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
- def self.config
5
- @config ||= Config.new
6
- yield(@config) if block_given?
7
- @config
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
- # When set to true, the operation will not validate the transient_attributes defition during the flow step execution.
20
- attr_accessor :strict_transient_attributes
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
- @strict_transient_attributes = true
30
- @strict_attributes = true
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
- class Error < StandardError
3
- end
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
- class InvalidResultError < Error
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 AttributeError < Error
9
- end
10
-
11
- class NotDefinedAttributeError < AttributeError
14
+ class Error < StandardError
12
15
  end
13
16
 
14
- class MissingAttributeError < AttributeError
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
- # When set to true, the operation will not validate the transient_attributes defition during the flow step execution.
14
- attr_writer :strict_transient_attributes
14
+ # Auto handle errors/exceptions.
15
+ attr_writer :strict
15
16
 
16
- # When set to true, the operation will not validate attributes definition before calling the operation.
17
- attr_writer :strict_attributes
17
+ def strict?
18
+ return @strict if defined? @strict
18
19
 
19
- def strict_attributes?
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 attribute_names
36
- @attribute_names ||= []
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 attributes(*names)
44
- @attribute_names ||= []
45
- names = names.map(&:to_sym) - @attribute_names
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(*names)
50
- @transient_attribute_names ||= []
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
@@ -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]).tap do |result|
29
- Fluxo.config.error_handlers.each { |handler| handler.call(result) }
30
- end
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(:__validate__) if self.class.validations_proxy # add validate step before the first step
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
- attributes = old_attributes.merge(new_attributes)
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
- attr_reader :operation, :type, :value, :ids
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
- self.class.new(**{operation: operation, type: type, value: value, ids: ids}.merge(attrs))
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)
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fluxo
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.0"
5
5
  end
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.2.1
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: 2022-01-18 00:00:00.000000000 Z
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.4
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