statecraft 0.9.0 → 0.10.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: c8c55553745396ae74c5ffdac6de4dfad3df8ed075e26ef06ac627084aa87216
4
- data.tar.gz: 995e759e6a394619f991b1f6921065219a9259928f4797aa4a340f66c601e0b7
3
+ metadata.gz: 8e51b28a602d49b1cf5f71183e5c8b2080bd3e9f4e3a857147a65a7f5529373c
4
+ data.tar.gz: 2068032e1f963037d4521fbca8a02e53d5594bb5ce9902b9c809d1448ba7d9bd
5
5
  SHA512:
6
- metadata.gz: 96794cd2d550849d99ccef2d9e6d0e12d12b790e585d7b8734a04a36ba55321f98bee6db237e9199e4c8ff02875900809070bc8fd295fb22dab7f5d41e71c87b
7
- data.tar.gz: 6d5400fd71f2448099d9fc5c60ac71b663bbaed19b2c2d811e25172fa75148f970b9336b21fde31c09865f525dd428e2703314234bfb3e052c4dc1c5b2708085
6
+ metadata.gz: 4588eac402af35b053ce88660b39efb3a26694fdb9a5f3935844dedfff102f8d687f52af1c3b5da422cd7be99052bc55ef83e3f318d918746ef761bfba49a187
7
+ data.tar.gz: a995c7b056c6a2dd0beae23de1a06ced70f6ba3fbccce223375ea974f0fad506defd0bbf96426f10a3d2c95106be02c37c4075d569d1d8d342199bab8f8a6306
data/README.md CHANGED
@@ -161,6 +161,36 @@ refused — the guards would be silently skipped. The escape hatch is explicit:
161
161
  still run) and records `event: nil` in the log, so audited bypasses stay
162
162
  visible.
163
163
 
164
+ ## Which rules belong in a guard
165
+
166
+ Not every rule about a transition is a guard, and the three slots above are
167
+ not an invitation to move form validation into the machine. One question
168
+ draws the line: **would the rule still have to hold if the transition came
169
+ from a job, a console or an ETL run instead of a form?**
170
+
171
+ A rule about the record — this order is on credit, so it is not cancellable
172
+ — always answers yes: it is a fact of the domain, it must hold for every
173
+ caller, and it is checked inside the transaction where nothing can change
174
+ under it. That is a `record_guard:`, and it is the case guards were built
175
+ for.
176
+
177
+ A rule that reads the input earns its place in two situations. The first is
178
+ a rule that compares the input *against the record* — a refund amount that
179
+ must not exceed the order total. One side of that comparison can change
180
+ between a caller's check and the write, so the check belongs inside the
181
+ transaction. The second is a rule protecting the log: metadata is write-once
182
+ and the log is append-only, so a cancellation recorded without its reason
183
+ stays useless forever. The last gate before something becomes permanent is
184
+ worth having in the pipeline, whoever opened it.
185
+
186
+ Everything else is the caller's job. A rule that reads only the input,
187
+ never touches the record, and does not decide whether the log row makes
188
+ sense — a minimum length, a format, a spelling — gains nothing from the
189
+ machine but centralization, and it pays for that with a poorer contract: a
190
+ guard answers with one boolean and a symbol, while a form object answers
191
+ with several errors, attributed to fields, in the user's language. Validate
192
+ the form in the form; hand the machine input it has already accepted.
193
+
164
194
  ## Callbacks and chains
165
195
 
166
196
  `before_transition`, `after_transition` and `after_commit` accept `from:`,
@@ -444,9 +474,18 @@ and parse them in the guard.
444
474
 
445
475
  Facts of the transition moment (a price snapshot, a rules version) are
446
476
  collected by the caller: `order.pay!(metadata: { price: order.total })`.
