flow_state 0.1.5 → 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 +4 -4
- data/MIGRATION_0_1_to_0_2.md +87 -0
- data/README.md +120 -177
- data/lib/flow_state/base.rb +76 -31
- data/lib/flow_state/version.rb +1 -1
- data/lib/generators/flow_state/templates/create_flow_state_flows.rb +4 -1
- metadata +10 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d474458f9e1405cd005acba075a3c0bcf21497f086f874171dbe47fe8497aac2
|
4
|
+
data.tar.gz: 56b3ab22b7a5b46bcaa79dad827861123dc24d31ecc91fc1f66f7b85b503545d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 84668ec5878cadacbf156e30290ec0225cebc1f8460141872a51eaeead892a4b533e9532386c4c02fd69c0a43e8ea8fd744afaeff089291e62d7e363fb03c88a
|
7
|
+
data.tar.gz: e2f992dfee8d35e15c55fcd7036721092e5cda2f282e251fdacea44d12de21b3c8e423db94addbde62987b2f9fd181fe8a67d160e5a6e5b55a8ecdfed3fb6bf9
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# Flow State 0.1 → 0.2 Migration Guide
|
2
|
+
|
3
|
+
---
|
4
|
+
|
5
|
+
## 1. Database migration
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
# db/migrate/xxxxxxxxxxxxxx_flow_state_02_upgrade.rb
|
9
|
+
class FlowState02Upgrade < ActiveRecord::Migration[6.1]
|
10
|
+
def change
|
11
|
+
rename_column :flow_state_flows, :payload, :props
|
12
|
+
add_column :flow_state_flows, :type, :string
|
13
|
+
add_column :flow_state_flows, :completed_at, :datetime
|
14
|
+
add_column :flow_state_flows, :last_errored_at, :datetime
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
```
|
19
|
+
|
20
|
+
Run the migration and bump your gem version to `0.2.0`.
|
21
|
+
|
22
|
+
---
|
23
|
+
|
24
|
+
## 2. Required changes
|
25
|
+
|
26
|
+
| Area | Old (≤ 0.1) | New (0.2) |
|
27
|
+
| ------------------ | -------------------------------------- | ---------------------------------------------------------- |
|
28
|
+
| Flow setup | `initial_state` optional | `initial_state` and `completed_state` **required** |
|
29
|
+
| Completed handling | manual `after_transition { destroy! }` | `destroy_on_complete` handles cleanup automatically |
|
30
|
+
| Column rename | `payload` | `props`, used via `flow.props["key"]` only |
|
31
|
+
| Prop accessors | auto-generated methods | removed – use `props["key"]` |
|
32
|
+
| Macro rename | `persist :foo, Hash` | `persists :foo, Hash` |
|
33
|
+
| Transition keyword | `persists:` | `persist:` |
|
34
|
+
| Timestamps | — | `completed_at` and `last_errored_at` tracked automatically |
|
35
|
+
|
36
|
+
---
|
37
|
+
|
38
|
+
## 3. Example refactor
|
39
|
+
|
40
|
+
### Flow definition
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
class SignupFlow < FlowState::Base
|
44
|
+
state :draft
|
45
|
+
state :processing
|
46
|
+
state :failed, error: true
|
47
|
+
state :completed
|
48
|
+
|
49
|
+
initial_state :draft
|
50
|
+
completed_state :completed
|
51
|
+
destroy_on_complete
|
52
|
+
|
53
|
+
prop :user_id, Integer
|
54
|
+
persists :external_response, Hash
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
### Usage
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
flow = SignupFlow.create!(props: { "user_id" => 42 })
|
62
|
+
|
63
|
+
flow.transition!(from: :draft, to: :processing)
|
64
|
+
|
65
|
+
flow.transition!(
|
66
|
+
from: :processing,
|
67
|
+
to: :completed,
|
68
|
+
persist: :external_response
|
69
|
+
) { { status: 200 } }
|
70
|
+
|
71
|
+
# If destroy_on_complete was set:
|
72
|
+
# flow.destroyed? # => true
|
73
|
+
# Otherwise:
|
74
|
+
# flow.completed_at # => Time
|
75
|
+
# flow.last_errored_at # => nil (unless failed state was hit)
|
76
|
+
```
|
77
|
+
|
78
|
+
---
|
79
|
+
|
80
|
+
## 4. Cleanup tips
|
81
|
+
|
82
|
+
- Remove any `after_transition { destroy! }` logic and replace with `destroy_on_complete`.
|
83
|
+
- Stop calling dynamic prop getters like `flow.name`; use `flow.props["name"]` instead.
|
84
|
+
- Rename all usages of `persist` (macro) to `persists`.
|
85
|
+
- Update any `persists:` keyword args to `persist:` in your `transition!` calls.
|
86
|
+
|
87
|
+
You're now on **Flow State 0.2.0**.
|
data/README.md
CHANGED
@@ -4,251 +4,194 @@
|
|
4
4
|
|
5
5
|
---
|
6
6
|
|
7
|
-
**FlowState**
|
7
|
+
**FlowState** is a small gem for Rails, for building *state-machine–style* workflows that persist every step, artefact and decision to your database.
|
8
|
+
Everything is explicit – no metaprogramming, no hidden callbacks, no magic helpers.
|
8
9
|
|
9
|
-
|
10
|
-
Every workflow instance, transition and artefact is persisted to the database.
|
11
|
-
Every change happens through clear, intention-revealing methods that you define yourself.
|
10
|
+
Use it when you need to:
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
- Store and type-check arbitrary payloads (artefacts) between steps
|
18
|
-
- Avoid race conditions via database locks and explicit guards
|
12
|
+
* orchestrate multi-step jobs that call external services
|
13
|
+
* restart safely after crashes or retries
|
14
|
+
* inspect an audit trail of *what happened, when and why*
|
15
|
+
* attach typed artefacts (payloads) to a given transition
|
19
16
|
|
20
17
|
---
|
21
18
|
|
22
|
-
##
|
19
|
+
## What’s new in 0.2
|
23
20
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
- **No Magic** — No metaprogramming, no dynamic method generation, no `method_missing` tricks.
|
21
|
+
| Change | Why it matters |
|
22
|
+
| ----------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
23
|
+
| **`initial_state` & `completed_state` are mandatory** | Keeps definitions explicit and prevents silent mis-configuration. |
|
24
|
+
| **`destroy_on_complete` macro** | One-liner to delete finished flows – replaces manual `after_transition { destroy! }`. |
|
25
|
+
| **`payload` → `props` column** | Aligns storage with the `prop` DSL (`flow.props["key"]`). No more auto-generated getters. |
|
26
|
+
| **`persist` macro → `persists`** | Reads better, matches the transition keyword (`persist:`). |
|
27
|
+
| **`completed_at` & `last_errored_at` timestamps** | Easier querying: `where(completed_at: ..)` or `where.not(last_errored_at: nil)`. |
|
32
28
|
|
33
|
-
|
34
|
-
|
35
|
-
## Installation
|
36
|
-
|
37
|
-
Add to your bundle:
|
38
|
-
|
39
|
-
```bash
|
40
|
-
bundle add flow_state
|
41
|
-
```
|
42
|
-
|
43
|
-
Generate the tables:
|
44
|
-
|
45
|
-
```bash
|
46
|
-
bin/rails generate flow_state:install
|
47
|
-
bin/rails db:migrate
|
48
|
-
```
|
29
|
+
See the [migration guide](./MIGRATION_0_1_to_0_2.md) for a drop-in migration.
|
49
30
|
|
50
31
|
---
|
51
32
|
|
52
|
-
##
|
53
|
-
|
54
|
-
Suppose you want to build a workflow that:
|
55
|
-
- Fetches a response from a third party API
|
56
|
-
- Allows for retrying the fetch on failure
|
57
|
-
- And persists the response to the workflow
|
58
|
-
- Then saves the persisted response to the database
|
59
|
-
- As two separate, encapsulated jobs
|
60
|
-
- Tracking each step, while protecting against race conditions
|
33
|
+
## Quick example – syncing an API and saving the result
|
61
34
|
|
62
|
-
|
63
|
-
|
64
|
-
### Define your Flow
|
35
|
+
### 1 Define the flow
|
65
36
|
|
66
37
|
```ruby
|
67
|
-
class
|
68
|
-
|
69
|
-
prop :
|
38
|
+
class SyncApiFlow < FlowState::Base
|
39
|
+
# typed metadata saved in the JSON `props` column
|
40
|
+
prop :record_id, String
|
41
|
+
prop :remote_api_id, String
|
70
42
|
|
43
|
+
# states
|
71
44
|
state :pending
|
72
|
-
state :
|
73
|
-
state :
|
74
|
-
state :
|
75
|
-
state :
|
76
|
-
state :
|
77
|
-
state :
|
78
|
-
state :
|
79
|
-
state :completed
|
45
|
+
state :fetching
|
46
|
+
state :fetched
|
47
|
+
state :saving
|
48
|
+
state :saved
|
49
|
+
state :failed_fetch, error: true
|
50
|
+
state :failed_save, error: true
|
51
|
+
state :done
|
80
52
|
|
81
|
-
|
53
|
+
# mandatory
|
54
|
+
initial_state :pending
|
55
|
+
completed_state :done
|
56
|
+
destroy_on_complete # <— remove if you prefer to keep rows
|
82
57
|
|
83
|
-
|
58
|
+
# artefacts persisted at runtime
|
59
|
+
persists :api_response, Hash
|
84
60
|
|
85
|
-
|
86
|
-
transition!(
|
87
|
-
from: %i[pending],
|
88
|
-
to: :picked,
|
89
|
-
after_transition: -> { enqueue_fetch }
|
90
|
-
)
|
91
|
-
end
|
61
|
+
# public API ---------------------------------------------------------
|
92
62
|
|
93
|
-
def
|
94
|
-
transition!(
|
95
|
-
from: %i[picked failed_to_fetch_third_party_api],
|
96
|
-
to: :fetching_third_party_api
|
97
|
-
)
|
63
|
+
def start_fetch!
|
64
|
+
transition!(from: :pending, to: :fetching)
|
98
65
|
end
|
99
66
|
|
100
|
-
def
|
67
|
+
def finish_fetch!(response)
|
101
68
|
transition!(
|
102
|
-
from:
|
103
|
-
to:
|
104
|
-
|
105
|
-
after_transition: -> {
|
106
|
-
) {
|
69
|
+
from: :fetching,
|
70
|
+
to: :fetched,
|
71
|
+
persist: :api_response,
|
72
|
+
after_transition: -> { SaveJob.perform_later(id) }
|
73
|
+
) { response }
|
107
74
|
end
|
108
75
|
|
109
|
-
def
|
110
|
-
transition!(
|
111
|
-
from: :fetching_third_party_api,
|
112
|
-
to: :failed_to_fetch_third_party_api
|
113
|
-
)
|
114
|
-
end
|
115
|
-
|
116
|
-
def start_record_save!
|
117
|
-
transition!(
|
118
|
-
from: %i[fetched_third_party_api failed_to_save_my_record],
|
119
|
-
to: :saving_my_record,
|
120
|
-
guard: -> { flow_artefacts.where(name: 'third_party_api_response').exists? }
|
121
|
-
)
|
76
|
+
def fail_fetch!
|
77
|
+
transition!(from: :fetching, to: :failed_fetch)
|
122
78
|
end
|
123
79
|
|
124
|
-
def
|
125
|
-
transition!(
|
126
|
-
from: :saving_my_record,
|
127
|
-
to: :saved_my_record,
|
128
|
-
after_transition: -> { complete! }
|
129
|
-
)
|
80
|
+
def start_save!
|
81
|
+
transition!(from: :fetched, to: :saving)
|
130
82
|
end
|
131
83
|
|
132
|
-
def
|
133
|
-
transition!(
|
134
|
-
from: :saving_my_record,
|
135
|
-
to: :failed_to_save_my_record
|
136
|
-
)
|
84
|
+
def finish_save!
|
85
|
+
transition!(from: :saving, to: :saved, after_transition: -> { complete! })
|
137
86
|
end
|
138
87
|
|
139
|
-
def
|
140
|
-
transition!(from: :
|
141
|
-
end
|
142
|
-
|
143
|
-
private
|
144
|
-
|
145
|
-
def enqueue_fetch
|
146
|
-
FetchThirdPartyJob.perform_later(flow_id: id)
|
88
|
+
def fail_save!
|
89
|
+
transition!(from: :saving, to: :failed_save)
|
147
90
|
end
|
148
91
|
|
149
|
-
def
|
150
|
-
|
92
|
+
def complete!
|
93
|
+
transition!(from: :saved, to: :done)
|
151
94
|
end
|
152
95
|
end
|
153
96
|
```
|
154
97
|
|
155
|
-
|
156
|
-
|
157
|
-
### Background Jobs
|
158
|
-
|
159
|
-
Each job moves the flow through the correct states, step-by-step.
|
160
|
-
|
161
|
-
---
|
162
|
-
|
163
|
-
**Create and start the flow**
|
98
|
+
### 2 Kick it off
|
164
99
|
|
165
100
|
```ruby
|
166
|
-
flow =
|
167
|
-
|
168
|
-
|
169
|
-
)
|
101
|
+
flow = SyncApiFlow.create!(props: {
|
102
|
+
"record_id" => record.id,
|
103
|
+
"remote_api_id" => remote_id
|
104
|
+
})
|
170
105
|
|
171
|
-
flow.
|
106
|
+
flow.start_fetch!
|
107
|
+
FetchJob.perform_later(flow.id)
|
172
108
|
```
|
173
109
|
|
174
|
-
|
175
|
-
|
176
|
-
**Fetch Third Party API Response**
|
110
|
+
### 3 Jobs move the flow
|
177
111
|
|
178
112
|
```ruby
|
179
|
-
class
|
180
|
-
|
181
|
-
|
182
|
-
|
113
|
+
class FetchJob < ApplicationJob
|
114
|
+
def perform(flow_id)
|
115
|
+
flow = SyncApiFlow.find(flow_id)
|
116
|
+
|
117
|
+
response = ThirdParty::Client.new(flow.props["remote_api_id"]).get
|
118
|
+
flow.finish_fetch!(response)
|
119
|
+
rescue StandardError => e
|
120
|
+
begin
|
121
|
+
flow.fail_fetch!
|
122
|
+
rescue StandardError
|
123
|
+
nil
|
124
|
+
end
|
125
|
+
raise e
|
126
|
+
end
|
127
|
+
end
|
183
128
|
|
184
|
-
|
185
|
-
|
129
|
+
class SaveJob < ApplicationJob
|
130
|
+
def perform(flow_id)
|
131
|
+
flow = SyncApiFlow.find(flow_id)
|
186
132
|
|
187
|
-
flow.
|
133
|
+
flow.start_save!
|
188
134
|
|
189
|
-
|
135
|
+
MyRecord.find(flow.props["record_id"]).update!(payload: artefact(flow, :api_response))
|
190
136
|
|
191
|
-
flow.
|
192
|
-
rescue
|
193
|
-
|
194
|
-
|
137
|
+
flow.finish_save!
|
138
|
+
rescue StandardError => e
|
139
|
+
begin
|
140
|
+
flow.fail_save!
|
141
|
+
rescue StandardError
|
142
|
+
nil
|
143
|
+
end
|
144
|
+
raise e
|
145
|
+
end
|
195
146
|
end
|
196
147
|
|
197
148
|
private
|
198
149
|
|
199
|
-
def flow
|
200
|
-
|
150
|
+
def artefact(flow, name)
|
151
|
+
flow.flow_artefacts.find_by!(name: name.to_s).payload
|
201
152
|
end
|
202
153
|
end
|
203
154
|
```
|
204
155
|
|
205
|
-
|
156
|
+
That’s it – every step, timestamp, artefact and error is stored automatically.
|
206
157
|
|
207
|
-
|
158
|
+
---
|
208
159
|
|
209
|
-
|
210
|
-
class SaveLocalRecordJob < ApplicationJob
|
211
|
-
def perform(flow_id:)
|
212
|
-
@flow_id = flow_id
|
160
|
+
## API reference
|
213
161
|
|
214
|
-
|
162
|
+
### DSL macros
|
215
163
|
|
216
|
-
|
164
|
+
| Macro | Description |
|
165
|
+
| --------------------------------- | --------------------------------------------------------------------- |
|
166
|
+
| `state :name, error: false` | Declare a state. `error: true` marks it as a failure state. |
|
167
|
+
| `initial_state :name` | **Required.** First state assigned to new flows. |
|
168
|
+
| `completed_state :name` | **Required.** Terminal state that marks the flow as finished. |
|
169
|
+
| `destroy_on_complete(flag: true)` | Delete the row automatically once the flow reaches `completed_state`. |
|
170
|
+
| `prop :key, Type` | Typed key stored in JSONB `props`. Access via `flow.props["key"]`. |
|
171
|
+
| `persists :name, Type` | Declare an artefact that can be saved during a transition. |
|
217
172
|
|
218
|
-
|
219
|
-
rescue
|
220
|
-
flow.fail_record_save!
|
221
|
-
raise
|
222
|
-
end
|
173
|
+
### Instance helpers
|
223
174
|
|
224
|
-
|
175
|
+
| Method | Use |
|
176
|
+
| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
177
|
+
| `transition!(from:, to:, guard: nil, persist: nil, after_transition: nil) { ... }` | Perform a state change with optional guard, artefact persistence and callback. |
|
178
|
+
| `completed?` | `true` if `current_state == completed_state`. |
|
179
|
+
| `errored?` | `true` if the current state is marked `error: true`. |
|
225
180
|
|
226
|
-
|
227
|
-
@flow ||= SyncThirdPartyApiFlow.find(@flow_id)
|
228
|
-
end
|
181
|
+
---
|
229
182
|
|
230
|
-
|
231
|
-
flow.flow_artefacts
|
232
|
-
.find_by!(name: 'third_party_api_response')
|
233
|
-
.payload
|
234
|
-
end
|
183
|
+
## Installation
|
235
184
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
185
|
+
```bash
|
186
|
+
bundle add flow_state
|
187
|
+
bin/rails generate flow_state:install
|
188
|
+
bin/rails db:migrate
|
240
189
|
```
|
241
190
|
|
242
|
-
|
243
|
-
|
244
|
-
## Why use FlowState?
|
245
|
-
|
246
|
-
Because it enables you to model workflows explicitly,
|
247
|
-
and track real-world execution reliably —
|
248
|
-
**without any magic**.
|
191
|
+
Follow the [migration guide](./MIGRATION_0_1_to_0_2.md) if you’re upgrading from 0.1.
|
249
192
|
|
250
193
|
---
|
251
194
|
|
252
195
|
## License
|
253
196
|
|
254
|
-
MIT
|
197
|
+
MIT
|
data/lib/flow_state/base.rb
CHANGED
@@ -3,11 +3,14 @@
|
|
3
3
|
module FlowState
|
4
4
|
# Base Model to be extended by app flows
|
5
5
|
class Base < ActiveRecord::Base # rubocop:disable Metrics/ClassLength
|
6
|
-
class UnknownStateError
|
6
|
+
class UnknownStateError < StandardError; end
|
7
7
|
class InvalidTransitionError < StandardError; end
|
8
8
|
class PayloadValidationError < StandardError; end
|
9
|
-
class
|
10
|
-
class
|
9
|
+
class PropsValidationError < StandardError; end
|
10
|
+
class GuardFailedError < StandardError; end
|
11
|
+
class UnknownArtefactError < StandardError; end
|
12
|
+
class MissingInitialStateError < StandardError; end
|
13
|
+
class MissingCompletedStateError < StandardError; end
|
11
14
|
|
12
15
|
DEPRECATOR = ActiveSupport::Deprecation.new(FlowState::VERSION, 'FlowState')
|
13
16
|
|
@@ -32,12 +35,23 @@ module FlowState
|
|
32
35
|
name ? @initial_state = name.to_sym : @initial_state
|
33
36
|
end
|
34
37
|
|
38
|
+
def completed_state(name = nil)
|
39
|
+
name ? @completed_state = name.to_sym : @completed_state
|
40
|
+
end
|
41
|
+
|
42
|
+
def destroy_on_complete(flag: true)
|
43
|
+
@destroy_on_complete = flag
|
44
|
+
end
|
45
|
+
|
46
|
+
def destroy_on_complete?
|
47
|
+
!!@destroy_on_complete
|
48
|
+
end
|
49
|
+
|
35
50
|
def prop(name, type)
|
36
|
-
|
37
|
-
define_method(name) { payload&.dig(name.to_s) }
|
51
|
+
props_schema[name.to_sym] = type
|
38
52
|
end
|
39
53
|
|
40
|
-
def
|
54
|
+
def persists(name, type)
|
41
55
|
artefact_schema[name.to_sym] = type
|
42
56
|
end
|
43
57
|
|
@@ -49,8 +63,8 @@ module FlowState
|
|
49
63
|
@error_states ||= []
|
50
64
|
end
|
51
65
|
|
52
|
-
def
|
53
|
-
@
|
66
|
+
def props_schema
|
67
|
+
@props_schema ||= {}
|
54
68
|
end
|
55
69
|
|
56
70
|
def artefact_schema
|
@@ -59,13 +73,15 @@ module FlowState
|
|
59
73
|
end
|
60
74
|
|
61
75
|
validates :current_state, presence: true
|
62
|
-
validate :
|
76
|
+
validate :validate_props
|
77
|
+
after_commit :handle_completion, on: :update
|
63
78
|
|
64
|
-
after_initialize
|
79
|
+
after_initialize :validate_initial_states!, if: :new_record?
|
80
|
+
after_initialize :assign_initial_state, if: :new_record?
|
65
81
|
|
66
|
-
def transition!(from:, to:, guard: nil,
|
67
|
-
setup_transition!(from, to, guard,
|
68
|
-
perform_transition!(to,
|
82
|
+
def transition!(from:, to:, guard: nil, persist: nil, after_transition: nil, &block)
|
83
|
+
setup_transition!(from, to, guard, persist, &block)
|
84
|
+
perform_transition!(to, persist)
|
69
85
|
after_transition&.call
|
70
86
|
end
|
71
87
|
|
@@ -73,8 +89,37 @@ module FlowState
|
|
73
89
|
self.class.error_states.include?(current_state&.to_sym)
|
74
90
|
end
|
75
91
|
|
92
|
+
def completed?
|
93
|
+
self.class.completed_state && current_state&.to_sym == self.class.completed_state
|
94
|
+
end
|
95
|
+
|
96
|
+
def handle_completion
|
97
|
+
return unless completed?
|
98
|
+
|
99
|
+
if self.class.destroy_on_complete?
|
100
|
+
destroy!
|
101
|
+
elsif completed_at.nil?
|
102
|
+
update_column(:completed_at, Time.current)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
76
106
|
private
|
77
107
|
|
108
|
+
def validate_initial_states!
|
109
|
+
init_state = self.class.initial_state
|
110
|
+
comp_state = self.class.completed_state
|
111
|
+
|
112
|
+
raise MissingInitialStateError, "#{self.class} must declare initial_state" unless init_state
|
113
|
+
raise MissingCompletedStateError, "#{self.class} must declare completed_state" unless comp_state
|
114
|
+
|
115
|
+
unknown = [init_state, comp_state] - self.class.all_states
|
116
|
+
raise UnknownStateError, "unknown #{unknown.join(', ')}" if unknown.any?
|
117
|
+
end
|
118
|
+
|
119
|
+
def assign_initial_state
|
120
|
+
self.current_state ||= self.class.initial_state
|
121
|
+
end
|
122
|
+
|
78
123
|
def setup_transition!(from, to, guard, persists, &block)
|
79
124
|
@from_states = Array(from).map(&:to_sym)
|
80
125
|
@to_state = to.to_sym
|
@@ -85,14 +130,19 @@ module FlowState
|
|
85
130
|
end
|
86
131
|
|
87
132
|
def perform_transition!(to, persists) # rubocop:disable Metrics/MethodLength
|
88
|
-
|
89
|
-
|
90
|
-
|
133
|
+
transaction do
|
134
|
+
save! if changed?
|
135
|
+
with_lock do
|
136
|
+
ensure_valid_from_state!(@from_states, to)
|
91
137
|
@tr = flow_transitions.create!(
|
92
138
|
transitioned_from: current_state,
|
93
139
|
transitioned_to: to
|
94
140
|
)
|
95
|
-
|
141
|
+
|
142
|
+
attrs = { current_state: to }
|
143
|
+
attrs[:last_errored_at] = (Time.current if self.class.error_states.include?(to.to_sym))
|
144
|
+
update!(attrs)
|
145
|
+
|
96
146
|
persist_artefact! if persists
|
97
147
|
end
|
98
148
|
end
|
@@ -121,7 +171,8 @@ module FlowState
|
|
121
171
|
def persist_artefact!
|
122
172
|
expected = self.class.artefact_schema[@artefact_name]
|
123
173
|
unless @artefact_data.is_a?(expected)
|
124
|
-
raise PayloadValidationError,
|
174
|
+
raise PayloadValidationError,
|
175
|
+
"artefact #{@artefact_name} must be #{expected}"
|
125
176
|
end
|
126
177
|
|
127
178
|
@tr.flow_artefacts.create!(
|
@@ -130,28 +181,22 @@ module FlowState
|
|
130
181
|
)
|
131
182
|
end
|
132
183
|
|
133
|
-
def resolve_initial_state
|
134
|
-
init = self.class.initial_state || self.class.all_states.first
|
135
|
-
ensure_known_states!([init]) if init
|
136
|
-
init
|
137
|
-
end
|
138
|
-
|
139
184
|
def ensure_known_states!(states)
|
140
185
|
unknown = states - self.class.all_states
|
141
186
|
raise UnknownStateError, "unknown #{unknown.join(', ')}" if unknown.any?
|
142
187
|
end
|
143
188
|
|
144
|
-
def
|
145
|
-
schema = self.class.
|
189
|
+
def validate_props
|
190
|
+
schema = self.class.props_schema
|
146
191
|
return if schema.empty?
|
147
192
|
|
148
193
|
schema.each do |key, klass|
|
149
|
-
v =
|
150
|
-
raise
|
151
|
-
raise
|
194
|
+
v = props&.dig(key.to_s)
|
195
|
+
raise PropsValidationError, "#{key} missing" unless v
|
196
|
+
raise PropsValidationError, "#{key} must be #{klass}" unless v.is_a?(klass)
|
152
197
|
end
|
153
|
-
rescue
|
154
|
-
errors.add(:
|
198
|
+
rescue PropsValidationError => e
|
199
|
+
errors.add(:props, e.message)
|
155
200
|
end
|
156
201
|
end
|
157
202
|
end
|
data/lib/flow_state/version.rb
CHANGED
@@ -4,8 +4,11 @@
|
|
4
4
|
class CreateFlowStateFlows < ActiveRecord::Migration[8.0]
|
5
5
|
def change
|
6
6
|
create_table :flow_state_flows do |t|
|
7
|
+
t.string :type, null: false
|
7
8
|
t.string :current_state, null: false
|
8
|
-
t.
|
9
|
+
t.datetime :completed_at
|
10
|
+
t.datetime :last_errored_at
|
11
|
+
t.json :props
|
9
12
|
t.timestamps
|
10
13
|
end
|
11
14
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flow_state
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Garrett
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-05-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -37,6 +37,7 @@ files:
|
|
37
37
|
- ".rubocop.yml"
|
38
38
|
- CHANGELOG.md
|
39
39
|
- LICENSE.txt
|
40
|
+
- MIGRATION_0_1_to_0_2.md
|
40
41
|
- README.md
|
41
42
|
- Rakefile
|
42
43
|
- lib/flow_state.rb
|
@@ -58,7 +59,13 @@ metadata:
|
|
58
59
|
source_code_uri: https://github.com/hyperlaunch/flow-state
|
59
60
|
changelog_uri: https://github.com/hyperlaunch/flow-state/changelog.md
|
60
61
|
rubygems_mfa_required: 'true'
|
61
|
-
post_install_message:
|
62
|
+
post_install_message: |
|
63
|
+
**FlowState 0.2 contains breaking changes.**
|
64
|
+
|
65
|
+
If you are upgrading from any 0.1.x release,
|
66
|
+
read the migration guide first:
|
67
|
+
|
68
|
+
https://github.com/hyperlaunch/flow_state/blob/main/MIGRATION_0_1_to_0_2.md
|
62
69
|
rdoc_options: []
|
63
70
|
require_paths:
|
64
71
|
- lib
|