statesmin 1.0.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.
@@ -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).