flow 0.10.4 → 0.10.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +140 -8
  3. data/lib/flow.rb +13 -1
  4. data/lib/flow/flow/callbacks.rb +7 -5
  5. data/lib/flow/flow/core.rb +16 -14
  6. data/lib/flow/flow/flux.rb +35 -33
  7. data/lib/flow/flow/operations.rb +16 -14
  8. data/lib/flow/flow/status.rb +15 -13
  9. data/lib/flow/flow/transactions.rb +7 -5
  10. data/lib/flow/flow/trigger.rb +26 -24
  11. data/lib/flow/flow_base.rb +2 -6
  12. data/lib/flow/operation/core.rb +12 -1
  13. data/lib/flow/operation/execute.rb +1 -1
  14. data/lib/flow/operation_base.rb +10 -15
  15. data/lib/flow/{spec_helper/rspec_configuration.rb → rspec/configuration.rb} +0 -0
  16. data/lib/flow/{spec_helper → rspec}/custom_matchers.rb +3 -0
  17. data/lib/flow/rspec/custom_matchers/access_state.rb +27 -0
  18. data/lib/flow/{spec_helper → rspec}/custom_matchers/define_failure.rb +0 -0
  19. data/lib/flow/{spec_helper → rspec}/custom_matchers/define_output.rb +0 -0
  20. data/lib/flow/{spec_helper → rspec}/custom_matchers/handle_error.rb +0 -0
  21. data/lib/flow/{spec_helper → rspec}/custom_matchers/have_on_state.rb +0 -0
  22. data/lib/flow/rspec/custom_matchers/read_state.rb +23 -0
  23. data/lib/flow/{spec_helper → rspec}/custom_matchers/use_operations.rb +0 -0
  24. data/lib/flow/{spec_helper → rspec}/custom_matchers/wrap_in_transaction.rb +0 -0
  25. data/lib/flow/rspec/custom_matchers/write_state.rb +23 -0
  26. data/lib/flow/{spec_helper → rspec}/shared_contexts.rb +0 -0
  27. data/lib/flow/{spec_helper → rspec}/shared_contexts/with_failing_operation.rb +0 -0
  28. data/lib/flow/{spec_helper → rspec}/shoulda_matcher_helper.rb +0 -0
  29. data/lib/flow/spec_helper.rb +4 -4
  30. data/lib/flow/state/output.rb +1 -1
  31. data/lib/flow/state_base.rb +2 -4
  32. data/lib/flow/state_proxy.rb +30 -0
  33. data/lib/flow/version.rb +1 -1
  34. data/lib/generators/flow/operation/templates/operation.rb.erb +4 -0
  35. data/lib/generators/rspec/application_operation/templates/application_operation_spec.rb +9 -5
  36. data/lib/generators/rspec/operation/templates/operation_spec.rb.erb +4 -0
  37. metadata +33 -18
  38. data/lib/flow/flow/errors/state_invalid.rb +0 -7
  39. data/lib/flow/operation/errors/already_executed.rb +0 -9
  40. data/lib/flow/operation/errors/already_rewound.rb +0 -9
  41. data/lib/flow/state/errors/not_validated.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8a4adcc10876350cd8358f5847ad928962037235e646bded446262f98c5c05e
4
- data.tar.gz: b50a5bae755061296098405e5e93241911822132b84abd83f65fdad2a00b3bfb
3
+ metadata.gz: 946ff11930523a9982a5f4210d74ef656e66c0f84c0267f484799b6a4173956e
4
+ data.tar.gz: 7c148b9935abaf957f966c3f576620f5d444e5d8561e35fa40cbc3dc7c525ca7
5
5
  SHA512:
6
- metadata.gz: 5dbd29e46bc44783d9ccbac12cf0fd8bf626cc61809fbdf787ede2d1283418a4646dacfdb6d13b0d278d139a9bedfecca6a8f4b6f5df66010c93842ff82ac717
7
- data.tar.gz: f53636ff842d54aef60c0f8e84c5fff32d394ae66f74d05f5244470307217373f7187d94013980ac6594e9f2f99e71b8caff23a84db9d93f2a98c04106a9b5ad
6
+ metadata.gz: a3c5c2cf05a2b9ffbe25bf0ebad9a3b33c83d3f76a79f76717e7ca96744a39b89778cb8fb4ad3cb6522c1f59094fd9619cc878ec4152a26c6876c01e6afadbcb
7
+ data.tar.gz: 374fdd652be6bc5bff0db2f5eed97f974f0e78cd0512742294ad8196fa47e58fa72b21ca319375314c0d98ffedc01b1d11279d9a5645b488bef4151d89d17c44
data/README.md CHANGED
@@ -11,6 +11,7 @@
11
11
  * [How it Works](#how-it-works)
12
12
  * [Flows](#flows)
13
13
  * [Operations](#operations)
14
+ * [Accessors](#accessors)
14
15
  * [States](#states)
15
16
  * [Input](#input)
16
17
  * [Output](#output)
@@ -146,19 +147,24 @@ An **Operation** is a service object which is executed with a **State**.
146
147
  Operations should **not** be named with the `Operation` suffix; name them what they do!
147
148
 
148
149
  ```ruby
149
- class ClearExistingTimetables < ApplicationOperation
150
+ class ClearExistingTimetable < ApplicationOperation
151
+ state_accessor :existing_timetable_cells
152
+
150
153
  def behavior
151
- state.existing_timetable_cells.update_all(total_minutes: 0)
154
+ existing_timetable_cells.update_all(total_minutes: 0)
152
155
  end
153
156
  end
154
157
  ```
155
158
 
156
159
  ```ruby
157
160
  class CalculateTimetables < ApplicationOperation
161
+ state_accessor :minutes_by_project_employee
162
+ state_reader :timeframe
163
+
158
164
  def behavior
159
- state.minutes_by_project_employee.each do |project_employee, total_minutes|
165
+ minutes_by_project_employee.each do |project_employee, total_minutes|
160
166
  project_id, employee_id = project_employee
161
- timetable = state.timeframe.timetables.find_or_create_by!(project_id: project_id)
167
+ timetable = timeframe.timetables.find_or_create_by!(project_id: project_id)
162
168
  cell = timetable.cells.find_or_create_by!(employee_id: employee_id)
163
169
 
164
170
  cell.update!(total_minutes: total_minutes)
@@ -169,8 +175,10 @@ end
169
175
 
170
176
  ```ruby
171
177
  class SummarizeTimetables < ApplicationOperation
178
+ state_accessor :timetables
179
+
172
180
  def behavior
173
- state.timetables.each do |timetable|
181
+ timetables.each do |timetable|
174
182
  timetable.update!(total_minutes: timetable.cells.sum(:total_minutes))
175
183
  end
176
184
  end
@@ -179,8 +187,10 @@ end
179
187
 
180
188
  ```ruby
181
189
  class DestroyEmptyTimetableCells < ApplicationOperation
190
+ state_accessor :empty_cells
191
+
182
192
  def behavior
183
- state.empty_cells.destroy_all
193
+ empty_cells.destroy_all
184
194
  end
185
195
  end
186
196
  ```
@@ -191,8 +201,10 @@ Operations take a state as input and define a `#behavior` that occurs when `#exe
191
201
 
192
202
  ```ruby
193
203
  class ExampleOperation < ApplicationOperation
204
+ state_reader :first_name
205
+
194
206
  def behavior
195
- puts "Hello, #{state.first_name}"
207
+ puts "Hello, #{first_name}"
196
208
  end
197
209
  end
198
210
 
@@ -202,6 +214,117 @@ operation.execute
202
214
  operation.executed? # => true
203
215
  ```
204
216
 
217
+ ⚠️ **Deprecation Warning**: Direct state access in Operations is being removed. [Learn more](./DEPRECATION_NOTICE.md)
218
+
219
+ #### Accessors
220
+
221
+ **Operations** define, through *State Accessors*, what data they will read from and/or write to a **State**.
222
+
223
+ These accessors provide an explicit means to declare input needs and output expectations.
224
+
225
+ ```ruby
226
+ class ExampleOperation < ApplicationOperation
227
+ state_reader :foo
228
+ state_writer :bar
229
+ state_accessor :baz
230
+ end
231
+ ```
232
+
233
+ Under the hood, a **StateProxy** adapts a **State** to an **Operation** using these accessors.
234
+
235
+ ⚠️ *Warning*: Your Operation will not be able to access methods on your State if you do not declare them using these accessors!
236
+
237
+ The following are considered best practices for working with state accessors:
238
+
239
+ `state_reader` should **only** read data and not alter it
240
+
241
+ ```ruby
242
+ # Bad, don't do it this way:
243
+ class BadOperation < ApplicationOperation
244
+ state_reader :foo
245
+
246
+ def behavior
247
+ foo << :more_dataz
248
+ end
249
+ end
250
+
251
+ # Good, do it this way:
252
+ class GoodOperation < ApplicationOperation
253
+ state_reader :foo
254
+
255
+ def behavior
256
+ SomeThirdParty.do_a_thing if foo == :foo
257
+ end
258
+ end
259
+ ```
260
+
261
+ `state_writer` should map to a field marked as `output` on the State
262
+
263
+ ```ruby
264
+ class ExampleOperation < ApplicationOperation
265
+ state_writer :foo
266
+
267
+ def behavior
268
+ state.foo = :foo
269
+ end
270
+ end
271
+
272
+ # Bad, don't do it this way:
273
+ class BadState < ApplicationState
274
+ option :foo
275
+ end
276
+
277
+ # Good, do it this way:
278
+ class GoodState < ApplicationState
279
+ output :foo
280
+ end
281
+ ```
282
+
283
+ `state_accessor` should be used rather than defining both setters and getters:
284
+
285
+ ```ruby
286
+ # Bad, don't do it this way:
287
+ class BadOperation < ApplicationOperation
288
+ state_reader :foo
289
+ state_writer :foo
290
+
291
+ def behavior
292
+ state.foo = :bar if foo == :baz
293
+ end
294
+ end
295
+
296
+ # Good, do it this way:
297
+ class GoodOperation < ApplicationOperation
298
+ state_accessor :foo
299
+
300
+ def behavior
301
+ state.foo = :bar if foo == :baz
302
+ end
303
+ end
304
+ ```
305
+
306
+ `state_accessor` should also be used when altering a memory object (ex: Array, Hash, ActiveModel):
307
+
308
+ ```ruby
309
+ # Bad, don't do it this way:
310
+ class BadOperation < ApplicationOperation
311
+ state_reader :foo
312
+
313
+ def behavior
314
+ foo << :more_dataz
315
+ end
316
+ end
317
+
318
+ # Good, do it this way:
319
+ class GoodOperation < ApplicationOperation
320
+ state_accessor :foo
321
+
322
+ def behavior
323
+ foo << :more_dataz
324
+ end
325
+ end
326
+ ```
327
+
205
328
  ### States
206
329
 
207
330
  A **State** is an aggregation of input and derived data.
@@ -1024,6 +1147,9 @@ This will allow you to use the following custom matchers:
1024
1147
  * [use_operations](lib/flow/custom_matchers/use_operations.rb) tests usage of `ApplicationFlow.operations`
1025
1148
  * [wrap_in_transaction](lib/flow/custom_matchers/wrap_in_transaction.rb) tests usage of `.wrap_in_transaction` for `ApplicationFlow` or `ApplicationOperation`
1026
1149
  * [have_on_state](lib/flow/custom_matchers/have_on_state.rb) tests for data on State after a `ApplicationFlow` or `ApplicationOperation` has been run
1150
+ * [access_state](lib/flow/custom_matchers/access_state.rb) tests usage of `ApplicationOperation.state_accessor` (or use of both `state_(reader|writer)`)
1151
+ * [read_state](lib/flow/custom_matchers/read_state.rb) tests usage of `ApplicationOperation.state_reader`
1152
+ * [write_state](lib/flow/custom_matchers/write_state.rb) tests usage of `ApplicationOperation.state_writer`
1027
1153
 
1028
1154
  ### Testing Flows
1029
1155
 
@@ -1086,6 +1212,10 @@ RSpec.describe MakeTheThingDoTheStuff, type: :operation do
1086
1212
 
1087
1213
  it { is_expected.to inherit_from ApplicationOperation }
1088
1214
 
1215
+ # it { is_expected.to access_state :foo }
1216
+ # it { is_expected.to read_state :bar }
1217
+ # it { is_expected.to write_state :baz }
1218
+
1089
1219
  describe "#execute" do
1090
1220
  subject(:execute) { operation.execute }
1091
1221
 
@@ -1145,10 +1275,12 @@ You are encouraged to use `execute`, rather than `execute!` in testing. You can
1145
1275
  Doing so will allow you to make assertions on the failure without having to expect errors:
1146
1276
  ```ruby
1147
1277
  class SomeOperation < ApplicationOperation
1278
+ state_reader :foo
1279
+
1148
1280
  failure :somethings_invalid
1149
1281
 
1150
1282
  def behavior
1151
- somethings_invalid_failure! baz: "relevant data" if state.foo == "something invalid"
1283
+ somethings_invalid_failure! baz: "relevant data" if foo == "something invalid"
1152
1284
  end
1153
1285
  end
1154
1286
  ```
@@ -13,5 +13,17 @@ require "flow/concerns/transaction_wrapper"
13
13
  require "flow/flow_base"
14
14
  require "flow/operation_base"
15
15
  require "flow/state_base"
16
+ require "flow/state_proxy"
16
17
 
17
- module Flow; end
18
+ module Flow
19
+ class Error < StandardError; end
20
+
21
+ class FlowError < Error; end
22
+ class StateInvalidError < FlowError; end
23
+
24
+ class OperationError < Error; end
25
+ class AlreadyExecutedError < OperationError; end
26
+
27
+ class StateError < Error; end
28
+ class NotValidatedError < StateError; end
29
+ end
@@ -2,12 +2,14 @@
2
2
 
3
3
  # Callbacks provide an extensible mechanism for hooking into a Flow.
4
4
  module Flow
5
- module Callbacks
6
- extend ActiveSupport::Concern
5
+ module Flow
6
+ module Callbacks
7
+ extend ActiveSupport::Concern
7
8
 
8
- included do
9
- include ActiveSupport::Callbacks
10
- define_callbacks :initialize, :trigger, :flux
9
+ included do
10
+ include ActiveSupport::Callbacks
11
+ define_callbacks :initialize, :trigger, :flux
12
+ end
11
13
  end
12
14
  end
13
15
  end
@@ -2,25 +2,27 @@
2
2
 
3
3
  # Accepts input representing the arguments and options which define the initial state.
4
4
  module Flow
5
- module Core
6
- extend ActiveSupport::Concern
5
+ module Flow
6
+ module Core
7
+ extend ActiveSupport::Concern
7
8
 
8
- class_methods do
9
- def state_class
10
- "#{name.chomp("Flow")}State".constantize
9
+ class_methods do
10
+ def state_class
11
+ "#{name.chomp("Flow")}State".constantize
12
+ end
11
13
  end
12
- end
13
14
 
14
- included do
15
- delegate :state_class, to: :class
16
- delegate :outputs, to: :state
15
+ included do
16
+ delegate :state_class, to: :class
17
+ delegate :outputs, to: :state
17
18
 
18
- attr_reader :state
19
- end
19
+ attr_reader :state
20
+ end
20
21
 
21
- def initialize(state_instance = nil, **options)
22
- run_callbacks(:initialize) do
23
- @state = state_instance || state_class.new(**options)
22
+ def initialize(state_instance = nil, **options)
23
+ run_callbacks(:initialize) do
24
+ @state = state_instance || state_class.new(**options)
25
+ end
24
26
  end
25
27
  end
26
28
  end
@@ -2,54 +2,56 @@
2
2
 
3
3
  # When `#trigger` is called on a Flow, `#execute` is called on Operations sequentially in their given order.
4
4
  module Flow
5
- module Flux
6
- extend ActiveSupport::Concern
5
+ module Flow
6
+ module Flux
7
+ extend ActiveSupport::Concern
7
8
 
8
- included do
9
- set_callback(:initialize, :after) { @executed_operations = [] }
9
+ included do
10
+ set_callback(:initialize, :after) { @executed_operations = [] }
10
11
 
11
- attr_reader :failed_operation
12
+ attr_reader :failed_operation
12
13
 
13
- delegate :operation_failure, to: :failed_operation, allow_nil: true
14
+ delegate :operation_failure, to: :failed_operation, allow_nil: true
14
15
 
15
- private
16
+ private
16
17
 
17
- attr_reader :executed_operations
18
+ attr_reader :executed_operations
18
19
 
19
- def _flux
20
- executable_operations.each do |operation|
21
- operation.execute
22
- (@failed_operation = operation) and raise Flow::Flux::Failure if operation.failed?
23
- executed_operations << operation
20
+ def _flux
21
+ executable_operations.each do |operation|
22
+ operation.execute
23
+ (@failed_operation = operation) and raise Flow::Flux::Failure if operation.failed?
24
+ executed_operations << operation
25
+ end
26
+ end
27
+
28
+ def executable_operations
29
+ operation_instances - executed_operations
24
30
  end
25
- end
26
31
 
27
- def executable_operations
28
- operation_instances - executed_operations
32
+ def operation_instances
33
+ _operations.map { |operation_class| operation_class.new(state) }
34
+ end
35
+ memoize :operation_instances
29
36
  end
30
37
 
31
- def operation_instances
32
- _operations.map { |operation_class| operation_class.new(state) }
38
+ def failed_operation?
39
+ failed_operation.present?
33
40
  end
34
- memoize :operation_instances
35
- end
36
41
 
37
- def failed_operation?
38
- failed_operation.present?
39
- end
42
+ def flux
43
+ flux!
44
+ rescue StandardError => exception
45
+ info :error_executing_operation, state: state, exception: exception
40
46
 
41
- def flux
42
- flux!
43
- rescue StandardError => exception
44
- info :error_executing_operation, state: state, exception: exception
47
+ raise exception unless exception.is_a? Flow::Flux::Failure
48
+ end
45
49
 
46
- raise exception unless exception.is_a? Flow::Flux::Failure
47
- end
50
+ def flux!
51
+ run_callbacks(:flux) { _flux }
52
+ end
48
53
 
49
- def flux!
50
- run_callbacks(:flux) { _flux }
54
+ class Failure < StandardError; end
51
55
  end
52
-
53
- class Failure < StandardError; end
54
56
  end
55
57
  end
@@ -2,25 +2,27 @@
2
2
 
3
3
  # Operations are an ordered list of the behaviors which are executed with (and possibly change) the Flow's state.
4
4
  module Flow
5
- module Operations
6
- extend ActiveSupport::Concern
5
+ module Flow
6
+ module Operations
7
+ extend ActiveSupport::Concern
7
8
 
8
- included do
9
- class_attribute :_operations, instance_writer: false, default: []
9
+ included do
10
+ class_attribute :_operations, instance_writer: false, default: []
10
11
 
11
- delegate :_operations, to: :class
12
- end
13
-
14
- class_methods do
15
- def inherited(base)
16
- base._operations = _operations.dup
17
- super
12
+ delegate :_operations, to: :class
18
13
  end
19
14
 
20
- private
15
+ class_methods do
16
+ def inherited(base)
17
+ base._operations = _operations.dup
18
+ super
19
+ end
20
+
21
+ private
21
22
 
22
- def operations(*operations)
23
- _operations.concat(operations.flatten)
23
+ def operations(*operations)
24
+ _operations.concat(operations.flatten)
25
+ end
24
26
  end
25
27
  end
26
28
  end
@@ -2,23 +2,25 @@
2
2
 
3
3
  # The Flow status is a summary of what has occurred during the runtime, used mainly for analysis and program flow.
4
4
  module Flow
5
- module Status
6
- extend ActiveSupport::Concern
5
+ module Flow
6
+ module Status
7
+ extend ActiveSupport::Concern
7
8
 
8
- def pending?
9
- executed_operations.none?
10
- end
9
+ def pending?
10
+ executed_operations.none?
11
+ end
11
12
 
12
- def triggered?
13
- executed_operations.any? || failed_operation?
14
- end
13
+ def triggered?
14
+ executed_operations.any? || failed_operation?
15
+ end
15
16
 
16
- def success?
17
- triggered? && (operation_instances - executed_operations).none?
18
- end
17
+ def success?
18
+ triggered? && (operation_instances - executed_operations).none?
19
+ end
19
20
 
20
- def failed?
21
- triggered? && !success?
21
+ def failed?
22
+ triggered? && !success?
23
+ end
22
24
  end
23
25
  end
24
26
  end
@@ -2,12 +2,14 @@
2
2
 
3
3
  # Flows where no operation should be persisted unless all are successful should use a transaction.
4
4
  module Flow
5
- module Transactions
6
- extend ActiveSupport::Concern
5
+ module Flow
6
+ module Transactions
7
+ extend ActiveSupport::Concern
7
8
 
8
- class_methods do
9
- def callback_name
10
- :flux
9
+ class_methods do
10
+ def callback_name
11
+ :flux
12
+ end
11
13
  end
12
14
  end
13
15
  end
@@ -2,37 +2,39 @@
2
2
 
3
3
  # Triggering a Flow executes all its operations in sequential order (see `Flow::Flux`) *iff* it has a valid state.
4
4
  module Flow
5
- module Trigger
6
- extend ActiveSupport::Concern
7
-
8
- class_methods do
9
- def trigger!(*arguments)
10
- new(*arguments).trigger!
5
+ module Flow
6
+ module Trigger
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ def trigger!(*arguments)
11
+ new(*arguments).trigger!
12
+ end
13
+
14
+ def trigger(*arguments)
15
+ new(*arguments).trigger
16
+ end
11
17
  end
12
18
 
13
- def trigger(*arguments)
14
- new(*arguments).trigger
15
- end
16
- end
19
+ included do
20
+ delegate :valid?, :validated?, to: :state, prefix: true
17
21
 
18
- included do
19
- delegate :valid?, :validated?, to: :state, prefix: true
20
-
21
- set_callback :trigger, :around, ->(_, block) { surveil(:trigger) { block.call } }
22
- end
22
+ set_callback :trigger, :around, ->(_, block) { surveil(:trigger) { block.call } }
23
+ end
23
24
 
24
- def trigger!
25
- raise Flow::Errors::StateInvalid unless state_valid?
25
+ def trigger!
26
+ raise StateInvalidError unless state_valid?
26
27
 
27
- run_callbacks(:trigger) { flux }
28
+ run_callbacks(:trigger) { flux }
28
29
 
29
- self
30
- end
30
+ self
31
+ end
31
32
 
32
- def trigger
33
- trigger!
34
- rescue Flow::Errors::StateInvalid
35
- self
33
+ def trigger
34
+ trigger!
35
+ rescue StateInvalidError
36
+ self
37
+ end
36
38
  end
37
39
  end
38
40
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "flow/errors/state_invalid"
4
-
5
3
  require_relative "flow/callbacks"
6
4
  require_relative "flow/core"
7
5
  require_relative "flow/flux"
@@ -12,10 +10,8 @@ require_relative "flow/trigger"
12
10
 
13
11
  # A **Flow** is a collection of procedurally executed **Operations** sharing a common **State**.
14
12
  module Flow
15
- class FlowBase
16
- include ShortCircuIt
17
- include Technologic
18
- include Flow::TransactionWrapper
13
+ class FlowBase < Spicerack::RootObject
14
+ include TransactionWrapper
19
15
  include Flow::Callbacks
20
16
  include Flow::Core
21
17
  include Flow::Flux
@@ -8,10 +8,21 @@ module Flow
8
8
 
9
9
  included do
10
10
  attr_reader :state
11
+
12
+ delegate :state_proxy_class, to: :class
13
+ end
14
+
15
+ class_methods do
16
+ def state_proxy_class
17
+ @state_proxy_class ||= Class.new(StateProxy).tap do |proxy_class|
18
+ delegate_method_names = _state_writers.map { |method_name| "#{method_name}=" } + _state_readers
19
+ proxy_class.delegate(*delegate_method_names, to: :state) if delegate_method_names.any?
20
+ end
21
+ end
11
22
  end
12
23
 
13
24
  def initialize(state)
14
- @state = state
25
+ @state = state_proxy_class.new(state)
15
26
  end
16
27
  end
17
28
  end
@@ -12,7 +12,7 @@ module Flow
12
12
  attr_reader :operation_failure
13
13
 
14
14
  set_callback :execute, :around, ->(_, block) { surveil(:execute) { block.call } }
15
- set_callback :execute, :before, -> { raise Operation::Errors::AlreadyExecuted }, if: :executed?
15
+ set_callback :execute, :before, -> { raise AlreadyExecutedError }, if: :executed?
16
16
  end
17
17
 
18
18
  def execute!
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "operation/errors/already_executed"
4
- require_relative "operation/errors/already_rewound"
5
-
6
3
  require_relative "operation/accessors"
7
4
  require_relative "operation/callbacks"
8
5
  require_relative "operation/core"
@@ -14,17 +11,15 @@ require_relative "operation/transactions"
14
11
 
15
12
  # An **Operation** is a service object which is executed with a **State**.
16
13
  module Flow
17
- class OperationBase
18
- include ShortCircuIt
19
- include Technologic
20
- include Flow::TransactionWrapper
21
- include Flow::Operation::Accessors
22
- include Flow::Operation::Callbacks
23
- include Flow::Operation::Core
24
- include Flow::Operation::ErrorHandler
25
- include Flow::Operation::Execute
26
- include Flow::Operation::Failures
27
- include Flow::Operation::Status
28
- include Flow::Operation::Transactions
14
+ class OperationBase < Spicerack::RootObject
15
+ include TransactionWrapper
16
+ include Operation::Accessors
17
+ include Operation::Callbacks
18
+ include Operation::Core
19
+ include Operation::ErrorHandler
20
+ include Operation::Execute
21
+ include Operation::Failures
22
+ include Operation::Status
23
+ include Operation::Transactions
29
24
  end
30
25
  end
@@ -6,3 +6,6 @@ require_relative "custom_matchers/handle_error"
6
6
  require_relative "custom_matchers/use_operations"
7
7
  require_relative "custom_matchers/wrap_in_transaction"
8
8
  require_relative "custom_matchers/have_on_state"
9
+ require_relative "custom_matchers/read_state"
10
+ require_relative "custom_matchers/write_state"
11
+ require_relative "custom_matchers/access_state"
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RSpec matcher that tests usage of `ApplicationOperation.state_accessor` (or use of both `state_(reader|writer)`)
4
+ #
5
+ # class SomeTask < ApplicationOperation
6
+ # state_accessor :foo
7
+ # state_accessor :bar
8
+ #
9
+ # state_reader :baz
10
+ # state_writer :baz
11
+ # end
12
+ #
13
+ # RSpec.describe SomeTask, type: :operation do
14
+ # subject { described_class.new(state) }
15
+ #
16
+ # let(:state) { Class.new(ApplicationState).new }
17
+ #
18
+ # it { is_expected.to access_state :foo }
19
+ # it { is_expected.to access_state :bar }
20
+ # it { is_expected.to access_state :baz }
21
+ # end
22
+
23
+ RSpec::Matchers.define :access_state do |field|
24
+ match { |operation| expect(operation._state_accessors).to include field }
25
+ description { "access state" }
26
+ failure_message { "expected #{described_class} to access state #{field}" }
27
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RSpec matcher that tests usage of `ApplicationOperation.state_reader`
4
+ #
5
+ # class SomeTask < ApplicationOperation
6
+ # state_reader :foo
7
+ # state_reader :bar
8
+ # end
9
+ #
10
+ # RSpec.describe SomeTask, type: :operation do
11
+ # subject { described_class.new(state) }
12
+ #
13
+ # let(:state) { Class.new(ApplicationState).new }
14
+ #
15
+ # it { is_expected.to read_state :foo }
16
+ # it { is_expected.to read_state :bar }
17
+ # end
18
+
19
+ RSpec::Matchers.define :read_state do |field|
20
+ match { |operation| expect(operation._state_readers).to include field }
21
+ description { "read state" }
22
+ failure_message { "expected #{described_class} to read state #{field}" }
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RSpec matcher that tests usage of `ApplicationOperation.state_writer`
4
+ #
5
+ # class SomeTask < ApplicationOperation
6
+ # state_writer :foo
7
+ # state_writer :bar
8
+ # end
9
+ #
10
+ # RSpec.describe SomeTask, type: :operation do
11
+ # subject { described_class.new(state) }
12
+ #
13
+ # let(:state) { Class.new(ApplicationState).new }
14
+ #
15
+ # it { is_expected.to write_state :foo }
16
+ # it { is_expected.to write_state :bar }
17
+ # end
18
+
19
+ RSpec::Matchers.define :write_state do |field|
20
+ match { |operation| expect(operation._state_writers).to include field }
21
+ description { "write state" }
22
+ failure_message { "expected #{described_class} to write state #{field}" }
23
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "spicerack/spec_helper"
4
4
 
5
- require_relative "spec_helper/custom_matchers"
6
- require_relative "spec_helper/shared_contexts"
7
- require_relative "spec_helper/shoulda_matcher_helper"
8
- require_relative "spec_helper/rspec_configuration"
5
+ require_relative "rspec/custom_matchers"
6
+ require_relative "rspec/shared_contexts"
7
+ require_relative "rspec/shoulda_matcher_helper"
8
+ require_relative "rspec/configuration"
@@ -38,7 +38,7 @@ module Flow
38
38
 
39
39
  def ensure_validation_before(method)
40
40
  around_method method do |*arguments|
41
- raise Flow::State::Errors::NotValidated unless validated?
41
+ raise NotValidatedError unless validated?
42
42
 
43
43
  super(*arguments)
44
44
  end
@@ -1,14 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "state/errors/not_validated"
4
-
5
3
  require_relative "state/status"
6
4
  require_relative "state/output"
7
5
 
8
6
  # A **State** is an aggregation of input and derived data.
9
7
  module Flow
10
8
  class StateBase < Spicerack::InputModel
11
- include Flow::State::Status
12
- include Flow::State::Output
9
+ include State::Status
10
+ include State::Output
13
11
  end
14
12
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A **StateProxy** adapts a **State** to an **Operation** through an Operation's `state_*` accessors.
4
+ module Flow
5
+ class StateProxy
6
+ def initialize(state)
7
+ @state = state
8
+ end
9
+
10
+ # @deprecated
11
+ def method_missing(method_name, *arguments, &block)
12
+ return super unless state.respond_to?(method_name)
13
+
14
+ ActiveSupport::Deprecation.warn(
15
+ "Direct state access of `#{method_name}' on #{state.inspect} will be removed in a future version of flow. "\
16
+ "Use a state accessor instead - for more information see github/freshly/flow/deprecation_notice"
17
+ )
18
+ state.public_send(method_name, *arguments, &block)
19
+ end
20
+
21
+ # @deprecated
22
+ def respond_to_missing?(method_name, include_private = false)
23
+ state.respond_to?(method_name) || super
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :state
29
+ end
30
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Flow
4
- VERSION = "0.10.4"
4
+ VERSION = "0.10.5"
5
5
  end
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class <%= class_name %> < ApplicationOperation
4
+ # state_accessor :foo
5
+ # state_reader :bar
6
+ # state_writer :baz
7
+
4
8
  def behavior
5
9
  # Define what this operation does here
6
10
  end
@@ -3,15 +3,19 @@
3
3
  require "rails_helper"
4
4
 
5
5
  RSpec.describe ApplicationOperation, type: :operation do
6
- subject(:operation) { described_class.new(state) }
7
-
8
- let(:state) { double }
6
+ subject { described_class }
9
7
 
10
8
  it { is_expected.to inherit_from Flow::OperationBase }
11
9
 
12
10
  describe "#execute" do
13
11
  subject(:execute) { operation.execute }
14
-
15
- it "has some behavior"
12
+
13
+ context "when unsuccessful" do
14
+ pending "describe the effects of an unsuccessful `Operation#execute` (or delete) #{__FILE__}"
15
+ end
16
+
17
+ context "when successful" do
18
+ pending "describe the effects of a successful `Operation#execute` (or delete) #{__FILE__}"
19
+ end
16
20
  end
17
21
  end
@@ -19,6 +19,10 @@ RSpec.describe <%= class_name %>, type: :operation do
19
19
 
20
20
  it { is_expected.to inherit_from ApplicationOperation }
21
21
 
22
+ # it { is_expected.to access_state :foo }
23
+ # it { is_expected.to read_state :bar }
24
+ # it { is_expected.to write_state :baz }
25
+
22
26
  describe "#execute" do
23
27
  subject(:execute) { operation.execute }
24
28
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.4
4
+ version: 0.10.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Garside
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2020-01-07 00:00:00.000000000 Z
15
+ date: 2020-01-08 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: activemodel
@@ -174,6 +174,20 @@ dependencies:
174
174
  - - "~>"
175
175
  - !ruby/object:Gem::Version
176
176
  version: '1.3'
177
+ - !ruby/object:Gem::Dependency
178
+ name: bcrypt
179
+ requirement: !ruby/object:Gem::Requirement
180
+ requirements:
181
+ - - "~>"
182
+ - !ruby/object:Gem::Version
183
+ version: 3.1.13
184
+ type: :development
185
+ prerelease: false
186
+ version_requirements: !ruby/object:Gem::Requirement
187
+ requirements:
188
+ - - "~>"
189
+ - !ruby/object:Gem::Version
190
+ version: 3.1.13
177
191
  - !ruby/object:Gem::Dependency
178
192
  name: rspice
179
193
  requirement: !ruby/object:Gem::Requirement
@@ -242,7 +256,6 @@ files:
242
256
  - lib/flow/concerns/transaction_wrapper.rb
243
257
  - lib/flow/flow/callbacks.rb
244
258
  - lib/flow/flow/core.rb
245
- - lib/flow/flow/errors/state_invalid.rb
246
259
  - lib/flow/flow/flux.rb
247
260
  - lib/flow/flow/operations.rb
248
261
  - lib/flow/flow/status.rb
@@ -253,29 +266,30 @@ files:
253
266
  - lib/flow/operation/callbacks.rb
254
267
  - lib/flow/operation/core.rb
255
268
  - lib/flow/operation/error_handler.rb
256
- - lib/flow/operation/errors/already_executed.rb
257
- - lib/flow/operation/errors/already_rewound.rb
258
269
  - lib/flow/operation/execute.rb
259
270
  - lib/flow/operation/failures.rb
260
271
  - lib/flow/operation/status.rb
261
272
  - lib/flow/operation/transactions.rb
262
273
  - lib/flow/operation_base.rb
274
+ - lib/flow/rspec/configuration.rb
275
+ - lib/flow/rspec/custom_matchers.rb
276
+ - lib/flow/rspec/custom_matchers/access_state.rb
277
+ - lib/flow/rspec/custom_matchers/define_failure.rb
278
+ - lib/flow/rspec/custom_matchers/define_output.rb
279
+ - lib/flow/rspec/custom_matchers/handle_error.rb
280
+ - lib/flow/rspec/custom_matchers/have_on_state.rb
281
+ - lib/flow/rspec/custom_matchers/read_state.rb
282
+ - lib/flow/rspec/custom_matchers/use_operations.rb
283
+ - lib/flow/rspec/custom_matchers/wrap_in_transaction.rb
284
+ - lib/flow/rspec/custom_matchers/write_state.rb
285
+ - lib/flow/rspec/shared_contexts.rb
286
+ - lib/flow/rspec/shared_contexts/with_failing_operation.rb
287
+ - lib/flow/rspec/shoulda_matcher_helper.rb
263
288
  - lib/flow/spec_helper.rb
264
- - lib/flow/spec_helper/custom_matchers.rb
265
- - lib/flow/spec_helper/custom_matchers/define_failure.rb
266
- - lib/flow/spec_helper/custom_matchers/define_output.rb
267
- - lib/flow/spec_helper/custom_matchers/handle_error.rb
268
- - lib/flow/spec_helper/custom_matchers/have_on_state.rb
269
- - lib/flow/spec_helper/custom_matchers/use_operations.rb
270
- - lib/flow/spec_helper/custom_matchers/wrap_in_transaction.rb
271
- - lib/flow/spec_helper/rspec_configuration.rb
272
- - lib/flow/spec_helper/shared_contexts.rb
273
- - lib/flow/spec_helper/shared_contexts/with_failing_operation.rb
274
- - lib/flow/spec_helper/shoulda_matcher_helper.rb
275
- - lib/flow/state/errors/not_validated.rb
276
289
  - lib/flow/state/output.rb
277
290
  - lib/flow/state/status.rb
278
291
  - lib/flow/state_base.rb
292
+ - lib/flow/state_proxy.rb
279
293
  - lib/flow/version.rb
280
294
  - lib/generators/flow/USAGE
281
295
  - lib/generators/flow/application_flow/USAGE
@@ -334,7 +348,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
334
348
  - !ruby/object:Gem::Version
335
349
  version: '0'
336
350
  requirements: []
337
- rubygems_version: 3.0.6
351
+ rubyforge_project:
352
+ rubygems_version: 2.7.6
338
353
  signing_key:
339
354
  specification_version: 4
340
355
  summary: Write modular and reusable business logic that's understandable and maintainable.
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Flow
4
- module Errors
5
- class StateInvalid < StandardError; end
6
- end
7
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Flow
4
- module Operation
5
- module Errors
6
- class AlreadyExecuted < StandardError; end
7
- end
8
- end
9
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Flow
4
- module Operation
5
- module Errors
6
- class AlreadyRewound < StandardError; end
7
- end
8
- end
9
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Flow
4
- module State
5
- module Errors
6
- class NotValidated < StandardError; end
7
- end
8
- end
9
- end