447
- There is no metadata schema mechanism required fields are enforced by
448
- guards — and the shape evolves by convention: carry a `v:` key when you need
449
- versioned readers.
477
+ There is no metadata schema mechanism, and the shape evolves by convention:
478
+ carry a `v:` key when you need versioned readers.
479
+
480
+ A guard can require a key, but that is a narrow licence rather than the
481
+ schema mechanism in disguise: the reason to enforce a field here is that
482
+ the row is about to become permanent — metadata is write-once and the log
483
+ is append-only, so a field missing at insert time is missing for good. Note
484
+ what such a guard does *not* buy: metadata is frozen at the pipeline
485
+ entrance, before the transaction opens, so checking it inside a guard is no
486
+ more atomic than checking it in the caller. Shape and format belong to
487
+ whoever assembled the hash — see
488
+ [which rules belong in a guard](#which-rules-belong-in-a-guard).
450
489
 
451
490
  ## Initial state is not a transition
452
491
 
@@ -575,6 +614,52 @@ statesman never had: see [Stale transitions](#stale-transitions-versioning-again
575
614
  — the version column is a constant default, so adding it costs a
576
615
  metadata-only migration on PostgreSQL 11+.
577
616
 
617
+ ## Migrating from aasm
618
+
619
+ aasm already keeps the current state in a column, so this move is the
620
+ opposite shape of the statesman one: nothing is backfilled, and the
621
+ conversion adds the history aasm never had.
622
+
623
+ ```sh
624
+ bin/rails generate statecraft:from_aasm Order
625
+ ```
626
+
627
+ The generator reads the live aasm machine off the model (pass a second
628
+ argument to name one machine of a model that declares several) and writes
629
+ the machine skeleton, the log-table migration and the mounting line. More
630
+ survives the trip than from statesman: aasm has real event names, and its
631
+ state column arrives as the mounting's `column:`.
632
+
633
+ Guards come over by nature. An aasm guard judges the record and never sees
634
+ what the caller submitted, which is exactly statecraft's `record_guard:` —
635
+ so a symbol `guard:`/`if:` becomes a `record_guard:` plus a generated
636
+ one-line delegate (`def payable?(record) = record.payable?`, because a
637
+ guard symbol resolves on the machine here, not on the record), and
638
+ `unless: :locked?` becomes a negating delegate. A lambda guard carries its
639
+ own closure and cannot be moved: it arrives as a TODO with its `file:line`.
640
+
641
+ One graph shape is refused rather than guessed. aasm lets one event branch
642
+ from a single state and picks the first transition whose guard passes;
643
+ statecraft makes an event a partial function, so `from` is unique within an
644
+ event. The generator stops, names the branching events, and asks you to
645
+ split them in aasm first — the `pay` / `fail_payment` pattern. Naming the
646
+ halves is a domain decision, and a bad name would outlive the migration.
647
+
648
+ The migration creates the log table and adds an index on the existing state
649
+ column. It deliberately does **not** tighten that column: `NOT NULL`, a
650
+ default and a `CHECK` on a live table mean long locks and an explosion on
651
+ any legacy row outside the state list. The recipe for doing it later, in
652
+ the `NOT VALID` → `VALIDATE CONSTRAINT` style, sits in the migration's own
653
+ header.
654
+
655
+ Then finish by hand (the generator prints the list): drop `include AASM`
656
+ and the `aasm do ... end` block. Event calls keep their names under
657
+ `helpers: true`, but their semantics sharpen — a lost race now raises
658
+ `TransitionConflict` where aasm quietly answered `false` or, without an
659
+ explicit lock, silently overwrote the column. aasm's state predicates
660
+ (`order.paid?`) are not generated: use `in_state?(:paid)` or the `paid`
661
+ scope.
662
+
578
663
  ## PII and erasure
579
664
 
580
665
  Metadata is the only place personal data can live — `from_state`, `to_state`
@@ -0,0 +1,34 @@
1
+ Description:
2
+ Migrates a model off aasm. aasm already keeps the current state in a
3
+ column, so nothing is backfilled: the conversion adds the append-only
4
+ history aasm never had, indexes the existing column and generates a
5
+ machine skeleton from the live aasm reflection.
6
+
7
+ Copied over: the states, the initial state, the real event names (unlike
8
+ statesman, aasm has them) and the state column, which becomes the
9
+ mounting's `column:`. Symbol guards (guard:/if:/unless:) come over as
10
+ record_guard: with a generated one-line delegate — aasm calls a guard on
11
+ the record, and statecraft resolves a guard symbol on the machine.
12
+ Lambda guards carry their own closure and cannot be moved: each becomes
13
+ a TODO comment with its source location.
14
+
15
+ The second argument names an aasm machine when the model declares
16
+ several (`aasm(:shipment)`); without it the unnamed machine is read.
17
+ An event that branches from one state (aasm picks the first transition
18
+ whose guard passes) is refused: statecraft makes an event a partial
19
+ function, so split it into two named events in aasm first.
20
+
21
+ Example:
22
+ bin/rails generate statecraft:from_aasm Order
23
+
24
+ Creates (when missing) and edits:
25
+ app/state_machines/application_machine.rb
26
+ app/state_machines/order_flow.rb (skeleton: states, events, guards)
27
+ app/models/order.rb (state_machine mounting line)
28
+ db/migrate/XXX_create_order_transitions.rb
29
+
30
+ The migration creates the log table and indexes the state column; the
31
+ column is otherwise left alone, with the NOT NULL / default / CHECK
32
+ recipe in its header. After running it, follow the printed cleanup list:
33
+ drop `include AASM` and the aasm block, and replace aasm's state
34
+ predicates (`order.paid?`) with `in_state?(:paid)` or the scope.
@@ -0,0 +1,250 @@
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_aasm Order [machine_name]` — the door
9
+ # off aasm. Reads the live aasm machine through reflection (states, the
10
+ # initial state, real event names, the state column and the symbol
11
+ # guards), generates the machine skeleton, the log table migration and
12
+ # the mounting line. aasm keeps state in a column already, so nothing is
13
+ # backfilled: the conversion adds the history aasm never had.
14
+ class FromAasmGenerator < Rails::Generators::NamedBase
15
+ include ActiveRecord::Generators::Migration
16
+
17
+ source_root File.expand_path("templates", __dir__)
18
+
19
+ argument :aasm_machine_name, type: :string, required: false,
20
+ desc: "The named aasm machine (default: the unnamed one)"
21
+
22
+ def load_aasm_machine
23
+ @aasm_machine = resolve_aasm_machine
24
+ assert_graph_present
25
+ assert_no_branching_events
26
+ end
27
+
28
+ def create_application_machine
29
+ application_machine_path = "app/state_machines/application_machine.rb"
30
+ return if File.exist?(File.join(destination_root, application_machine_path))
31
+
32
+ template "application_machine.rb.tt", application_machine_path
33
+ end
34
+
35
+ def create_machine_skeleton
36
+ template "machine_from_aasm.rb.tt", "app/state_machines/#{file_path}_flow.rb"
37
+ end
38
+
39
+ def mount_model
40
+ unless File.exist?(File.join(destination_root, model_file))
41
+ say_status :skip, "#{model_file} not found — mount the machine yourself: #{mounting_line.strip}", :yellow
42
+ return
43
+ end
44
+
45
+ inject_into_class model_file, class_name, mounting_line
46
+ end
47
+
48
+ def create_log_migration
49
+ migration_template "create_log_table.rb.tt",
50
+ "#{migration_directory}/create_#{migration_slug}_transitions.rb"
51
+ end
52
+
53
+ def print_cleanup_instructions
54
+ say "\nOne migration: it creates the log table aasm never had and indexes " \
55
+ "the existing #{state_column} column.", :green
56
+ say "The column itself is left alone — NOT NULL, a default and a CHECK on a live"
57
+ say "table are locks and legacy-row explosions; the migration header carries the recipe."
58
+ say "\nAfter it runs, finish the move by hand:", :green
59
+ say " * #{class_name}: drop `include AASM` and the whole `aasm do ... end` block."
60
+ say " * Event calls keep their names with helpers: true (`order.pay!`), but a losing"
61
+ say " race now raises TransitionConflict instead of quietly answering false."
62
+ say " * aasm state predicates (`order.paid?`) are not generated: use `in_state?(:paid)`"
63
+ say " or the `#{class_name}.paid` scope."
64
+ say " * Guards that were lambdas are TODO comments in the skeleton — port them by hand."
65
+ end
66
+
67
+ private
68
+
69
+ def resolve_aasm_machine
70
+ unless model_class.respond_to?(:aasm)
71
+ raise Thor::Error, "#{class_name} does not respond to .aasm — " \
72
+ "point the generator at the model that includes AASM"
73
+ end
74
+
75
+ machine = aasm_machine_name ? model_class.aasm(aasm_machine_name.to_sym) : model_class.aasm
76
+ unless machine.respond_to?(:states) && machine.respond_to?(:events)
77
+ raise Thor::Error, "#{class_name}.aasm does not look like an aasm machine (no .states/.events)"
78
+ end
79
+
80
+ machine
81
+ end
82
+
83
+ def model_class
84
+ @model_class ||= class_name.safe_constantize ||
85
+ raise(Thor::Error, "model class #{class_name} not found — " \
86
+ "the generator reads the live aasm machine off it")
87
+ end
88
+
89
+ # A model with named machines answers the unnamed `.aasm` with the
90
+ # :default machine, which holds no states at all — a silently empty
91
+ # graph. Name the machines instead of generating a skeleton of nothing.
92
+ def assert_graph_present
93
+ return unless states.empty?
94
+
95
+ raise Thor::Error, "#{class_name}'s #{machine_label} declares no states#{named_machines_hint}"
96
+ end
97
+
98
+ def named_machines_hint
99
+ names = declared_machine_names - ["default"]
100
+ return "" if names.empty?
101
+
102
+ " — it declares named machines: #{names.join(", ")}; " \
103
+ "pass one: rails g statecraft:from_aasm #{name} #{names.first}"
104
+ end
105
+
106
+ def declared_machine_names
107
+ store = defined?(AASM::StateMachineStore) && AASM::StateMachineStore.fetch(model_class)
108
+ store.respond_to?(:machine_names) ? store.machine_names.map(&:to_s) : []
109
+ rescue StandardError
110
+ []
111
+ end
112
+
113
+ # aasm picks the first transition of an event whose guard passes, so one
114
+ # event may branch from a single state; statecraft compiles an event as a
115
+ # partial function (from is unique within an event) and refuses such a
116
+ # graph. The split is a domain decision, so the door stops here.
117
+ def assert_no_branching_events
118
+ branching = event_descriptors.filter_map do |event|
119
+ froms = event[:transitions].map { |transition| transition[:from] }
120
+ duplicated = froms.tally.select { |_from, count| count > 1 }.keys
121
+ "#{event[:name]} (from #{duplicated.join(", ")})" unless duplicated.empty?
122
+ end
123
+ return if branching.empty?
124
+
125
+ raise Thor::Error,
126
+ "these aasm events branch from one state: #{branching.join("; ")}. " \
127
+ "statecraft makes an event a partial function — within one event, from is unique. " \
128
+ "Split each into two named events (the pay / fail_payment pattern) in aasm first, " \
129
+ "then rerun this generator."
130
+ end
131
+
132
+ def event_descriptors
133
+ @event_descriptors ||= @aasm_machine.events.map do |event|
134
+ {
135
+ name: event.name,
136
+ transitions: event.transitions.map { |transition| describe_transition(event, transition) }
137
+ }
138
+ end
139
+ end
140
+
141
+ def describe_transition(event, transition)
142
+ options = transition.respond_to?(:opts) ? transition.opts : {}
143
+ { from: transition.from, to: transition.to, guard: guard_descriptor(event, transition, options) }
144
+ end
145
+
146
+ # aasm spells the record-level guard three ways and calls it on the
147
+ # record; statecraft's record_guard: is the same nature (arity 1) but
148
+ # resolves on the machine, so every symbol becomes a one-line delegate.
149
+ # A lambda carries its own closure and cannot be moved — it is reported.
150
+ def guard_descriptor(event, transition, options)
151
+ positive = options[:guard] || options[:if]
152
+ negative = options[:unless]
153
+ return symbol_guard(positive, negated: false) if positive.is_a?(Symbol)
154
+ return symbol_guard(negative, negated: true) if negative.is_a?(Symbol)
155
+
156
+ lambda_note(event, transition, positive || negative)
157
+ end
158
+
159
+ def symbol_guard(name, negated:)
160
+ { kind: :delegate, source: name, method_name: negated ? "not_#{name.to_s.delete_suffix("?")}?" : name.to_s,
161
+ negated: negated }
162
+ end
163
+
164
+ def lambda_note(event, transition, callable)
165
+ return nil if callable.nil?
166
+
167
+ location = callable.respond_to?(:source_location) ? callable.source_location&.join(":") : nil
168
+ origin = location ? " — defined at #{location}" : ""
169
+ { kind: :todo, note: "#{event.name} (#{transition.from} -> #{transition.to})#{origin}" }
170
+ end
171
+
172
+ def states
173
+ @states ||= @aasm_machine.states.map { |state| state.name.to_s }
174
+ end
175
+
176
+ def initial_state
177
+ @aasm_machine.initial_state.to_s
178
+ end
179
+
180
+ def events
181
+ event_descriptors.flat_map do |event|
182
+ event[:transitions].map do |transition|
183
+ { name: event[:name], from: transition[:from], to: transition[:to],
184
+ guard: delegate_guard(transition) }
185
+ end
186
+ end
187
+ end
188
+
189
+ def guard_delegates
190
+ events.filter_map { |event| event[:guard] }.uniq { |guard| guard[:method_name] }
191
+ end
192
+
193
+ def guard_todos
194
+ all_guards.filter_map { |guard| guard[:note] if guard[:kind] == :todo }
195
+ end
196
+
197
+ def delegate_guard(transition)
198
+ guard = transition[:guard]
199
+ guard if guard && guard[:kind] == :delegate
200
+ end
201
+
202
+ def all_guards
203
+ event_descriptors.flat_map { |event| event[:transitions].filter_map { |t| t[:guard] } }
204
+ end
205
+
206
+ def state_column
207
+ @aasm_machine.respond_to?(:attribute_name) ? @aasm_machine.attribute_name.to_s : "state"
208
+ end
209
+
210
+ def machine_label
211
+ aasm_machine_name ? "aasm machine #{aasm_machine_name}" : "unnamed aasm machine"
212
+ end
213
+
214
+ def mounting_line
215
+ column = state_column == "state" ? "" : "column: :#{state_column}, "
216
+ " state_machine #{class_name}Flow, #{column}changed_at: true, helpers: true, scopes: true\n"
217
+ end
218
+
219
+ def model_file
220
+ "app/models/#{file_path}.rb"
221
+ end
222
+
223
+ def table_name
224
+ @table_name ||= model_class.respond_to?(:table_name) ? model_class.table_name : super
225
+ end
226
+
227
+ def log_table_name
228
+ "#{table_name.singularize}_transitions"
229
+ end
230
+
231
+ def foreign_key_column
232
+ "#{file_name}_id"
233
+ end
234
+
235
+ def migration_directory
236
+ db_config = model_class.respond_to?(:connection_db_config) && model_class.connection_db_config
237
+ configured = db_config && Array(db_config.migrations_paths).first
238
+ configured || "db/migrate"
239
+ end
240
+
241
+ def migration_slug
242
+ file_path.tr("/", "_")
243
+ end
244
+
245
+ def migration_class_name
246
+ "Create#{migration_slug.camelize}Transitions"
247
+ end
248
+ end
249
+ end
250
+ 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,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ # aasm kept the state in <%= table_name %>.<%= state_column %> and kept no history at
4
+ # all, so this migration adds only what the gem needs: the append-only log and
5
+ # an index on the column the scopes will read.
6
+ #
7
+ # The column itself is deliberately left as aasm made it. To tighten it later
8
+ # without blocking writes on a live table:
9
+ # ALTER TABLE <%= table_name %> ALTER COLUMN <%= state_column %> SET DEFAULT '<%= initial_state %>';
10
+ # -- backfill NULLs, then --
11
+ # ALTER TABLE <%= table_name %> ALTER COLUMN <%= state_column %> SET NOT NULL;
12
+ # ALTER TABLE <%= table_name %> ADD CONSTRAINT <%= table_name %>_<%= state_column %>_check
13
+ # CHECK (<%= state_column %> IN (<%= states.map { |state| "'#{state}'" }.join(", ") %>)) NOT VALID;
14
+ # -- clean the data --
15
+ # ALTER TABLE <%= table_name %> VALIDATE CONSTRAINT <%= table_name %>_<%= state_column %>_check;
16
+ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
17
+ def change
18
+ create_table :<%= log_table_name.pluralize %> do |t|
19
+ t.references :<%= file_name %>,
20
+ null: false, index: false,
21
+ foreign_key: { to_table: :<%= table_name %>, on_delete: :cascade }
22
+ t.string :from_state, null: false
23
+ t.string :to_state, null: false
24
+ t.string :event
25
+ t.column :metadata, metadata_column_type, null: false, default: {}
26
+ t.datetime :created_at, null: false
27
+ t.index %i[<%= foreign_key_column %> id]
28
+ end
29
+
30
+ add_index :<%= table_name %>, :<%= state_column %> unless index_exists?(:<%= table_name %>, :<%= state_column %>)
31
+ end
32
+
33
+ private
34
+
35
+ # jsonb where the database has it, json elsewhere: the adapter is known
36
+ # only when the migration actually runs, never at generation time.
37
+ def metadata_column_type
38
+ connection.adapter_name.match?(/postg/i) ? :jsonb : :json
39
+ end
40
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Skeleton generated from <%= class_name %>'s <%= machine_label %>: the graph and
4
+ # the event names are copied, the semantics are yours to port.
5
+ #
6
+ # - aasm guards judge the record and see no input, so every symbol guard came
7
+ # over as record_guard: with a one-line delegate below. A guard that reads
8
+ # what the caller submits belongs on guard: (arity 2) instead.
9
+ # - lambda guards carry their own closure and cannot be moved; each is a TODO
10
+ # with its original location.
11
+ class <%= class_name %>Flow < ApplicationMachine
12
+ <% states.each do |state_name| -%>
13
+ state :<%= state_name %><%= state_name == initial_state ? ", initial: true" : "" %>
14
+ <% end -%>
15
+
16
+ <% events.each do |event| -%>
17
+ event :<%= event[:name] %>, from: :<%= event[:from] %>, to: :<%= event[:to] %><%= event[:guard] ? ", record_guard: :#{event[:guard][:method_name]}" : "" %>
18
+ <% end -%>
19
+ <% unless guard_todos.empty? -%>
20
+
21
+ <% guard_todos.each do |note| -%>
22
+ # TODO(guard): <%= note %>
23
+ <% end -%>
24
+ <% end -%>
25
+ <% unless guard_delegates.empty? -%>
26
+
27
+ private
28
+
29
+ # The machine keeps the registry "event -> predicate" and delegates the
30
+ # domain facts to the record, exactly where aasm called them.
31
+ <% guard_delegates.each do |guard| -%>
32
+ def <%= guard[:method_name] %>(record) = <%= guard[:negated] ? "!record.#{guard[:source]}" : "record.#{guard[:source]}" %>
33
+ <% end -%>
34
+ <% end -%>
35
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Statecraft
4
- VERSION = "0.9.0"
4
+ VERSION = "0.10.0"
5
5
  end
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.9.0
4
+ version: 0.10.0
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_aasm/USAGE
64
+ - lib/generators/statecraft/from_aasm/from_aasm_generator.rb
65
+ - lib/generators/statecraft/from_aasm/templates/application_machine.rb.tt
66
+ - lib/generators/statecraft/from_aasm/templates/create_log_table.rb.tt
67
+ - lib/generators/statecraft/from_aasm/templates/machine_from_aasm.rb.tt
63
68
  - lib/generators/statecraft/from_statesman/USAGE
64
69
  - lib/generators/statecraft/from_statesman/from_statesman_generator.rb
65
70
  - lib/generators/statecraft/from_statesman/templates/application_machine.rb.tt