standard_procedure_operations 0.1.2 → 0.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d0da11583bd5edf6619553813c302fbe675ce5563921e9960149d79888130c5
4
- data.tar.gz: aefecc4a15ed64289822318feb10c68128c121a359e049ede7f8a2805ca6405c
3
+ metadata.gz: d19a26ed4cb26b809efc2e4d551a4895613608348f3524f1764c000aceb6aeca
4
+ data.tar.gz: 2290a93af8caeacfa00fd941e62e4785366e4efa80d59579201bfe93cd85fa0a
5
5
  SHA512:
6
- metadata.gz: 998bca6b52931b7048585a22ab6cc53a53a9a872290bd6243a84bed7442c57209a2ea71f0502b744aeaa8b65461e9e7d1aca22321a3811fdac7f7a7d156e15e1
7
- data.tar.gz: 474207de30dbe046bd59587f1dfe0fa5f14f7a13d2f72075838e6594d8533c1c9b8ab79e245eafecaf490782329f220fe5dc0f9546e194138c0dafedf0544ef3
6
+ metadata.gz: 898f19eb0c69a359a939f5f3b563645dd36d8d6bc2e77707ecc1099698541c02210cbc351866d255a3a6747fb957fbb5f82146a120e53c1ba7d87cd68b280e61
7
+ data.tar.gz: 8b44ab81583ce10937a02d9cc9cbe7e6d7c153a83b29661d4cb1055876b39f181de7aa022aa9a3bbb2adf0e97aa618034582512bb72465172908b388243d11b1
data/README.md CHANGED
@@ -215,29 +215,69 @@ This gives you a number of possibilities:
215
215
  However, it also means that your database table could fill up with junk that you're no longer interested in. Therefore you can specify the maximum age of a task and, periodically, clean old tasks away. Every task has a `delete_at` field that, by default, is set to `90.days.from_now`. This can be changed by calling `Operations::Task.delete_after 7.days` (or whatever value you prefer). Then, run a cron job (once per day) that calls `Operations::Task.delete_expired`, removing any tasks whose `deleted_at` date has passed.
216
216
 
217
217
  ### Status messages
218
-
219
218
  Documentation coming soon.
220
219
 
221
220
  ### Child tasks
222
-
223
221
  Coming soon.
224
222
 
225
223
  ### Background operations and pauses
226
-
227
224
  Coming soon.
228
225
 
229
- ## Installation
230
- Add the gem to your Rails application's Gemfile:
226
+ ## Testing
227
+ Because operations are intended to model long, complex, flowcharts of decisions and actions, it can be a pain coming up with the combinations of inputs to test every path through the sequence.
228
+
229
+ Instead, you can test each state handler in isolation. As the handlers are state-less, we can simulate calling one by creating a task object and then calling the appropriate handler with the data that it expects. This is done by calling `handling`, which yields a `test` object with outcomes from the handler that we can inspect
231
230
 
231
+ To test if we have moved on to another state (for actions or decisions):
232
+ ```ruby
233
+ MyOperation.handling(:an_action_or_decision, some: "data") do |test|
234
+ assert_equal test.next_state, "new_state"
235
+ # or
236
+ expect(test).to have_moved_to "new_state"
237
+ end
238
+ ```
239
+ To test if some data has been set or modified (for actions):
240
+ ```ruby
241
+ MyOperation.handling(:an_action, existing_data: "some_value") do |test|
242
+ # has a new data value been added?
243
+ assert_equal test.new_data, "new_value"
244
+ # or
245
+ expect(test.new_data).to eq "new_value"
246
+ # has an existing data value been modified?
247
+ assert_equal test.existing_data, "some_other_value"
248
+ # or
249
+ expect(test.existing_data).to eq "some_other_value"
250
+ end
251
+ ```
252
+ To test the results from a result handler:
253
+ ```ruby
254
+ MyOperation.handling(:a_result, some: "data") do |test|
255
+ assert_equal test.outcome, "everything is as expected"
256
+ # or
257
+ expect(test.outcome).to eq "everything is as expected"
258
+ end
259
+ ```
260
+ To test if a handler has failed:
261
+ ```ruby
262
+ MyOperation.handling(:a_failure, some: "data") do |test|
263
+ assert_equal test.failure_message, "oh dear"
264
+ # or
265
+ expect(test).to have_failed_with "oh dear"
266
+ end
267
+ ```
268
+ If you are using RSpec, you must `require "operations/matchers"` to make the matchers available to your specs.
269
+
270
+ ## Installation
271
+ Step 1: Add the gem to your Rails application's Gemfile:
232
272
  ```ruby
233
273
  gem "standard_procedure_operations"
234
274
  ```
235
- Run `bundle install`, then copy and run the migrations to add the tasks table to your database:
275
+ Step 2: Run `bundle install`, then copy and run the migrations to add the tasks table to your database:
236
276
  ```sh
237
277
  bin/rails operations:install:migrations
238
278
  bin/rails db:migrate
239
279
  ```
