fluxo 0.1.0 → 0.2.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: c9233de7ffd75f41b5448b99093874d462f38eb53bd0230e87ce110618db4811
4
- data.tar.gz: 8fc63126176a8f49904ad2c2276442cbe71799f61c35f7eacd64fcccd176142d
3
+ metadata.gz: 9453729318ec0b9d59e2833d19a4383c4b2f1bb241a8f97774c3f3b2cb4e6654
4
+ data.tar.gz: b428e33f29ababb0e2099eb9807f295fb899c965088ab2e650c111961d24c76a
5
5
  SHA512:
6
- metadata.gz: 59f97254f8ef0ef915f8d3e7d3bc18dc37ef1873bfd845606a56250680bc7eac870504381908859d08693c8b7ac3619881ee8ab82c13ebdcfa236591cf41fda7
7
- data.tar.gz: 0ab68181c7f41d299ca02272a216ea94bc2ff078f2c128f90cad99cd6ca88e362a398b127829ab9320a46aaafd4a576e2050d06d4c802bb117ade1461b56301f
6
+ metadata.gz: 6ce303be1b5fa233fb9fc0e72eccebafd6d36e5ac81b2f6b8b1796c07c63bc5cbc097ed5fa7f48fdaf3457d3414335b8bbd1a63765557e7f3de0279a0dbb78ba
7
+ data.tar.gz: 514da4e79141fae0847ef16e2f4e9c67ac0246de87415277c657c93471cfe444a6eca8ec332b7a64aaa6286b61375b80d46ce49185fb5942b887d5a01ea4a271
data/.gitignore CHANGED
@@ -6,3 +6,4 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ fluxo-*.gem
data/.travis.yml CHANGED
@@ -4,7 +4,6 @@ before_install:
4
4
  - gem --version
5
5
  - gem update bundler
6
6
  - gem --version
7
- - bash ci/setup.sh
8
7
 
9
8
  jobs:
