statesmin 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 91c3034b9d8315bcdd2dbe906b00982dae1db372
4
+ data.tar.gz: 4c20e9f7573b7f97022995dd0826b16373895036
5
+ SHA512:
6
+ metadata.gz: 25f7b623da7287071cbf5da621458723ded93c17be418573d8916b68916c0c6d9e82fe0e8b5f837d1b6c65c66096e0dad2886385c06f7201c60dc689f94b3d71
7
+ data.tar.gz: 007baf17484bc3479115a40fe98bcf7a7811a0f6eefce3081a89fe93f69f134c109b555b9833d23900fcd80a82f387fd0b0aa22d1e325206df63bf406a7b6d57
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .rspec
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
@@ -0,0 +1,48 @@
1
+ # For all options see https://github.com/bbatsov/rubocop/tree/master/config
2
+
3
+ AllCops:
4
+ Include:
5
+ - Rakefile
6
+ - statesman.gemfile
7
+ - lib/tasks/*.rake
8
+ Exclude:
9
+ - vendor/**/*
10
+ - .*/**
11
+ - spec/fixtures/**/*
12
+
13
+ StringLiterals:
14
+ Enabled: false
15
+
16
+ Documentation:
17
+ Enabled: false
18
+
19
+ SignalException:
20
+ EnforcedStyle: only_raise
21
+
22
+ # Avoid methods longer than 15 lines of code
23
+ MethodLength:
24
+ CountComments: false
25
+ Max: 15
26
+
27
+ AbcSize:
28
+ Max: 25
29
+
30
+ # Don't require utf-8 encoding comment
31
+ Encoding:
32
+ Enabled: false
33
+
34
+ LineLength:
35
+ Max: 80
36
+
37
+ GuardClause:
38
+ Enabled: false
39
+
40
+ SingleSpaceBeforeFirstArg:
41
+ Enabled: false
42
+
43
+ DotPosition:
44
+ EnforcedStyle: trailing
45
+
46
+ # Allow class and message or instance raises
47
+ Style/RaiseArgs:
48
+ Enabled: false
@@ -0,0 +1,15 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 2.3.0
5
+ - 2.2.4
6
+ - 2.1
7
+ - 2.0.0
8
+
9
+ sudo: false
10
+
11
+ script:
12
+ - bundle exec rubocop
13
+ - bundle exec rake
14
+
15
+ cache: bundler
@@ -0,0 +1,28 @@
1
+ Thanks for taking an interest in contributing to Statesmin, here are a few
2
+ ways you can help make this project better!
3
+
4
+ # Contribute.md
5
+
6
+ ## Team members
7
+
8
+ - [Andy Appleton](https://twitter.com/appltn)
9
+ - [Harry Marr](https://twitter.com/harrymarr)
10
+
11
+ ## Contributing
12
+
13
+ - Generally we welcome new features but please first open an issue where we
14
+ can discuss whether it fits with our vision for the project.
15
+ - Any new feature or bug fix needs an accompanying test case.
16
+ - No need to add to the changelog, we will take care of updating it as we make
17
+ releases.
18
+
19
+ ## Style
20
+
21
+ We use [Rubocop](https://github.com/bbatsov/rubocop) to help maintain a
22
+ consistent code style across the project. Please check that your pull
23
+ request passes by running `rubocop`.
24
+
25
+ ## Documentation
26
+
27
+ Please add a section to the readme for any new feature additions or behaviour
28
+ changes.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,14 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec, all_on_start: true, cmd: 'bundle exec rspec --color' do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
9
+
10
+ guard :rubocop, all_on_start: true, cli: ['--format', 'clang'] do
11
+ watch(%r{.+\.rb$})
12
+ watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) }
13
+ watch(%r{(?:.+/)?\rubocop-todo\.yml$}) { |m| File.dirname(m[0]) }
14
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Harry Marr
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,556 @@
1
+ # Statesmin
2
+
3
+ [![Build Status](https://travis-ci.org/mkcode/statesmin.svg?branch=master)](https://travis-ci.org/mkcode/statesmin)
4
+
5
+ Statesmin is a fork of [stateman](https://github.com/gocardless/statesman) that
6
+ uses a machete to rip out all of the database related code leaving you with a
7
+ simple, robust, and well tested DSL for defining state machines in your
8
+ application.
9
+
10
+ ### When to use statesmin over statesman:
11
+
12
+ * You wish to manage an object's current state yourself, including not
13
+ persisting it at all.
14
+ * You have custom requirements for your transition log entries.
15
+ * You need multiple (and very different) transition processes.
16
+ * You enjoy and habitually write service objects with small scopes.
17
+ * You will be frequently updating the state of an object and you can expect the
18
+ transitions log to contain a lot of entries.
19
+
20
+ If any of the above apply to your application, then consider using statesmin. In
21
+ addition to defining your state machines, statesmin also requires you to:
22
+
23
+ * Persist the current state of the object(s) yourself.
24
+ * Instantiate a state machine with the object's current state yourself.
25
+ * Maintain an transition / audit log yourself (if required)
26
+ * Define a custom transition process yourself.
27
+
28
+ All in all, statesmin takes considerably more work to get setup and running than
29
+ statesman, so statesman is recommended if you need to get a state machine setup
30
+ and running without any special requirements or concerns.
31
+
32
+ ### Working with Statesmin::Machine
33
+
34
+ Defining a state machine uses the same DSL as statesman. See
35
+ [tldr-usage](https://github.com/mkcode/statesmin#tldr-usage) for a more complete
36
+ example.
37
+
38
+ ```ruby
39
+ class OrderStateMachine
40
+ include Statesmin::Machine
41
+
42
+ state :pending, initial: true
43
+ state :checking_out
44
+ state :purchased
45
+ state :cancelled
46
+
47
+ transition from: :pending, to: [:checking_out, :cancelled]
48
+ transition from: :checking_out, to: [:purchased, :cancelled]
49
+
50
+ guard_transition(to: :checking_out) do |order|
51
+ order.products_in_stock?
52
+ end
53
+
54
+ before_transition(from: :checking_out, to: :cancelled) do |order, transition|
55
+ order.reallocate_stock
56
+ end
57
+
58
+ after_transition(to: :purchased) do |order, transition|
59
+ MailerService.order_confirmation(order).deliver
60
+ end
61
+ end
62
+ ```
63
+
64
+ ### Instantiating a Statesmin::Machine
65
+
66
+ The `Statesman::Machine` instance initializer now takes a `state` option which
67
+ sets the initial state of the state machine. If the `state` option is omitted,
68
+ the `initial: true` state from the Machine definition is used. Passing an
69
+ invalid state will yield a `Statesmin::InvalidStateError`.
70
+
71
+ ```ruby
72
+ # A valid state is set as the current_state
73
+ state_machine = OrderStateMachine.new(Order.first, state: :cancelled)
74
+ state_machine.current_state # => "cancelled"
75
+
76
+ # Invalid states raise an InvaliedStateError
77
+ state_machine = OrderStateMachine.new(Order.first, state: :whoops)
78
+ # => raise Statesmin::InvalidStateError
79
+
80
+ # No state option sets the state to the initial state
81
+ state_machine = OrderStateMachine.new(Order.first)
82
+ state_machine.current_state # => "pending"
83
+ ```
84
+
85
+ ### Statesmin::Machine instance methods
86
+
87
+ All instance methods from statesman are available on statesmin with the
88
+ exception of `#history` and `#last_transition`.
89
+
90
+ ```ruby
91
+ state_machine = OrderStateMachine.new(Order.first)
92
+ state_machine.current_state # => "pending"
93
+ state_machine.in_state?(:failed, :cancelled) # => true/false
94
+ state_machine.allowed_transitions # => ["checking_out", "cancelled"]
95
+ state_machine.can_transition_to?(:cancelled) # => true/false
96
+ ```
97
+
98
+ The `#transition_to` and `#transition_to!` methods are updated. They now simply
99
+ update the state machines internal current state to the new state when it is
100
+ valid. `transition_to!` raises a `Statesmin::TransitionFailedError` when an
101
+ invalid state is given. `transition_to` returns false.
102
+
103
+ ```ruby
104
+ state_machine = OrderStateMachine.new(Order.first, state: :pending)
105
+ state_machine.current_state # => "pending"
106
+
107
+ state_machine.transition_to!(:invalid_state)
108
+ # => raise Statesmin::TransitionFailedError
109
+
110
+ state_machine.transition_to(:invalid_state)
111
+ # => false
112
+ state_machine.current_state # => "pending"
113
+
114
+ state_machine.transition_to!(:checking_out) # => true
115
+ state_machine.current_state # => "checking_out"
116
+ ```
117
+
118
+ ### Statesmin::Machine #transition_to! and #transition_to
119
+
120
+ The `#transition_to` and `#transition_to!` methods now both take a block
121
+ argument as well. If a block is given, any error raised in the block body will
122
+ halt the transition and not update the current state. `transition_to!` will
123
+ always raise the error from the block body, while `transition_to` will return
124
+ false if a `Statesmin::TransitionFailedError` is raised. `transition_to` will
125
+ still raise all other errors.
126
+
127
+ `#transition_to` and `#transition_to!` will both return the value returned from
128
+ the block when they are called without errors. The state machine's current state
129
+ is updated to the new state immediately after the block has executed.
130
+
131
+ Finally, `#transition_to` and `#transition_to!` will only execute the given
132
+ block if the state argument is a valid transition. Invalid state arguments will
133
+ behave the same way as they do without blocks, either returning false or raising
134
+ a `Statesmin::TransitionFailedError` respectively.
135
+
136
+ ```ruby
137
+ state_machine = OrderStateMachine.new(Order.first, state: :pending)
138
+ state_machine.current_state # => "pending"
139
+
140
+ state_machine.transition_to! :invalid_state do
141
+ puts 'never evaluated due to the :invalid_state argument'
142
+ end
143
+ # => raise Statesmin::TransitionFailedError
144
+
145
+ state_machine.transition_to :checking_out do
146
+ raise Statesmin::TransitionFailedError
147
+ end
148
+ # => false
149
+
150
+ state_machine.transition_to :checking_out do
151
+ raise Order::InvalidAddress
152
+ end
153
+ # => raise Order::InvalidAddress
154
+ state_machine.current_state # => "pending"
155
+
156
+ state_machine.transition_to :checking_out do
157
+ OrderLogEntry.create!(order_data)
158
+ end
159
+ # => <#OrderLogEntry>
160
+ state_machine.current_state # => "checking_out
161
+ ```
162
+
163
+ The transition block is the basis of how Statesmin allows for custom transition
164
+ behavior and distinguishes itself from Statesman. For small application or
165
+ transition requirements, the transition block may be sufficient but in most
166
+ cases defining a Transition class is recommended.
167
+
168
+ ### Defining a Transition class
169
+
170
+ You are free to set up a state machine and corresponding transition behavior
171
+ however you like. The `TransitionHelper` module is included to help provide
172
+ structure and reduce boilerplate code.
173
+
174
+ Create a new class which includes the `Statesmin::TransitionHelper` module. This
175
+ module does the following for you:
176
+
177
+ * Sets up a good outline for a Transaction (service) class
178
+ * Delegates reader methods to an underlying state machine instance
179
+ * Intercepts transition methods so they may be extend with specific behavior
180
+
181
+ `Statesmin::TransitionHelper` requires you to define two methods in your
182
+ transition class:
183
+
184
+ * `state_machine` - This method returns the instance of the
185
+ `Statesmin::Machine` class to use in the class. The reader methods delegate
186
+ to this state machine instance. You will most likely also need it in other
187
+ methods.
188
+
189
+ * `transition` - This method defines the custom portion of the transition logic
190
+ for this application and object. Usually, you will trigger state persistence,
191
+ Transition logging, and callback execution from this method. Multiple
192
+ database updates are always recommended to be wrapped in a transaction.
193
+
194
+ #### Example
195
+
196
+ The following example does the following during a transition:
197
+
198
+ * Builds and saves an OrderLog record to the OrderLog table
199
+ * Persists the current state of the order in the Order table.
200
+ * Executes any before, after, and after_commit callbacks for the specific
201
+ transition
202
+ * Commits all of these database updates atomically (everything or nothing)
203
+ * Returns the newly created order log record.
204
+
205
+ ```ruby
206
+ class OrderTransitionService
207
+ include Statesmin::TransitionHelper
208
+
209
+ def initialize(order)
210
+ @order = order
211
+ end
212
+
213
+ private
214
+
215
+ def transition(next_state, data = {})
216
+ order_log = build_order_log_entry(next_state, data)
217
+
218
+ ::ActiveRecord::Base.transaction do
219
+ state_machine.execute(:before, current_state, next_state, data)
220
+ @order.update!(state: next_state)
221
+ order_log.save!
222
+ state_machine.execute(:after, current_state, next_state, data)
223
+ end
224
+ state_machine.execute(:after_commit, current_state, next_state, data)
225
+
226
+ order_log
227
+ end
228
+
229
+ def state_machine
230
+ @state_machine ||= OrderStateMachine.new(@order, state: @order.state)
231
+ end
232
+
233
+ def build_order_log_entry(next_state, data)
234
+ log_attributes = { from: current_state, to: next_state, data: data }
235
+ @order.order_logs.build(log_attributes)
236
+ end
237
+ end
238
+ ```
239
+
240
+ An instance of OrderTransitionService now has the same methods as
241
+ `Statesmin::Machine`.
242
+
243
+ ```ruby
244
+ order_transition = OrderTransitionService.new(Order.first)
245
+
246
+ # reader methods are delegated to `state_machine`
247
+ order_transition.current_state # => "pending"
248
+ order_transition.in_state?(:failed, :cancelled) # => true/false
249
+ order_transition.allowed_transitions # => ["checking_out", "cancelled"]
250
+ order_transition.can_transition_to?(:cancelled) # => true/false
251
+
252
+ # `transition_to` and `transition_to!` also execute the transition method
253
+ order_transition.transition_to(:invalid_state)
254
+ # => false
255
+ order_transition.current_state # => "pending"
256
+
257
+ order_transition.transition_to!(:checking_out)
258
+ # => <#OrderLogEntry>
259
+ order_transition.current_state # => "checking_out"
260
+ ```
261
+
262
+ ### Flexibility
263
+
264
+ The above example defines behavior similar to Statesman. Some examples of what
265
+ else can be done with an open Transition class.
266
+
267
+ * Have multiple state machines for the same object by adding a condition in the
268
+ `states_machine` method.
269
+ * Have multiple types a transitions for the same object by defining multiple
270
+ Transition classes with the same instantiating object.
271
+ * Have different Transition logs/tables for different objects.
272
+ * Turn parts of a transition on and off based off of an initializer argument
273
+
274
+
275
+ The following is an adapted version of the original Statesman README.
276
+
277
+ ---
278
+
279
+ ![Statesmin](http://f.cl.ly/items/410n2A0S3l1W0i3i0o2K/statesman.png)
280
+
281
+ A statesmanlike state machine library for Ruby 2.0.0 and up.
282
+
283
+ Statesmin is an opinionated state machine library designed to provide a robust
284
+ audit trail and data integrity. It decouples the state machine logic from the
285
+ underlying model and allows for easy composition with one or more model classes.
286
+
287
+ As such, the design of statesman is a little different from other state machine
288
+ libraries:
289
+ - State behaviour is defined in a separate, "state machine" class, rather than
290
+ added directly onto a model. State machines are then instantiated with the model
291
+ to which they should apply.
292
+ - ~~State transitions are also modelled as a class, which can optionally be
293
+ persisted to the database for a full audit history. This audit history can
294
+ include JSON metadata set during a transition.~~
295
+ - ~~Database indices are used to offer database-level transaction duplication
296
+ protection.~~
297
+ - Free to define your own transition logic for your application!
298
+
299
+ ## TL;DR Usage
300
+
301
+ ```ruby
302
+
303
+ #######################
304
+ # State Machine Class #
305
+ #######################
306
+ class OrderStateMachine
307
+ include Statesmin::Machine
308
+
309
+ state :pending, initial: true
310
+ state :checking_out
311
+ state :purchased
312
+ state :shipped
313
+ state :cancelled
314
+ state :failed
315
+ state :refunded
316
+
317
+ transition from: :pending, to: [:checking_out, :cancelled]
318
+ transition from: :checking_out, to: [:purchased, :cancelled]
319
+ transition from: :purchased, to: [:shipped, :failed]
320
+ transition from: :shipped, to: :refunded
321
+
322
+ guard_transition(to: :checking_out) do |order|
323
+ order.products_in_stock?
324
+ end
325
+
326
+ before_transition(from: :checking_out, to: :cancelled) do |order, transition|
327
+ order.reallocate_stock
328
+ end
329
+
330
+ before_transition(to: :purchased) do |order, transition|
331
+ PaymentService.new(order).submit
332
+ end
333
+
334
+ after_transition(to: :purchased) do |order, transition|
335
+ MailerService.order_confirmation(order).deliver
336
+ end
337
+ end
338
+
339
+ ##############
340
+ # Your Model #
341
+ ##############
342
+ class Order < ActiveRecord::Base
343
+ include Statesmin::Adapters::ActiveRecordQueries
344
+
345
+ has_many :order_transitions, autosave: false
346
+
347
+ def state_machine
348
+ @state_machine ||= OrderStateMachine.new(self, transition_class: OrderTransition)
349
+ end
350
+
351
+ def self.transition_class
352
+ OrderTransition
353
+ end
354
+ private_class_method :transition_class
355
+
356
+ def self.initial_state
357
+ :pending
358
+ end
359
+ private_class_method :initial_state
360
+ end
361
+
362
+ ####################
363
+ # Transition Model #
364
+ ####################
365
+ class OrderTransition < ActiveRecord::Base
366
+ include Statesmin::Adapters::ActiveRecordTransition
367
+
368
+ belongs_to :order, inverse_of: :order_transitions
369
+ end
370
+
371
+ ########################
372
+ # Example method calls #
373
+ ########################
374
+ Order.first.state_machine.current_state # => "pending"
375
+ Order.first.state_machine.allowed_transitions # => ["checking_out", "cancelled"]
376
+ Order.first.state_machine.can_transition_to?(:cancelled) # => true/false
377
+ Order.first.state_machine.transition_to(:cancelled, optional: :metadata) # => true/false
378
+ Order.first.state_machine.transition_to!(:cancelled) # => true/exception
379
+
380
+ Order.in_state(:cancelled) # => [#<Order id: "123">]
381
+ Order.not_in_state(:checking_out) # => [#<Order id: "123">]
382
+
383
+ ```
384
+
385
+
386
+ ## Class methods
387
+
388
+ #### `Machine.state`
389
+ ```ruby
390
+ Machine.state(:some_state, initial: true)
391
+ Machine.state(:another_state)
392
+ ```
393
+ Define a new state and optionally mark as the initial state.
394
+
395
+ #### `Machine.transition`
396
+ ```ruby
397
+ Machine.transition(from: :some_state, to: :another_state)
398
+ ```
399
+ Define a transition rule. Both method parameters are required, `to` can also be
400
+ an array of states (`.transition(from: :some_state, to: [:another_state, :some_other_state])`).
401
+
402
+ #### `Machine.guard_transition`
403
+ ```ruby
404
+ Machine.guard_transition(from: :some_state, to: :another_state) do |object|
405
+ object.some_boolean?
406
+ end
407
+ ```
408
+ Define a guard. `to` and `from` parameters are optional, a nil parameter means
409
+ guard all transitions. The passed block should evaluate to a boolean and must
410
+ be idempotent as it could be called many times.
411
+
412
+ #### `Machine.before_transition`
413
+ ```ruby
414
+ Machine.before_transition(from: :some_state, to: :another_state) do |object|
415
+ object.side_effect
416
+ end
417
+ ```
418
+ Define a callback to run before a transition. `to` and `from` parameters are
419
+ optional, a nil parameter means run before all transitions. This callback can
420
+ have side-effects as it will only be run once immediately before the transition.
421
+
422
+ #### `Machine.after_transition`
423
+ ```ruby
424
+ Machine.after_transition(from: :some_state, to: :another_state) do |object, transition|
425
+ object.side_effect
426
+ end
427
+ ```
428
+ Define a callback to run after a successful transition. `to` and `from`
429
+ parameters are optional, a nil parameter means run after all transitions. The
430
+ model object and transition object are passed as arguments to the callback.
431
+ This callback can have side-effects as it will only be run once immediately
432
+ after the transition.
433
+
434
+ If you specify `after_commit: true`, the callback will be executed once the
435
+ transition has been committed to the database.
436
+
437
+ #### `Machine.new`
438
+ ```ruby
439
+ my_machine = Machine.new(my_model)
440
+ ```
441
+ Initialize a new state machine instance. `my_model` is required.
442
+
443
+ #### `Machine.retry_conflicts`
444
+ ```ruby
445
+ Machine.retry_conflicts { instance.transition_to(:new_state) }
446
+ ```
447
+ Automatically retry the given block if a `TransitionConflictError` is raised.
448
+ If you know you want to retry a transition if it fails due to a race condition
449
+ call it from within this block. Takes an (optional) argument for the maximum
450
+ number of retry attempts (defaults to 1).
451
+
452
+ ## Instance methods
453
+
454
+ #### `Machine#current_state`
455
+ Returns the current state based on existing transition objects.
456
+
457
+ #### `Machine#in_state?(:state_1, :state_2, ...)`
458
+ Returns true if the machine is in any of the given states.
459
+
460
+ #### `Machine#allowed_transitions`
461
+ Returns an array of states you can `transition_to` from current state.
462
+
463
+ #### `Machine#can_transition_to?(:state)`
464
+ Returns true if the current state can transition to the passed state and all
465
+ applicable guards pass.
466
+
467
+ #### `Machine#transition_to!(:state)`
468
+ Transition to the passed state, returning `true` on success. Raises
469
+ `Statesmin::GuardFailedError` or `Statesmin::TransitionFailedError` on failure.
470
+
471
+ #### `Machine#transition_to(:state)`
472
+ Transition to the passed state, returning `true` on success. Swallows all
473
+ Statesmin exceptions and returns false on failure. (NB. if your guard or
474
+ callback code throws an exception, it will not be caught.)
475
+
476
+ ## Frequently Asked Questions
477
+
478
+ #### Storing the state on the model object
479
+
480
+ If you wish to store the model state on the model directly, you can keep it up
481
+ to date using an `after_transition` hook:
482
+
483
+ ```ruby
484
+ after_transition do |model, transition|
485
+ model.state = transition.to_state
486
+ model.save!
487
+ end
488
+ ```
489
+
490
+ You could also use a calculated column or view in your database.
491
+
492
+ #### Accessing metadata from the last transition
493
+
494
+ Given a field `foo` that was stored in the metadata, you can access it like so:
495
+
496
+ ```ruby
497
+ model_instance.last_transition.metadata["foo"]
498
+ ```
499
+
500
+ #### Events
501
+
502
+ Used to using a state machine with "events"? Support for events is provided by
503
+ the [statesman-events](https://github.com/gocardless/statesman-events) gem. Once
504
+ that's included in your Gemfile you can include event functionality in your
505
+ state machine as follows:
506
+
507
+ ```ruby
508
+ class OrderStateMachine
509
+ include Statesmin::Machine
510
+ include Statesmin::Events
511
+
512
+ ...
513
+ end
514
+ ```
515
+
516
+ ## Testing Statesmin Implementations
517
+
518
+ This answer was abstracted from [this issue](https://github.com/gocardless/statesman/issues/77).
519
+
520
+ At GoCardless we focus on testing that:
521
+ - guards correctly prevent / allow transitions
522
+ - callbacks execute when expected and perform the expected actions
523
+
524
+ #### Testing Guards
525
+
526
+ Guards can be tested by asserting that `transition_to!` does or does not raise a `Statesmin::GuardFailedError`:
527
+
528
+ ```ruby
529
+ describe "guards" do
530
+ it "cannot transition from state foo to state bar" do
531
+ expect { some_model.transition_to!(:bar) }.to raise_error(Statesmin::GuardFailedError)
532
+ end
533
+
534
+ it "can transition from state foo to state baz" do
535
+ expect { some_model.transition_to!(:baz) }.to_not raise_error
536
+ end
537
+ end
538
+ ```
539
+
540
+ #### Testing Callbacks
541
+
542
+ Callbacks are tested by asserting that the action they perform occurs:
543
+
544
+ ```ruby
545
+ describe "some callback" do
546
+ it "adds one to the count property on the model" do
547
+ expect { some_model.transition_to!(:some_state) }.
548
+ to change { some_model.reload.count }.
549
+ by(1)
550
+ end
551
+ end
552
+ ```
553
+
554
+ ---
555
+
556
+ GoCardless ♥ open source. If you do too, come [join us](https://gocardless.com/jobs#software-engineer).