flow 0.10.4 → 0.10.5

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