240
- Create your own operations by inheriting from `Operations::Task` and revel in the stateful flowcharts!
280
+ Step 3: Create your own operations by inheriting from `Operations::Task` and revel in the stateful flowcharts!
241
281
  ```ruby
242
282
  class DailyLife < Operations::Task
243
283
  starts_with :am_i_awake?
@@ -253,6 +293,7 @@ class DailyLife < Operations::Task
253
293
  def am_i_awake? = (7..23).include?(Time.now.hour)
254
294
  end
255
295
  ```
296
+ Step 4: If you're using RSpec for testing, add `require "operations/matchers" to your "spec/rails_helper.rb" file.
256
297
 
257
298
  ## License
258
299
  The gem is available as open source under the terms of the [LGPL License](/LICENSE). This may or may not make it suitable for your needs.
@@ -2,4 +2,6 @@ class Operations::Task::DataCarrier < OpenStruct
2
2
  def go_to(state, message = nil) = task.go_to(state, self, message)
3
3
 
4
4
  def fail_with(message) = task.fail_with(message)
5
+
6
+ def complete(results) = task.complete(results)
5
7
  end
@@ -69,7 +69,7 @@ module Operations::Task::StateManagement
69
69
  def call(task, data)
70
70
  result = @condition.nil? ? task.send(@name, data) : data.instance_exec(&@condition)
71
71
  next_state = result ? @true_state : @false_state
72
- next_state.respond_to?(:call) ? data.instance_eval(&next_state) : task.go_to(next_state, data)
72
+ next_state.respond_to?(:call) ? data.instance_eval(&next_state) : data.go_to(next_state, data)
73
73
  end
74
74
  end
75
75
 
@@ -82,7 +82,7 @@ module Operations::Task::StateManagement
82
82
  def call(task, data)
83
83
  results = OpenStruct.new
84
84
  data.instance_exec(results, &@handler) unless @handler.nil?
85
- task.send :complete, results
85
+ data.complete(results)
86
86
  end
87
87
  end
88
88
  end
@@ -0,0 +1,27 @@
1
+ module Operations::Task::Testing
2
+ extend ActiveSupport::Concern
3
+
4
+ class_methods do
5
+ def handling state, **data, &block
6
+ task = new state: state
7
+ data = TestResultCarrier.new(data.merge(task: task))
8
+ handler_for(state).call(task, data)
9
+ data.completion_results.nil? ? block.call(data) : block.call(data.completion_results)
10
+ end
11
+ end
12
+
13
+ class TestResultCarrier < OpenStruct
14
+ def go_to(state, message = nil)
15
+ self.next_state = state
16
+ self.status_message = message || next_state.to_s
17
+ end
18
+
19
+ def fail_with(message)
20
+ self.failure_message = message
21
+ end
22
+
23
+ def complete(results)
24
+ self.completion_results = results
25
+ end
26
+ end
27
+ end
@@ -2,6 +2,7 @@ module Operations
2
2
  class Task < ApplicationRecord
3
3
  include StateManagement
4
4
  include Deletion
5
+ include Testing
5
6
  enum :status, in_progress: 0, completed: 1, failed: -1
6
7
  composed_of :results, class_name: "OpenStruct", constructor: ->(results) { results.to_h }, converter: ->(hash) { OpenStruct.new(hash) }
7
8
  serialize :results, coder: Operations::GlobalIDSerialiser, type: Hash, default: {}
@@ -20,6 +21,6 @@ module Operations
20
21
 
21
22
  def fail_with(message) = update! status: "failed", results: {failure_message: message.to_s}
22
23
 
23
- private def complete(results) = update!(status: "completed", results: results)
24
+ def complete(results) = update!(status: "completed", results: results)
24
25
  end
25
26
  end
@@ -0,0 +1,23 @@
1
+ require "rspec/expectations"
2
+
3
+ # Has the state of the task moved to the expected new state?
4
+ #
5
+ # Example:
6
+ # expect(test).to have_moved_to "new_state"
7
+ #
8
+ RSpec::Matchers.matcher :have_moved_to do |state|
9
+ match do |test_result|
10
+ test_result.next_state.to_s == state.to_s
11
+ end
12
+ end
13
+
14
+ # Has the task failed with a given failure message?
15
+ #
16
+ # Example:
17
+ # expect(test).to have_failed_with "some_error"
18
+ #
19
+ RSpec::Matchers.matcher :have_failed_with do |failure_message|
20
+ match do |test_result|
21
+ test_result.failure_message.to_s == failure_message.to_s
22
+ end
23
+ end
@@ -1,3 +1,3 @@
1
1
  module Operations
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard_procedure_operations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rahoul Baruah
@@ -37,11 +37,13 @@ files:
37
37
  - app/models/operations/task/data_carrier.rb
38
38
  - app/models/operations/task/deletion.rb
39
39
  - app/models/operations/task/state_management.rb
40
+ - app/models/operations/task/testing.rb
40
41
  - config/routes.rb
41
42
  - db/migrate/20250127160616_create_operations_tasks.rb
42
43
  - lib/operations.rb
43
44
  - lib/operations/engine.rb
44
45
  - lib/operations/global_id_serialiser.rb
46
+ - lib/operations/matchers.rb
45
47
  - lib/operations/missing_inputs_error.rb
46
48
  - lib/operations/version.rb
47
49
  - lib/standard_procedure_operations.rb