10
9
  fast_finish: true
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fluxo (0.1.0)
4
+ fluxo (0.2.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Fluxo
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/fluxo`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ Provides a simple and powerful way to create operations service objects for complex workflows.
6
4
 
7
5
  ## Installation
8
6
 
@@ -22,7 +20,238 @@ Or install it yourself as:
22
20
 
23
21
  ## Usage
24
22
 
25
- TODO: Write usage instructions here
23
+ Minimal operation definition:
24
+ ```ruby
25
+ class MyOperation < Fluxo::Operation
26
+ def call!(**)
27
+ Success(:ok)
28
+ end
29
+ end
30
+ ```
31
+
32
+ And then just use the opperation by calling:
33
+ ```
34
+ result = MyOperation.call
35
+ result.success? # => true
36
+ result.value # => :ok
37
+ ```
38
+
39
+ In order to execute an operation with parameters, you must first define list of attributes:
40
+
41
+ ```ruby
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
+ def call!(param1:, param2:)
55
+ Success(:ok)
56
+ end
57
+ end
58
+ ```
59
+
60
+ ### Operation Result
61
+
62
+ The execution result of an operation is a `Fluxo::Result` object. There are three types of results:
63
+ * `:ok`: the operation was successful
64
+ * `:failure`: the operation failed
65
+ * `:exception`: the operation raised an error
66
+
67
+ Use the `Success` and `Failure` methods to create results accordingly.
68
+
69
+ ```ruby
70
+ class AgeCheckOperation < Fluxo::Operation(:age)
71
+ def call!(age:)
72
+ age >= 18 ? Success('ok') : Failure('too young')
73
+ end
74
+ end
75
+
76
+ result = AgeCheckOperation.call(age: 16) # #<Fluxo::Result @value="too young", @type=:failure>
77
+ result.success? # false
78
+ result.error? # false
79
+ result.failure? # true
80
+ result.value # "too young"
81
+
82
+ result = AgeCheckOperation.call(age: 18) # #<Fluxo::Result @value="ok", @type=:ok>
83
+ result.success? # true
84
+ result.error? # false
85
+ result.failure? # false
86
+ result.value # "ok"
87
+ ```
88
+
89
+ The `result` also provides `on_success`, `on_failure` and `on_error` methods to define callbacks for the `:ok` and `:failure` results.
90
+
91
+ ```ruby
92
+ AgeCheckOperation.call(age: 18)
93
+ .on_success { |result| puts result.value }
94
+ .on_failure { |_result| puts "Sorry, you are too young" }
95
+ ```
96
+
97
+ 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
+
99
+ ```ruby
100
+ class AgeCategoriesOperation < Fluxo::Operation(:age)
101
+ def call!(age:)
102
+ case age
103
+ when 0..14
104
+ Failure(:child) { "Sorry, you are too young" }
105
+ when 15..17
106
+ Failure(:teenager) { "You are a teenager" }
107
+ when 18..65
108
+ Success(:adult) { "You are an adult" }
109
+ else
110
+ Success(:senior) { "You are a senior" }
111
+ end
112
+ end
113
+ end
114
+
115
+ AgeCategoriesOperation.call(age: 18) \
116
+ .on_success { |_result| puts "Great, you are an adult" } \
117
+ .on_success(:senior) { |_result| puts "Enjoy your retirement" } \
118
+ .on_success(:adult, :senior) { |_result| puts "Allowed access" } \
119
+ .on_failure { |_result| puts "Sorry, you are too young" } \
120
+ .on_failure(:teenager) { |_result| puts "Almost there, you are a teenager" }
121
+ # The above example will print:
122
+ # Great, you are an adult
123
+ # Allowed access
124
+ ```
125
+
126
+ ### Operation Flow
127
+
128
+ Once things become more complex, you can use can define a `flow` with a list of steps to be executed:
129
+
130
+ ```ruby
131
+ class ArithmeticOperation < Fluxo::Operation(:num)
132
+ flow :normalize, :plus_one, :double, :square, :wrap
133
+
134
+ def normalize(num:)
135
+ Success(num: num.to_i)
136
+ end
137
+
138
+ def plus_one(num:)
139
+ return Failure('cannot be zero') if num == 0
140
+
141
+ Success(num: num + 1)
142
+ end
143
+
144
+ def double(num:)
145
+ Success(num: num * 2)
146
+ end
147
+
148
+ def square(num:)
149
+ Success(num: num * num)
150
+ end
151
+
152
+ def wrap(num:)
153
+ Success(num)
154
+ end
155
+ end
156
+
157
+ ArithmeticOperation.call(num: 1) \
158
+ .on_success { |result| puts "Result: #{result.value}" }
159
+ # Result: 16
160
+ ```
161
+
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:
165
+
166
+ ```ruby
167
+ class CreateUserOperation < Fluxo::Operation(:name, :age)
168
+ flow :build, :save
169
+
170
+ def build(name:, age:)
171
+ user = User.new(name: name, age: age)
172
+ Success(user: user)
173
+ end
174
+
175
+ def save(user:, **)
176
+ return Failure(user.errors) unless user.save
177
+
178
+ Success(user: user)
179
+ end
180
+ end
181
+ ```
182
+
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
+ ### Operation Groups
200
+
201
+ 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
+
203
+ ```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
+
209
+ private
210
+
211
+ def transaction(**kwargs, &block)
212
+ ActiveRecord::Base.transaction do
213
+ result = block.call(**kwargs)
214
+ raise(ActiveRecord::Rollback) unless result.success?
215
+ end
216
+ result
217
+ end
218
+
219
+ def build(name:, email:)
220
+ user = User.new(name: name, email: email)
221
+ Success(user: user)
222
+ end
223
+
224
+ def save_user(user:, **)
225
+ return Failure(user.errors) unless user.save
226
+
227
+ Success(user: user)
228
+ end
229
+
230
+ def save_profile(user:, **)
231
+ UserProfile.create!(user: user)
232
+ Success()
233
+ end
234
+
235
+ def enqueue_job(user:, **)
236
+ UserJob.perform_later(user.id)
237
+
238
+ Success(user)
239
+ end
240
+ end
241
+ ```
242
+
243
+ ### Configuration
244
+
245
+ ```ruby
246
+ Fluxo.config do |config|
247
+ config.wrap_falsey_result = false
248
+ config.wrap_truthy_result = false
249
+ config.strict_attributes = true
250
+ config.strict_transient_attributes = true
251
+ config.error_handlers << ->(result) { Honeybadger.notify(result.value) }
252
+ end
253
+ ```
254
+
26
255
 
27
256
  ## Development
28
257
 
data/fluxo.gemspec CHANGED
@@ -7,7 +7,8 @@ Gem::Specification.new do |spec|
7
7
  spec.email = ["mgzmaster@gmail.com"]
8
8
 
9
9
  spec.summary = "Simple Ruby DSL to create operation service objects."
10
- spec.description = "Simple Ruby DSL to create operation service objects"
10
+ spec.description = "Provides a simple and powerful way to create operations service objects for complex workflows."
11
+
11
12
  spec.homepage = "https://github.com/marcosgz/fluxo"
12
13
  spec.license = "MIT"
13
14
  spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
data/lib/fluxo/config.rb CHANGED
@@ -17,17 +17,17 @@ module Fluxo
17
17
  attr_accessor :wrap_truthy_result
18
18
 
19
19
  # When set to true, the operation will not validate the transient_attributes defition during the flow step execution.
20
- attr_accessor :sloppy_transient_attributes
20
+ attr_accessor :strict_transient_attributes
21
21
 
22
22
  # When set to true, the operation will not validate attributes definition before calling the operation.
23
- attr_accessor :sloppy_attributes
23
+ attr_accessor :strict_attributes
24
24
 
25
25
  def initialize
26
26
  @error_handlers = []
27
27
  @wrap_falsey_result = false
28
28
  @wrap_truthy_result = false
29
- @sloppy_transient_attributes = false
30
- @sloppy_attributes = false
29
+ @strict_transient_attributes = true
30
+ @strict_attributes = true
31
31
  end
32
32
  end
33
33
  end
@@ -11,21 +11,21 @@ module Fluxo
11
11
  attr_reader :validations_proxy
12
12
 
13
13
  # When set to true, the operation will not validate the transient_attributes defition during the flow step execution.
14
- attr_writer :sloppy_transient_attributes
14
+ attr_writer :strict_transient_attributes
15
15
 
16
16
  # When set to true, the operation will not validate attributes definition before calling the operation.
17
- attr_writer :sloppy_attributes
17
+ attr_writer :strict_attributes
18
18
 
19
- def sloppy_attributes?
20
- return @sloppy_attributes if defined?(@sloppy_attributes)
19
+ def strict_attributes?
20
+ return @strict_attributes if defined?(@strict_attributes)
21
21
 
22
- Fluxo.config.sloppy_attributes
22
+ Fluxo.config.strict_attributes
23
23
  end
24
24
 
25
- def sloppy_transient_attributes?
26
- return @sloppy_transient_attributes if defined?(@sloppy_transient_attributes)
25
+ def strict_transient_attributes?
26
+ return @strict_transient_attributes if defined?(@strict_transient_attributes)
27
27
 
28
- Fluxo.config.sloppy_transient_attributes
28
+ Fluxo.config.strict_transient_attributes
29
29
  end
30
30
 
31
31
  def validations
@@ -4,6 +4,8 @@ require_relative "operation/constructor"
4
4
  require_relative "operation/attributes"
5
5
 
6
6
  module Fluxo
7
+ # I know that the underline instance method name is not the best, but I don't want to
8
+ # conflict with the Operation step methods that are going to inherit this class.
7
9
  class Operation
8
10
  include Attributes
9
11
  include Constructor
@@ -40,13 +42,24 @@ module Fluxo
40
42
  # Calls step-method by step-method always passing the value to the next step
41
43
  # If one of the methods is a failure stop the execution and return a result.
42
44
  def __execute_flow__(steps: [], attributes: {})
43
- transient_attributes = attributes.dup
45
+ transient_attributes, transient_ids = attributes.dup, {ok: [], failure: [], exception: []}
44
46
  __validate_attributes__(first_step: steps.first, attributes: transient_attributes)
45
47
 
46
48
  result = nil
47
49
  steps.unshift(:__validate__) if self.class.validations_proxy # add validate step before the first step
48
50
  steps.each_with_index do |step, idx|
49
- result = __wrap_result__(send(step, **transient_attributes))
51
+ if step.is_a?(Hash)
52
+ step.each do |group_method, group_steps|
53
+ send(group_method, **transient_attributes) do |group_attrs|
54
+ result = __execute_flow__(steps: group_steps, attributes: (group_attrs || transient_attributes))
55
+ end
56
+ break unless result.success?
57
+ end
58
+ else
59
+ result = __wrap_result__(send(step, **transient_attributes))
60
+ transient_ids.fetch(result.type).push(*result.ids)
61
+ end
62
+
50
63
  break unless result.success?
51
64
 
52
65
  if steps[idx + 1]
@@ -57,7 +70,7 @@ module Fluxo
57
70
  )
58
71
  end
59
72
  end
60
- result
73
+ result.tap { |r| r.ids = transient_ids.fetch(r.type).uniq }
61
74
  end
62
75
 
63
76
  # @param value_or_result_id [Any] The value for the result or the id when the result comes from block
@@ -90,47 +103,82 @@ module Fluxo
90
103
 
91
104
  private
92
105
 
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
93
111
  def __validate_attributes__(attributes:, first_step:)
94
- if !self.class.sloppy_attributes? && (extra = attributes.keys - self.class.attribute_names).any?
112
+ if self.class.strict_attributes? && (extra = attributes.keys - self.class.attribute_names).any?
95
113
  raise NotDefinedAttributeError, <<~ERROR
96
114
  The following attributes are not defined: #{extra.join(", ")}
97
115
 
98
116
  You can use the #{self.class.name}.attributes method to specify list of allowed attributes.
99
- Or you can disable strict attributes mode by setting the sloppy_attributes to true.
117
+ Or you can disable strict attributes mode by setting the strict_attributes to true.
100
118
  ERROR
101
119
  end
102
120
 
103
- method(first_step).parameters.select { |type, _| type == :keyreq }.each do |(_type, name)|
104
- raise(MissingAttributeError, "Missing :#{name} attribute on #{self.class.name}#{first_step} step method.") unless attributes.key?(name)
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
105
125
  end
106
126
  end
107
127
 
128
+ # Merge the result attributes with the new attributes. Also checks if the upcomming step
129
+ # has the required attributes and transient attributes to a valid execution.
130
+ # @param new_attributes [Hash] The new attributes
131
+ # @param old_attributes [Hash] The old attributes
132
+ # @param next_step [Symbol, Hash] The next step method
108
133
  def __merge_result_attributes__(new_attributes:, old_attributes:, next_step:)
109
134
  return old_attributes unless new_attributes.is_a?(Hash)
110
135
 
111
136
  attributes = old_attributes.merge(new_attributes)
112
137
  allowed_attrs = self.class.attribute_names + self.class.transient_attribute_names
113
- if !self.class.sloppy_transient_attributes? &&
138
+ if self.class.strict_transient_attributes? &&
114
139
  (extra = attributes.keys - allowed_attrs).any?
115
140
  raise NotDefinedAttributeError, <<~ERROR
116
141
  The following transient attributes are not defined: #{extra.join(", ")}
117
142
 
118
143
  You can use the #{self.class.name}.transient_attributes method to specify list of allowed attributes.
119
- Or you can disable strict transient attributes mode by setting the sloppy_transient_attributes to true.
144
+ Or you can disable strict transient attributes mode by setting the strict_transient_attributes to true.
120
145
  ERROR
121
146
  end
122
147
 
123
- method(next_step).parameters.select { |type, _| type == :keyreq }.each do |(_type, name)|
124
- raise(MissingAttributeError, "Missing :#{name} transient attribute on #{self.class.name}##{next_step} step method.") unless attributes.key?(name)
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
125
152
  end
126
153
 
127
154
  attributes
128
155
  end
129
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
167
+ end
168
+
169
+ # Execute active_model validation as a flow step.
170
+ # @param attributes [Hash] The attributes to validate
171
+ # @return [Fluxo::Result] The result of the validation
130
172
  def __validate__(**attributes)
131
173
  self.class.validations_proxy.validate!(self, **attributes)
132
174
  end
133
175
 
176
+ # Wrap the step method result in a Fluxo::Result object.
177
+ #
178
+ # @param result [Fluxo::Result, *Object] The object to wrap
179
+ # @raise [Fluxo::InvalidResultError] When the result is not a Fluxo::Result config
180
+ # is set to not wrap results.
181
+ # @return [Fluxo::Result] The wrapped result
134
182
  def __wrap_result__(result)
135
183
  if result.is_a?(Fluxo::Result)
136
184
  return result
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
+ attr_reader :operation, :type, :value
6
+ attr_accessor :ids
6
7
 
7
8
  # @param options [Hash]
8
9
  # @option options [Fluxo::Operation] :operation The operation instance that gererated this result
@@ -31,16 +32,16 @@ module Fluxo
31
32
  type == :exception
32
33
  end
33
34
 
34
- def on_success(handler_id = nil)
35
- tap { yield(self) if success? && (handler_id.nil? || ids.include?(handler_id)) }
35
+ def on_success(*handler_ids)
36
+ tap { yield(self) if success? && (handler_ids.none? || (ids & handler_ids).any?) }
36
37
  end
37
38
 
38
- def on_failure(handler_id = nil)
39
- tap { yield(self) if failure? && (handler_id.nil? || ids.include?(handler_id)) }
39
+ def on_failure(*handler_ids)
40
+ tap { yield(self) if failure? && (handler_ids.none? || (ids & handler_ids).any?) }
40
41
  end
41
42
 
42
- def on_error(handler_id = nil)
43
- tap { yield(self) if error? && (handler_id.nil? || ids.include?(handler_id)) }
43
+ def on_error(*handler_ids)
44
+ tap { yield(self) if error? && (handler_ids.none? || (ids & handler_ids).any?) }
44
45
  end
45
46
  end
46
47
  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.1.0"
4
+ VERSION = "0.2.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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marcos G. Zimmermann
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-01-14 00:00:00.000000000 Z
11
+ date: 2022-01-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: standard
@@ -52,7 +52,8 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
- description: Simple Ruby DSL to create operation service objects
55
+ description: Provides a simple and powerful way to create operations service objects
56
+ for complex workflows.
56
57
  email:
57
58
  - mgzmaster@gmail.com
58
59
  executables: []