standard_procedure_operations 0.2.3 → 0.2.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.
- checksums.yaml +4 -4
- data/README.md +63 -1
- data/app/models/operations/task/data_carrier.rb +7 -0
- data/app/models/operations/task/input_validation.rb +17 -0
- data/app/models/operations/task/state_management/action_handler.rb +12 -0
- data/app/models/operations/task/state_management/completion_handler.rb +14 -0
- data/app/models/operations/task/state_management/decision_handler.rb +24 -0
- data/app/models/operations/task/state_management.rb +2 -56
- data/app/models/operations/task/testing.rb +1 -1
- data/app/models/operations/task.rb +3 -1
- data/lib/operations/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 278552bfcbf35e101f4022a0d95004cc593473803b0b6578349afe945c1b6d18
|
4
|
+
data.tar.gz: 6be975b9b3d9b88e67eb63cef2cb582c9fbb12ff7b881a1ceb288f33ac74c21d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d271751ea12a1efd3083b32445ae3dfceeadcdea464702515ff83bfbf1c91157192bf4797149557ca0a1009d9963a97fcd62910359b78375d15eb2225f5c9612
|
7
|
+
data.tar.gz: 670ac183d0f130c4086ac4d0057e265fc69bdc5f7898628837fd8ed831588551d5cf1c37b9c81e8a4cfa104a957ba4caee870fc943f655201ab38baf1a9909b8
|
data/README.md
CHANGED
@@ -48,27 +48,38 @@ class PrepareDocumentForDownload < Operations::Task
|
|
48
48
|
starts_with :authorised?
|
49
49
|
|
50
50
|
decision :authorised? do
|
51
|
+
inputs :user
|
52
|
+
|
51
53
|
if_true :within_download_limits?
|
52
54
|
if_false { fail_with "unauthorised" }
|
53
55
|
end
|
54
56
|
|
55
57
|
decision :within_download_limits? do
|
58
|
+
inputs :user
|
59
|
+
|
56
60
|
if_true :use_filename_scrambler?
|
57
61
|
if_false { fail_with "download_limit_reached" }
|
58
62
|
end
|
59
63
|
|
60
64
|
decision :use_filename_scrambler? do
|
65
|
+
inputs :use_filename_scrambler
|
61
66
|
condition { use_filename_scrambler }
|
67
|
+
|
62
68
|
if_true :scramble_filename
|
63
69
|
if_false :return_filename
|
64
70
|
end
|
65
71
|
|
66
72
|
action :scramble_filename do
|
73
|
+
inputs :document
|
74
|
+
|
67
75
|
self.filename = "#{Faker::Lorem.word}#{File.extname(document.filename.to_s)}"
|
68
76
|
go_to :return_filename
|
69
77
|
end
|
70
78
|
|
71
79
|
result :return_filename do |results|
|
80
|
+
inputs :document
|
81
|
+
optional :filename
|
82
|
+
|
72
83
|
results.filename = filename || document.filename.to_s
|
73
84
|
end
|
74
85
|
|
@@ -89,6 +100,7 @@ It's up to you whether you define the condition as a block, as part of the decis
|
|
89
100
|
```ruby
|
90
101
|
decision :is_it_the_weekend? do
|
91
102
|
condition { Date.today.wday.in? [0, 6] }
|
103
|
+
|
92
104
|
if_true :have_a_party
|
93
105
|
if_false :go_to_work
|
94
106
|
end
|
@@ -104,6 +116,7 @@ def is_it_the_weekend?(data)
|
|
104
116
|
Date.today.wday.in? [0, 6]
|
105
117
|
end
|
106
118
|
```
|
119
|
+
|
107
120
|
A decision can also mark a failure, which will terminate the task.
|
108
121
|
```ruby
|
109
122
|
decision :authorised? do
|
@@ -113,6 +126,20 @@ decision :authorised? do
|
|
113
126
|
end
|
114
127
|
```
|
115
128
|
|
129
|
+
You can specify the data that is required for a decision handler to run by specifying `inputs` and `optionals`:
|
130
|
+
```ruby
|
131
|
+
decision :authorised? do
|
132
|
+
inputs :user
|
133
|
+
optionals :override
|
134
|
+
|
135
|
+
condition { override || user.administrator? }
|
136
|
+
|
137
|
+
if_true :do_some_work
|
138
|
+
if_false { fail_with "Unauthorised" }
|
139
|
+
end
|
140
|
+
```
|
141
|
+
In this case, the task will fail if there is no `user` specified. However, `override` is optional (and in fact the `optional` method is just there to help you document your operations).
|
142
|
+
|
116
143
|
### Actions
|
117
144
|
An action handler does some work, then moves to another state.
|
118
145
|
|
@@ -124,7 +151,21 @@ action :have_a_party do
|
|
124
151
|
go_to :send_invitations
|
125
152
|
end
|
126
153
|
```
|
127
|
-
|
154
|
+
You can specify the required and optional data for your action handler within the block. `optional` is decorative and to help with your documentation. Ensure you call `inputs` at the start of the block.
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
action :have_a_party do
|
158
|
+
inputs :task
|
159
|
+
optional :music
|
160
|
+
|
161
|
+
self.food = task.buy_some_food_for(number_of_guests)
|
162
|
+
self.beer = task.buy_some_beer_for(number_of_guests)
|
163
|
+
self.music ||= task.plan_a_party_playlist
|
164
|
+
go_to :send_invitations
|
165
|
+
end
|
166
|
+
```
|
167
|
+
|
168
|
+
Again, instead of using a block in the action handler, you could provide a method to do the work. However, you cannot specify `inputs` or `optional` data when using a method.
|
128
169
|
|
129
170
|
```ruby
|
130
171
|
action :have_a_party
|
@@ -143,6 +184,8 @@ Do not forget to call `go_to` from your action handler, otherwise the operation
|
|
143
184
|
### Results
|
144
185
|
A result handler marks the end of an operation, optionally returning some results. You need to copy your desired results from your [data](#data-and-results) to your results object. This is so only the information that matters to you is stored in the database (as many operations may have a large set of working data).
|
145
186
|
|
187
|
+
There is no method equivalent to a block handler.
|
188
|
+
|
146
189
|
```ruby
|
147
190
|
action :send_invitations do
|
148
191
|
self.invited_friends = (0..number_of_guests).collect do |i|
|
@@ -165,6 +208,24 @@ result :go_to_work
|
|
165
208
|
```
|
166
209
|
In this case, the task will be marked as `completed?`, the task's state will be `go_to_work` and `results` will be empty.
|
167
210
|
|
211
|
+
You can also specify the required and optional data for your result handler within the block. `optional` is decorative and to help with your documentation. Ensure you call `inputs` at the start of the block.
|
212
|
+
```ruby
|
213
|
+
action :send_invitations do
|
214
|
+
inputs :number_of_guests
|
215
|
+
self.invited_friends = (0..number_of_guests).collect do |i|
|
216
|
+
friend = friends.pop
|
217
|
+
FriendsMailer.with(recipient: friend).party_invitation.deliver_later
|
218
|
+
friend
|
219
|
+
end
|
220
|
+
go_to :ready_to_party
|
221
|
+
end
|
222
|
+
|
223
|
+
result :ready_to_party do |results|
|
224
|
+
inputs :invited_friends
|
225
|
+
|
226
|
+
results.invited_friends = invited_friends
|
227
|
+
end
|
228
|
+
|
168
229
|
### Calling an operation
|
169
230
|
You would use the earlier [PrepareDocumentForDownload](spec/examples/prepare_document_for_download_spec.rb) operation in a controller like this:
|
170
231
|
|
@@ -312,6 +373,7 @@ The gem is available as open source under the terms of the [LGPL License](/LICEN
|
|
312
373
|
|
313
374
|
## Roadmap
|
314
375
|
|
376
|
+
- [x] Specify inputs (required and optional) per-state, not just at the start
|
315
377
|
- [ ] Always raise errors instead of just recording a failure (will be useful when dealing with sub-tasks)
|
316
378
|
- [ ] Simplify calling sub-tasks (and testing the same)
|
317
379
|
- [ ] Split out the state-management definition stuff from the task class (so you can use it without subclassing Operations::Task)
|
@@ -4,4 +4,11 @@ class Operations::Task::DataCarrier < OpenStruct
|
|
4
4
|
def fail_with(message) = task.fail_with(message)
|
5
5
|
|
6
6
|
def complete(results) = task.complete(results)
|
7
|
+
|
8
|
+
def inputs(*names)
|
9
|
+
missing_inputs = (names.map(&:to_sym) - to_h.keys)
|
10
|
+
raise ArgumentError.new("Missing inputs: #{missing_inputs.join(", ")}") if missing_inputs.any?
|
11
|
+
end
|
12
|
+
|
13
|
+
def optional(*names) = nil
|
7
14
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Operations::Task::InputValidation
|
2
|
+
def inputs(*names) = @required_inputs = names.map(&:to_sym)
|
3
|
+
|
4
|
+
def optional(*names) = @optional_inputs = names.map(&:to_sym)
|
5
|
+
|
6
|
+
def optional_inputs = @optional_inputs ||= []
|
7
|
+
|
8
|
+
def required_inputs = @required_inputs ||= []
|
9
|
+
|
10
|
+
def required_inputs_are_present_in?(hash) = missing_inputs_from(hash).empty?
|
11
|
+
|
12
|
+
def missing_inputs_from(hash) = (required_inputs - hash.keys.map(&:to_sym))
|
13
|
+
|
14
|
+
def validate_inputs! hash
|
15
|
+
raise ArgumentError, "Missing inputs: #{missing_inputs_from(hash).join(", ")}" unless required_inputs_are_present_in?(hash)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class Operations::Task::StateManagement::ActionHandler
|
2
|
+
def initialize name, inputs = [], optional = [], &action
|
3
|
+
@name = name.to_sym
|
4
|
+
@required_inputs = inputs
|
5
|
+
@optional_inputs = optional
|
6
|
+
@action = action
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(task, data)
|
10
|
+
@action.nil? ? task.send(@name, data) : data.instance_exec(&@action)
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class Operations::Task::StateManagement::CompletionHandler
|
2
|
+
def initialize name, inputs = [], optional = [], &handler
|
3
|
+
@name = name.to_sym
|
4
|
+
@required_inputs = inputs
|
5
|
+
@optional_inputs = optional
|
6
|
+
@handler = handler
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(task, data)
|
10
|
+
results = OpenStruct.new
|
11
|
+
data.instance_exec(results, &@handler) unless @handler.nil?
|
12
|
+
data.complete(results)
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Operations::Task::StateManagement::DecisionHandler
|
2
|
+
include Operations::Task::InputValidation
|
3
|
+
|
4
|
+
def initialize name, &config
|
5
|
+
@name = name.to_sym
|
6
|
+
@condition = nil
|
7
|
+
@true_state = nil
|
8
|
+
@false_state = nil
|
9
|
+
instance_eval(&config)
|
10
|
+
end
|
11
|
+
|
12
|
+
def condition(&condition) = @condition = condition
|
13
|
+
|
14
|
+
def if_true(state = nil, &handler) = @true_state = state || handler
|
15
|
+
|
16
|
+
def if_false(state = nil, &handler) = @false_state = state || handler
|
17
|
+
|
18
|
+
def call(task, data)
|
19
|
+
validate_inputs! data.to_h
|
20
|
+
result = @condition.nil? ? task.send(@name, data) : data.instance_exec(&@condition)
|
21
|
+
next_state = result ? @true_state : @false_state
|
22
|
+
next_state.respond_to?(:call) ? data.instance_eval(&next_state) : data.go_to(next_state, data)
|
23
|
+
end
|
24
|
+
end
|
@@ -7,27 +7,19 @@ module Operations::Task::StateManagement
|
|
7
7
|
end
|
8
8
|
|
9
9
|
class_methods do
|
10
|
-
def inputs(*names) = @required_inputs = names.map(&:to_sym)
|
11
|
-
|
12
|
-
def required_inputs = @required_inputs ||= []
|
13
|
-
|
14
10
|
def starts_with(value) = @initial_state = value.to_sym
|
15
11
|
|
16
12
|
def initial_state = @initial_state
|
17
13
|
|
18
14
|
def decision(name, &config) = state_handlers[name.to_sym] = DecisionHandler.new(name, &config)
|
19
15
|
|
20
|
-
def action(name, &handler) = state_handlers[name.to_sym] = ActionHandler.new(name, &handler)
|
16
|
+
def action(name, inputs: [], optional: [], &handler) = state_handlers[name.to_sym] = ActionHandler.new(name, inputs, optional, &handler)
|
21
17
|
|
22
|
-
def result(name, &results) = state_handlers[name.to_sym] = CompletionHandler.new(name, &results)
|
18
|
+
def result(name, inputs: [], optional: [], &results) = state_handlers[name.to_sym] = CompletionHandler.new(name, inputs, optional, &results)
|
23
19
|
|
24
20
|
def state_handlers = @state_handlers ||= {}
|
25
21
|
|
26
22
|
def handler_for(state) = state_handlers[state.to_sym]
|
27
|
-
|
28
|
-
def required_inputs_are_present_in?(data) = missing_inputs_from(data).empty?
|
29
|
-
|
30
|
-
def missing_inputs_from(data) = (required_inputs - data.keys.map(&:to_sym))
|
31
23
|
end
|
32
24
|
|
33
25
|
private def handler_for(state) = self.class.handler_for(state.to_sym)
|
@@ -39,50 +31,4 @@ module Operations::Task::StateManagement
|
|
39
31
|
private def state_is_valid
|
40
32
|
errors.add :state, :invalid if state.blank? || handler_for(state.to_sym).nil?
|
41
33
|
end
|
42
|
-
|
43
|
-
class ActionHandler
|
44
|
-
def initialize name, &action
|
45
|
-
@name = name.to_sym
|
46
|
-
@action = action
|
47
|
-
end
|
48
|
-
|
49
|
-
def call(task, data)
|
50
|
-
@action.nil? ? task.send(@name, data) : data.instance_exec(&@action)
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
class DecisionHandler
|
55
|
-
def initialize name, &config
|
56
|
-
@name = name.to_sym
|
57
|
-
@condition = nil
|
58
|
-
@true_state = nil
|
59
|
-
@false_state = nil
|
60
|
-
instance_eval(&config)
|
61
|
-
end
|
62
|
-
|
63
|
-
def condition(&condition) = @condition = condition
|
64
|
-
|
65
|
-
def if_true(state = nil, &handler) = @true_state = state || handler
|
66
|
-
|
67
|
-
def if_false(state = nil, &handler) = @false_state = state || handler
|
68
|
-
|
69
|
-
def call(task, data)
|
70
|
-
result = @condition.nil? ? task.send(@name, data) : data.instance_exec(&@condition)
|
71
|
-
next_state = result ? @true_state : @false_state
|
72
|
-
next_state.respond_to?(:call) ? data.instance_eval(&next_state) : data.go_to(next_state, data)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
class CompletionHandler
|
77
|
-
def initialize name, &handler
|
78
|
-
@name = name.to_sym
|
79
|
-
@handler = handler
|
80
|
-
end
|
81
|
-
|
82
|
-
def call(task, data)
|
83
|
-
results = OpenStruct.new
|
84
|
-
data.instance_exec(results, &@handler) unless @handler.nil?
|
85
|
-
data.complete(results)
|
86
|
-
end
|
87
|
-
end
|
88
34
|
end
|
@@ -10,7 +10,7 @@ module Operations::Task::Testing
|
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
|
-
class TestResultCarrier <
|
13
|
+
class TestResultCarrier < Operations::Task::DataCarrier
|
14
14
|
def go_to(state, message = nil)
|
15
15
|
self.next_state = state
|
16
16
|
self.status_message = message || next_state.to_s
|
@@ -3,11 +3,13 @@ module Operations
|
|
3
3
|
include StateManagement
|
4
4
|
include Deletion
|
5
5
|
include Testing
|
6
|
+
extend InputValidation
|
7
|
+
|
6
8
|
enum :status, in_progress: 0, completed: 1, failed: -1
|
7
9
|
serialize :results, coder: Operations::GlobalIDSerialiser, type: Hash, default: {}
|
8
10
|
|
9
11
|
def self.call(data = {})
|
10
|
-
|
12
|
+
validate_inputs! data
|
11
13
|
create!(state: initial_state, status_message: "").tap do |task|
|
12
14
|
task.send(:process_current_state, DataCarrier.new(data.merge(task: task)))
|
13
15
|
end
|
data/lib/operations/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: standard_procedure_operations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rahoul Baruah
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-02-03 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rails
|
@@ -36,7 +36,11 @@ files:
|
|
36
36
|
- app/models/operations/task.rb
|
37
37
|
- app/models/operations/task/data_carrier.rb
|
38
38
|
- app/models/operations/task/deletion.rb
|
39
|
+
- app/models/operations/task/input_validation.rb
|
39
40
|
- app/models/operations/task/state_management.rb
|
41
|
+
- app/models/operations/task/state_management/action_handler.rb
|
42
|
+
- app/models/operations/task/state_management/completion_handler.rb
|
43
|
+
- app/models/operations/task/state_management/decision_handler.rb
|
40
44
|
- app/models/operations/task/testing.rb
|
41
45
|
- config/routes.rb
|
42
46
|
- db/migrate/20250127160616_create_operations_tasks.rb
|