statecraft 0.3.0 → 0.4.1
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 +59 -0
- data/lib/generators/statecraft/from_statesman/USAGE +30 -0
- data/lib/generators/statecraft/from_statesman/from_statesman_generator.rb +160 -0
- data/lib/generators/statecraft/from_statesman/templates/application_machine.rb.tt +8 -0
- data/lib/generators/statecraft/from_statesman/templates/convert_transitions_migration.rb.tt +153 -0
- data/lib/generators/statecraft/from_statesman/templates/machine_from_statesman.rb.tt +26 -0
- data/lib/statecraft/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f62004e4153d3351bda79f2cf1f09db95e42b83302d016909cbe7d8d081ef3ba
|
|
4
|
+
data.tar.gz: e005456aca83c931019db903ff6a860efff9111da2de5bbe35fe7021192e1f28
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8c8bd36f37d3c0392cfc4c17562f071f66e8e3252253e2237bc84283418dea222db32327e30aab4f3cd10b408e37852f6cddf3f93171a026194f1a6787f26c83
|
|
7
|
+
data.tar.gz: f1a14781c925c5bc72da7e7120fc8e1e0a4809b17f36f03f8e55b72e6a8a1163056b730804bb871d0961d1139428bd1084cffa5437f03847674e1dd49c989c9d
|
data/README.md
CHANGED
|
@@ -337,6 +337,63 @@ rolling rename recipe on the column:
|
|
|
337
337
|
after the cleanup.
|
|
338
338
|
3. Drop the old state and its edges; narrow the CHECK back.
|
|
339
339
|
|
|
340
|
+
## Migrating from statesman
|
|
341
|
+
|
|
342
|
+
The names already match: statesman's `order_transitions` table and
|
|
343
|
+
`OrderTransition` class are exactly what statecraft's log convention expects,
|
|
344
|
+
so the move is an in-place conversion of the table you already have — history
|
|
345
|
+
stays where it is, nothing is copied.
|
|
346
|
+
|
|
347
|
+
```sh
|
|
348
|
+
bin/rails generate statecraft:from_statesman Order
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The generator reads the live statesman machine through reflection (pass the
|
|
352
|
+
class as a second argument when it is not `OrderStateMachine`; point at the
|
|
353
|
+
class that declares the DSL — statesman graphs are not inherited) and writes
|
|
354
|
+
three things: the conversion migration, a machine skeleton, and the
|
|
355
|
+
`state_machine` mounting line. Two things it honestly cannot write: statesman
|
|
356
|
+
has no events, so every edge arrives as a bare `transition` for you to name,
|
|
357
|
+
and guard bodies are anonymous blocks — each becomes a TODO comment carrying
|
|
358
|
+
the original `file:line`.
|
|
359
|
+
|
|
360
|
+
The migration converts in this order:
|
|
361
|
+
|
|
362
|
+
1. The model gains its `state` column, backfilled from the **last transition
|
|
363
|
+
by `sort_key`** — deliberately not `most_recent`, which drifts out of sync
|
|
364
|
+
often enough that statesman ships a repair task for it. Rows with no
|
|
365
|
+
transitions get the initial state; a CHECK constraint pins the value set.
|
|
366
|
+
2. The transitions table gains `from_state` (a `LAG` window along the
|
|
367
|
+
`sort_key` chain, the first hop starting from the initial state) and a
|
|
368
|
+
nullable `event` — imported history reads as direct transitions, which is
|
|
369
|
+
the honest description of what statesman recorded.
|
|
370
|
+
3. A text `metadata` column becomes native json(b) in one indivisible move
|
|
371
|
+
with removing `serialize` from the model: neither library works in the
|
|
372
|
+
half-converted state, so the type change and the code change ship
|
|
373
|
+
together.
|
|
374
|
+
4. The foreign key is re-created with `ON DELETE CASCADE` (statesman's
|
|
375
|
+
default one carries no action), statesman's unique indexes go, and the
|
|
376
|
+
`[foreign_key, id]` index that serves statecraft's history reads arrives.
|
|
377
|
+
5. `sort_key`, `most_recent` and `updated_at` are dropped last — the first
|
|
378
|
+
two would break the log INSERT outright, being NOT NULL without defaults.
|
|
379
|
+
|
|
380
|
+
The migration's header names two pre-flight checks on live data — that the
|
|
381
|
+
id order agrees with the `sort_key` order (statecraft reads history by id),
|
|
382
|
+
and that a text `metadata` column holds valid JSON in every row (rows written
|
|
383
|
+
by raw SQL may not survive the cast). Run both before migrating.
|
|
384
|
+
|
|
385
|
+
After the migration, finish by hand (the generator prints this list): drop
|
|
386
|
+
`Statesman::Adapters::ActiveRecordTransition` and the
|
|
387
|
+
`after_destroy :update_most_recent` callback from the transition model — they
|
|
388
|
+
read dropped columns — plus `ActiveRecordQueries` from the model and the
|
|
389
|
+
`Statesman.configure` initializer once no machine is left; then name your
|
|
390
|
+
events in the skeleton. One guarantee moves rather than disappears: the
|
|
391
|
+
race safety statesman derived from its unique `(parent, sort_key)` index is
|
|
392
|
+
statecraft's CAS on the state column.
|
|
393
|
+
|
|
394
|
+
Outside Rails, the same steps work by hand — pair the migration order above
|
|
395
|
+
with the reference schema in [Outside Rails](#outside-rails).
|
|
396
|
+
|
|
340
397
|
## PII and erasure
|
|
341
398
|
|
|
342
399
|
Metadata is the only place personal data can live — `from_state`, `to_state`
|
|
@@ -649,6 +706,8 @@ AR_VERSION=7.2 RUBY_VERSION=3.3 docker compose run --rm test-postgres
|
|
|
649
706
|
## Links
|
|
650
707
|
|
|
651
708
|
- Landing page: <https://supostat.github.io/statecraft/>
|
|
709
|
+
- Benchmarks vs statesman/aasm: <https://supostat.github.io/statecraft/compare.html>
|
|
710
|
+
(reproducible scripts in [`benchmark/`](benchmark/))
|
|
652
711
|
- RubyGems: <https://rubygems.org/gems/statecraft>
|
|
653
712
|
- Issues: <https://github.com/supostat/statecraft/issues>
|
|
654
713
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Migrates a model off statesman: converts its transitions table into the
|
|
3
|
+
statecraft log IN PLACE (the names already match the gem's convention),
|
|
4
|
+
gives the model its state column with a backfill from the last
|
|
5
|
+
transition, generates a machine skeleton from the statesman class's
|
|
6
|
+
reflection, and mounts the machine.
|
|
7
|
+
|
|
8
|
+
The statesman machine is read through reflection: states, the initial
|
|
9
|
+
state and the transition graph are copied; events and guard bodies
|
|
10
|
+
cannot be — statesman has no events (name them yourself in the skeleton)
|
|
11
|
+
and guard bodies are anonymous blocks (each becomes a TODO comment with
|
|
12
|
+
its original source location).
|
|
13
|
+
|
|
14
|
+
The second argument names the statesman machine class; without it the
|
|
15
|
+
<Model>StateMachine convention is assumed. Statesman graphs are not
|
|
16
|
+
inherited — point at the class that declares the DSL.
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
bin/rails generate statecraft:from_statesman Order
|
|
20
|
+
|
|
21
|
+
Creates (when missing) and edits:
|
|
22
|
+
app/state_machines/application_machine.rb
|
|
23
|
+
app/state_machines/order_flow.rb (skeleton: graph + TODOs)
|
|
24
|
+
app/models/order.rb (state_machine mounting line)
|
|
25
|
+
db/migrate/XXX_convert_order_transitions_to_statecraft.rb
|
|
26
|
+
|
|
27
|
+
The migration header lists two pre-flight checks on live data (id order
|
|
28
|
+
vs sort_key order; text metadata holding valid JSON) — read it before
|
|
29
|
+
running. After migrating, follow the printed cleanup list: drop the
|
|
30
|
+
statesman includes, the configure initializer, and name your events.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module Statecraft
|
|
7
|
+
module Generators
|
|
8
|
+
# `rails generate statecraft:from_statesman Order [OrderStateMachine]` —
|
|
9
|
+
# the migration path off statesman. Reads the live statesman machine
|
|
10
|
+
# through reflection (states, initial state, transition graph, guard
|
|
11
|
+
# locations), generates the in-place conversion migration that turns the
|
|
12
|
+
# statesman transitions table into this gem's log, a machine skeleton
|
|
13
|
+
# whose events and guards are left for the human, and mounts the machine
|
|
14
|
+
# into the model. The statesman class itself is never loaded by name
|
|
15
|
+
# magic beyond the optional second argument's default.
|
|
16
|
+
class FromStatesmanGenerator < Rails::Generators::NamedBase
|
|
17
|
+
include ActiveRecord::Generators::Migration
|
|
18
|
+
|
|
19
|
+
source_root File.expand_path("templates", __dir__)
|
|
20
|
+
|
|
21
|
+
argument :statesman_machine_name, type: :string, required: false,
|
|
22
|
+
desc: "The statesman machine class (default: <Model>StateMachine)"
|
|
23
|
+
|
|
24
|
+
def load_statesman_machine
|
|
25
|
+
@statesman_machine = resolve_statesman_machine
|
|
26
|
+
@initial_state = @statesman_machine.initial_state
|
|
27
|
+
return unless @initial_state.nil?
|
|
28
|
+
|
|
29
|
+
raise Thor::Error, "#{statesman_class_name} declares no initial state; " \
|
|
30
|
+
"statecraft requires exactly one — declare `state ..., initial: true` first"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def create_application_machine
|
|
34
|
+
application_machine_path = "app/state_machines/application_machine.rb"
|
|
35
|
+
return if File.exist?(File.join(destination_root, application_machine_path))
|
|
36
|
+
|
|
37
|
+
template "application_machine.rb.tt", application_machine_path
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def create_machine_skeleton
|
|
41
|
+
template "machine_from_statesman.rb.tt", "app/state_machines/#{file_path}_flow.rb"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def mount_model
|
|
45
|
+
unless File.exist?(File.join(destination_root, model_file))
|
|
46
|
+
say_status :skip, "#{model_file} not found — mount the machine yourself: #{mounting_line.strip}", :yellow
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
inject_into_class model_file, class_name, mounting_line
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def create_conversion_migration
|
|
54
|
+
migration_template "convert_transitions_migration.rb.tt",
|
|
55
|
+
"#{migration_directory}/convert_#{migration_slug}_transitions_to_statecraft.rb"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def print_cleanup_instructions
|
|
59
|
+
say "\nAfter running the migration, finish the move by hand:", :green
|
|
60
|
+
say " * #{log_class_name}: drop `include Statesman::Adapters::ActiveRecordTransition` and"
|
|
61
|
+
say " the `after_destroy :update_most_recent` callback (they read dropped columns);"
|
|
62
|
+
say " consider adding `def readonly? = persisted?` — the pipeline inserts around it."
|
|
63
|
+
say " * #{class_name}: drop `include Statesman::Adapters::ActiveRecordQueries` and the"
|
|
64
|
+
say " state_machine/transition helper methods that wrapped statesman."
|
|
65
|
+
say " * Delete the `Statesman.configure` initializer once no machine is left."
|
|
66
|
+
say " * Name your events in app/state_machines/#{file_path}_flow.rb — statesman had none."
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def resolve_statesman_machine
|
|
72
|
+
machine_class = statesman_class_name.safe_constantize
|
|
73
|
+
if machine_class.nil?
|
|
74
|
+
raise Thor::Error, "statesman machine class #{statesman_class_name} not found — " \
|
|
75
|
+
"pass it explicitly: rails g statecraft:from_statesman #{name} YourMachineClass"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
unless machine_class.respond_to?(:states) && machine_class.respond_to?(:successors)
|
|
79
|
+
raise Thor::Error, "#{statesman_class_name} does not look like a statesman machine " \
|
|
80
|
+
"(no .states/.successors); note that statesman graphs are not inherited — " \
|
|
81
|
+
"point at the class that declares the DSL"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
machine_class
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def statesman_class_name
|
|
88
|
+
statesman_machine_name || "#{class_name}StateMachine"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
attr_reader :initial_state
|
|
92
|
+
|
|
93
|
+
def states
|
|
94
|
+
@statesman_machine.states.map(&:to_s)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# statesman accumulates successors without deduplication and statecraft
|
|
98
|
+
# refuses a duplicate edge at compile time, so uniq is load-bearing.
|
|
99
|
+
def edges
|
|
100
|
+
@statesman_machine.successors.flat_map do |from, destinations|
|
|
101
|
+
Array(destinations).map { |to| [from.to_s, to.to_s] }
|
|
102
|
+
end.uniq
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def guard_notes
|
|
106
|
+
return [] unless @statesman_machine.respond_to?(:callbacks)
|
|
107
|
+
|
|
108
|
+
Array(@statesman_machine.callbacks[:guards]).map do |guard|
|
|
109
|
+
origin = guard.callback.respond_to?(:source_location) ? guard.callback.source_location&.join(":") : nil
|
|
110
|
+
from_label = guard.from || "any"
|
|
111
|
+
to_label = Array(guard.to).empty? ? "any" : Array(guard.to).join("/")
|
|
112
|
+
note = "#{from_label} -> #{to_label}"
|
|
113
|
+
origin ? "#{note} (defined at #{origin})" : note
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def mounting_line
|
|
118
|
+
" state_machine #{class_name}Flow, changed_at: true, helpers: true, scopes: true\n"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def model_file
|
|
122
|
+
"app/models/#{file_path}.rb"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def model_class
|
|
126
|
+
class_name.safe_constantize
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def table_name
|
|
130
|
+
@table_name ||= model_class.respond_to?(:table_name) ? model_class.table_name : super
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def log_table_name
|
|
134
|
+
"#{table_name.singularize}_transitions"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def log_class_name
|
|
138
|
+
"#{class_name}Transition"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def foreign_key_column
|
|
142
|
+
"#{file_name}_id"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def migration_directory
|
|
146
|
+
db_config = model_class.respond_to?(:connection_db_config) && model_class.connection_db_config
|
|
147
|
+
configured = db_config && Array(db_config.migrations_paths).first
|
|
148
|
+
configured || "db/migrate"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def migration_slug
|
|
152
|
+
file_path.tr("/", "_")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def migration_class_name
|
|
156
|
+
"Convert#{migration_slug.camelize}TransitionsToStatecraft"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Shared home for private guard helpers used by several machines. Keep it
|
|
4
|
+
# free of DSL declarations (states, transitions, events): machine definitions
|
|
5
|
+
# are NOT inherited — each machine declares its own graph.
|
|
6
|
+
class ApplicationMachine
|
|
7
|
+
include Statecraft::Machine
|
|
8
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Converts the statesman transitions table <%= log_table_name %> into the
|
|
4
|
+
# statecraft log IN PLACE and gives <%= table_name %> its state column.
|
|
5
|
+
#
|
|
6
|
+
# BEFORE RUNNING — two checks on live data, both cheap:
|
|
7
|
+
#
|
|
8
|
+
# 1. statecraft reads history ordered by id, statesman ordered by sort_key.
|
|
9
|
+
# They almost always agree; prove it for your data (must return 0 rows):
|
|
10
|
+
#
|
|
11
|
+
# SELECT t.<%= foreign_key_column %> FROM <%= log_table_name %> t
|
|
12
|
+
# JOIN <%= log_table_name %> later ON later.<%= foreign_key_column %> = t.<%= foreign_key_column %>
|
|
13
|
+
# AND later.sort_key > t.sort_key AND later.id < t.id LIMIT 1;
|
|
14
|
+
#
|
|
15
|
+
# 2. If your metadata column is text, every row must hold valid JSON (rows
|
|
16
|
+
# written by raw SQL may not) — the jsonb cast below dies on the first
|
|
17
|
+
# invalid one.
|
|
18
|
+
class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
19
|
+
def up
|
|
20
|
+
convert_parent_table
|
|
21
|
+
convert_transitions_table
|
|
22
|
+
convert_metadata_column
|
|
23
|
+
rebuild_indexes_and_foreign_key
|
|
24
|
+
drop_statesman_columns
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def down
|
|
28
|
+
raise ActiveRecord::IrreversibleMigration
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def convert_parent_table
|
|
34
|
+
add_column :<%= table_name %>, :state, :string
|
|
35
|
+
add_column :<%= table_name %>, :state_changed_at, :datetime
|
|
36
|
+
|
|
37
|
+
# The current state is the to_state of the LAST transition by sort_key —
|
|
38
|
+
# deliberately not most_recent, which statesman itself ships a repair
|
|
39
|
+
# rake task for; rows without transitions are in the initial state.
|
|
40
|
+
execute <<~SQL
|
|
41
|
+
UPDATE <%= table_name %> SET state = COALESCE(
|
|
42
|
+
(SELECT t.to_state FROM <%= log_table_name %> t
|
|
43
|
+
WHERE t.<%= foreign_key_column %> = <%= table_name %>.id
|
|
44
|
+
ORDER BY t.sort_key DESC LIMIT 1),
|
|
45
|
+
'<%= initial_state %>'
|
|
46
|
+
)
|
|
47
|
+
SQL
|
|
48
|
+
|
|
49
|
+
# The mounting says changed_at: true, so the column must exist — and the
|
|
50
|
+
# conversion knows the honest value: the moment of the last recorded
|
|
51
|
+
# transition. Rows that never transitioned stay NULL, exactly like a
|
|
52
|
+
# freshly created record.
|
|
53
|
+
execute <<~SQL
|
|
54
|
+
UPDATE <%= table_name %> SET state_changed_at =
|
|
55
|
+
(SELECT MAX(t.created_at) FROM <%= log_table_name %> t
|
|
56
|
+
WHERE t.<%= foreign_key_column %> = <%= table_name %>.id)
|
|
57
|
+
SQL
|
|
58
|
+
|
|
59
|
+
change_column_default :<%= table_name %>, :state, "<%= initial_state %>"
|
|
60
|
+
change_column_null :<%= table_name %>, :state, false
|
|
61
|
+
|
|
62
|
+
return unless connection.supports_check_constraints?
|
|
63
|
+
|
|
64
|
+
# add new states here when the machine grows
|
|
65
|
+
if postgresql?
|
|
66
|
+
add_check_constraint :<%= table_name %>, state_check_expression,
|
|
67
|
+
name: "<%= table_name %>_state_check", validate: false
|
|
68
|
+
validate_check_constraint :<%= table_name %>, name: "<%= table_name %>_state_check"
|
|
69
|
+
else
|
|
70
|
+
add_check_constraint :<%= table_name %>, state_check_expression,
|
|
71
|
+
name: "<%= table_name %>_state_check"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def convert_transitions_table
|
|
76
|
+
add_column :<%= log_table_name %>, :from_state, :string
|
|
77
|
+
add_column :<%= log_table_name %>, :event, :string
|
|
78
|
+
|
|
79
|
+
# from_state is the previous to_state along the sort_key chain; the
|
|
80
|
+
# chain's first hop starts from the initial state. Runs while sort_key
|
|
81
|
+
# is still alive — order matters.
|
|
82
|
+
execute <<~SQL
|
|
83
|
+
UPDATE <%= log_table_name %> SET from_state = COALESCE(prev.prev_state, '<%= initial_state %>')
|
|
84
|
+
FROM (SELECT id, LAG(to_state) OVER (
|
|
85
|
+
PARTITION BY <%= foreign_key_column %> ORDER BY sort_key
|
|
86
|
+
) AS prev_state
|
|
87
|
+
FROM <%= log_table_name %>) prev
|
|
88
|
+
WHERE prev.id = <%= log_table_name %>.id
|
|
89
|
+
SQL
|
|
90
|
+
|
|
91
|
+
change_column_null :<%= log_table_name %>, :from_state, false
|
|
92
|
+
# event stays NULL for the imported history: statesman had no events, and
|
|
93
|
+
# statecraft reads NULL as a direct transition.
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Statesman's default schema serializes metadata into a text column; the
|
|
97
|
+
# statecraft pipeline wants native json(b). Converting the type and
|
|
98
|
+
# removing `serialize :metadata` from the transition model are ONE move —
|
|
99
|
+
# neither library works in the half-converted state.
|
|
100
|
+
def convert_metadata_column
|
|
101
|
+
metadata_type = connection.columns(:<%= log_table_name %>).find { |column| column.name == "metadata" }&.sql_type.to_s
|
|
102
|
+
return unless metadata_type.match?(/text|char/i)
|
|
103
|
+
|
|
104
|
+
if postgresql?
|
|
105
|
+
change_column_default :<%= log_table_name %>, :metadata, nil
|
|
106
|
+
execute "ALTER TABLE <%= log_table_name %> ALTER COLUMN metadata TYPE jsonb USING metadata::jsonb"
|
|
107
|
+
execute "UPDATE <%= log_table_name %> SET metadata = '{}'::jsonb WHERE metadata IS NULL"
|
|
108
|
+
change_column_default :<%= log_table_name %>, :metadata, {}
|
|
109
|
+
change_column_null :<%= log_table_name %>, :metadata, false
|
|
110
|
+
else
|
|
111
|
+
# SQLite types dynamically and serialize already stored JSON text —
|
|
112
|
+
# the change is declarative (a table rebuild under the hood).
|
|
113
|
+
change_column :<%= log_table_name %>, :metadata, :json, null: false, default: {}
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def rebuild_indexes_and_foreign_key
|
|
118
|
+
# If your foreign key column is NOT named <%= foreign_key_column %>
|
|
119
|
+
# (check the has_many on your model), rename it first — statecraft
|
|
120
|
+
# derives the name from the model class and offers no override:
|
|
121
|
+
# rename_column :<%= log_table_name %>, :your_column, :<%= foreign_key_column %>
|
|
122
|
+
|
|
123
|
+
remove_index :<%= log_table_name %>, column: %i[<%= foreign_key_column %> sort_key], if_exists: true
|
|
124
|
+
remove_index :<%= log_table_name %>, column: %i[<%= foreign_key_column %> most_recent], if_exists: true
|
|
125
|
+
add_index :<%= log_table_name %>, %i[<%= foreign_key_column %> id]
|
|
126
|
+
|
|
127
|
+
# An audit without its subject is not an audit: the statecraft contract
|
|
128
|
+
# is ON DELETE CASCADE (statesman's default foreign key, when present,
|
|
129
|
+
# carries no action).
|
|
130
|
+
if foreign_key_exists?(:<%= log_table_name %>, :<%= table_name %>)
|
|
131
|
+
remove_foreign_key :<%= log_table_name %>, :<%= table_name %>
|
|
132
|
+
end
|
|
133
|
+
add_foreign_key :<%= log_table_name %>, :<%= table_name %>,
|
|
134
|
+
column: :<%= foreign_key_column %>, on_delete: :cascade
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# sort_key and most_recent are NOT NULL without defaults — the first
|
|
138
|
+
# statecraft insert would die on them; updated_at is merely absent from
|
|
139
|
+
# the statecraft schema.
|
|
140
|
+
def drop_statesman_columns
|
|
141
|
+
remove_column :<%= log_table_name %>, :sort_key
|
|
142
|
+
remove_column :<%= log_table_name %>, :most_recent if column_exists?(:<%= log_table_name %>, :most_recent)
|
|
143
|
+
remove_column :<%= log_table_name %>, :updated_at if column_exists?(:<%= log_table_name %>, :updated_at)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def state_check_expression
|
|
147
|
+
"state IN ('<%= initial_state %>'<% (states - [initial_state]).each do |state_name| %>, '<%= state_name %>'<% end %>)"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def postgresql?
|
|
151
|
+
connection.adapter_name.match?(/postg/i)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Skeleton generated from <%= statesman_class_name %>: the graph is copied,
|
|
4
|
+
# the semantics are yours to port.
|
|
5
|
+
#
|
|
6
|
+
# - statesman has no events, so every edge below is a bare transition. Where
|
|
7
|
+
# an edge is really a command in your domain, name it:
|
|
8
|
+
# event :pay, from: :pending, to: :paid
|
|
9
|
+
# - statesman guard bodies cannot be extracted; each is listed as a TODO with
|
|
10
|
+
# its original location. Port a guard that judges the record alone as
|
|
11
|
+
# record_guard: (arity 1), one that reads the input as guard:.
|
|
12
|
+
class <%= class_name %>Flow < ApplicationMachine
|
|
13
|
+
<% states.each do |state_name| -%>
|
|
14
|
+
state :<%= state_name %><%= state_name == initial_state ? ", initial: true" : "" %>
|
|
15
|
+
<% end -%>
|
|
16
|
+
|
|
17
|
+
<% edges.each do |from, to| -%>
|
|
18
|
+
transition from: :<%= from %>, to: :<%= to %>
|
|
19
|
+
<% end -%>
|
|
20
|
+
<% unless guard_notes.empty? -%>
|
|
21
|
+
|
|
22
|
+
<% guard_notes.each do |note| -%>
|
|
23
|
+
# TODO(guard): <%= note %>
|
|
24
|
+
<% end -%>
|
|
25
|
+
<% end -%>
|
|
26
|
+
end
|
data/lib/statecraft/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: statecraft
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Igor Pugachev
|
|
@@ -60,6 +60,11 @@ extra_rdoc_files: []
|
|
|
60
60
|
files:
|
|
61
61
|
- LICENSE.txt
|
|
62
62
|
- README.md
|
|
63
|
+
- lib/generators/statecraft/from_statesman/USAGE
|
|
64
|
+
- lib/generators/statecraft/from_statesman/from_statesman_generator.rb
|
|
65
|
+
- lib/generators/statecraft/from_statesman/templates/application_machine.rb.tt
|
|
66
|
+
- lib/generators/statecraft/from_statesman/templates/convert_transitions_migration.rb.tt
|
|
67
|
+
- lib/generators/statecraft/from_statesman/templates/machine_from_statesman.rb.tt
|
|
63
68
|
- lib/generators/statecraft/machine/USAGE
|
|
64
69
|
- lib/generators/statecraft/machine/machine_generator.rb
|
|
65
70
|
- lib/generators/statecraft/machine/templates/add_migration.rb.tt
|