simple_state_machine 0.6.0 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/build.yml +46 -0
- data/.gitignore +3 -0
- data/.travis.yml +17 -0
- data/Gemfile +7 -0
- data/README.rdoc +31 -29
- data/examples/conversation.rb +5 -5
- data/examples/lamp.rb +5 -5
- data/examples/relationship.rb +8 -8
- data/examples/traffic_light.rb +3 -3
- data/examples/user.rb +1 -0
- data/gemfiles/Gemfile.activerecord-5.2.x +9 -0
- data/gemfiles/Gemfile.activerecord-6.0.x +9 -0
- data/gemfiles/Gemfile.activerecord-6.1.x +8 -0
- data/gemfiles/Gemfile.activerecord-main.x +9 -0
- data/gemfiles/Gemfile.basic +6 -0
- data/lib/simple_state_machine/decorator/active_record.rb +12 -18
- data/lib/simple_state_machine/tools/graphviz.rb +6 -2
- data/lib/simple_state_machine/version.rb +1 -1
- data/lib/simple_state_machine.rb +5 -4
- data/lib/tasks/graphviz.rake +10 -0
- data/simple_state_machine.gemspec +14 -25
- data/spec/active_record_spec.rb +196 -199
- data/spec/decorator/default_spec.rb +28 -28
- data/spec/examples_spec.rb +13 -13
- data/spec/mountable_spec.rb +18 -16
- data/spec/simple_state_machine_spec.rb +71 -63
- data/spec/spec_helper.rb +16 -9
- data/spec/state_machine_definition_spec.rb +21 -21
- data/spec/state_machine_spec.rb +12 -12
- data/spec/tools/graphviz_spec.rb +3 -2
- data/spec/tools/inspector_spec.rb +3 -3
- metadata +36 -137
- data/autotest/discover.rb +0 -1
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4cf708bed33ca00037b1fbb2562b2a4c351666d82a4555b84078600b9e90ad03
|
4
|
+
data.tar.gz: 251e24c03f5c59313aec334045670ed024c2720630c188d17bfed1eced1827be
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 482b1ba0a9e91260dfe56153453b636640dcd7c37916fd692e13d8607f59ee141d7161c9db6e9a50cdbb14b21b2895227129edf1c4f35dbcb733caceafeb8beb
|
7
|
+
data.tar.gz: 5c3b05eb49e792a4289e85c5be1f10f9239020a0147e0ab5b617bba58d7fac4b727a47f45564c472e25e4079e8d1fb7d52838f9a8af9dd610a90e2580d6a9132
|
@@ -0,0 +1,46 @@
|
|
1
|
+
name: Build
|
2
|
+
on:
|
3
|
+
- push
|
4
|
+
- pull_request
|
5
|
+
|
6
|
+
jobs:
|
7
|
+
build:
|
8
|
+
name: Ruby ${{ matrix.ruby }} / Rails ${{ matrix.rails }}
|
9
|
+
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
10
|
+
strategy:
|
11
|
+
fail-fast: false
|
12
|
+
matrix:
|
13
|
+
ruby:
|
14
|
+
- "3.0"
|
15
|
+
- "2.7"
|
16
|
+
- "2.6"
|
17
|
+
- "2.5"
|
18
|
+
rails:
|
19
|
+
- "5.2"
|
20
|
+
- "6.0"
|
21
|
+
- "6.1"
|
22
|
+
- main
|
23
|
+
exclude:
|
24
|
+
- ruby: 2.5
|
25
|
+
rails: main
|
26
|
+
- ruby: 2.6
|
27
|
+
rails: main
|
28
|
+
- ruby: 3.0
|
29
|
+
rails: "5.1"
|
30
|
+
- ruby: 3.0
|
31
|
+
rails: "5.2"
|
32
|
+
|
33
|
+
runs-on: 'ubuntu-latest'
|
34
|
+
|
35
|
+
env:
|
36
|
+
BUNDLE_GEMFILE: gemfiles/Gemfile.activerecord-${{ matrix.rails }}.x
|
37
|
+
|
38
|
+
steps:
|
39
|
+
- uses: actions/checkout@v2
|
40
|
+
- uses: ruby/setup-ruby@v1
|
41
|
+
with:
|
42
|
+
ruby-version: ${{ matrix.ruby }}
|
43
|
+
- name: Setup project
|
44
|
+
run: bundle install
|
45
|
+
- name: Run test
|
46
|
+
run: bundle exec rspec spec
|
data/.gitignore
CHANGED
data/.travis.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 2.5
|
4
|
+
- 2.7
|
5
|
+
- 3.0
|
6
|
+
- jruby-9.2.16.0
|
7
|
+
script: bundle exec rspec spec
|
8
|
+
|
9
|
+
gemfile:
|
10
|
+
- gemfiles/Gemfile.activerecord-6.0.x
|
11
|
+
- gemfiles/Gemfile.activerecord-6.1.x
|
12
|
+
- gemfiles/Gemfile.basic
|
13
|
+
|
14
|
+
matrix:
|
15
|
+
exclude:
|
16
|
+
- rvm: jruby
|
17
|
+
gemfile: gemfiles/Gemfile.activerecord-4.2.x
|
data/Gemfile
CHANGED
data/README.rdoc
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
= SimpleStateMachine
|
2
2
|
|
3
|
+
{<img src="https://github.com/mdh/ssm/actions/workflows/build.yml/badge.svg" />}[https://github.com/mdh/ssm/actions?query=workflow%3A.github%2Fworkflows%2Fbuild.yml+branch%3Amaster++]
|
4
|
+
|
3
5
|
A simple DSL to decorate existing methods with state transition guards.
|
4
6
|
|
5
7
|
Instead of using a DSL to define events, SimpleStateMachine decorates methods
|
@@ -7,26 +9,14 @@ to help you encapsulate state and guard state transitions.
|
|
7
9
|
|
8
10
|
It supports exception rescuing, google chart visualization and mountable state machines.
|
9
11
|
|
10
|
-
|
11
|
-
== Installation
|
12
|
-
|
13
|
-
Use gem install:
|
14
|
-
|
15
|
-
gem install simple_state_machine
|
16
|
-
|
17
|
-
Or add it to your Gemfile:
|
18
|
-
|
19
|
-
gem 'simple_state_machine'
|
20
|
-
|
21
|
-
|
22
12
|
== Usage
|
23
13
|
|
24
14
|
Define an event and specify how the state should transition. If we want the state to change
|
25
|
-
from
|
15
|
+
from *pending* to *active* we write:
|
26
16
|
|
27
17
|
event :activate_account, :pending => :active
|
28
18
|
|
29
|
-
That's it. You can now call activate_account and the state will automatically change.
|
19
|
+
That's it. You can now call *activate_account* and the state will automatically change.
|
30
20
|
If the state change is not allowed, a SimpleStateMachine::IllegalStateTransitionError is
|
31
21
|
raised.
|
32
22
|
|
@@ -74,20 +64,23 @@ when the method is called.
|
|
74
64
|
|
75
65
|
== ActiveRecord
|
76
66
|
|
67
|
+
For ActiveRecord methods are decorated with state transition guards _and_ persistence.
|
68
|
+
Methods marked as events behave like ActiveRecord *save* and *save!*.
|
69
|
+
|
77
70
|
=== Example
|
78
71
|
|
79
72
|
To add a state machine to an ActiveRecord class, you will have to:
|
80
|
-
|
81
|
-
|
82
|
-
|
73
|
+
* extend SimpleStateMachine::ActiveRecord,
|
74
|
+
* set the initial state in after_initialize,
|
75
|
+
* turn methods into events
|
83
76
|
|
84
77
|
class User < ActiveRecord::Base
|
85
78
|
|
86
79
|
extend SimpleStateMachine::ActiveRecord
|
87
80
|
|
88
|
-
|
81
|
+
after_initialize do
|
89
82
|
self.state ||= 'pending'
|
90
|
-
|
83
|
+
end
|
91
84
|
|
92
85
|
def invite
|
93
86
|
self.activation_code = Digest::SHA1.hexdigest("salt #{Time.now.to_f}")
|
@@ -102,12 +95,12 @@ To add a state machine to an ActiveRecord class, you will have to:
|
|
102
95
|
user.activation_code # => 'SOMEDIGEST'
|
103
96
|
|
104
97
|
For the invite method this generates the following event methods
|
105
|
-
|
106
|
-
|
98
|
+
* *invite* (behaves like ActiveRecord save )
|
99
|
+
* *invite!* (behaves like ActiveRecord save!)
|
107
100
|
|
108
101
|
If you want to be more verbose you can also use:
|
109
|
-
|
110
|
-
|
102
|
+
* *invite_and_save* (alias for invite)
|
103
|
+
* *invite_and_save!* (alias for invite!)
|
111
104
|
|
112
105
|
|
113
106
|
=== Using ActiveRecord / ActiveModel validations
|
@@ -153,7 +146,7 @@ If you like to separate your state machine from your model class, you can do so
|
|
153
146
|
extend SimpleStateMachine::Mountable
|
154
147
|
mount_state_machine MyStateMachine
|
155
148
|
|
156
|
-
|
149
|
+
after_initialize do
|
157
150
|
self.state ||= 'new'
|
158
151
|
end
|
159
152
|
|
@@ -163,7 +156,7 @@ If you like to separate your state machine from your model class, you can do so
|
|
163
156
|
== Transitions
|
164
157
|
|
165
158
|
=== Catching all from states
|
166
|
-
If an event should transition from all other defined states, you can use
|
159
|
+
If an event should transition from all other defined states, you can use the *:all* state:
|
167
160
|
|
168
161
|
event :suspend, :all => :suspended
|
169
162
|
|
@@ -173,18 +166,18 @@ If an event should transition from all other defined states, you can use :all as
|
|
173
166
|
You can let the state machine handle exceptions by specifying the failure state for an Error:
|
174
167
|
|
175
168
|
def download_data
|
176
|
-
raise Service::ConnectionError
|
169
|
+
raise Service::ConnectionError, "Uhoh"
|
177
170
|
end
|
178
171
|
event :download_data, Service::ConnectionError => :download_failed
|
179
172
|
|
180
173
|
download_data # catches Service::ConnectionError
|
181
174
|
state # => "download_failed"
|
182
|
-
state_machine.raised_error #
|
175
|
+
state_machine.raised_error # "Uhoh"
|
183
176
|
|
184
177
|
|
185
178
|
=== Default error state
|
186
179
|
|
187
|
-
To automatically
|
180
|
+
To automatically catch all exceptions to a default error state use default_error_state:
|
188
181
|
|
189
182
|
state_machine_definition.default_error_state = :failed
|
190
183
|
|
@@ -192,7 +185,7 @@ To automatically change all states to a default error state use default_error_st
|
|
192
185
|
|
193
186
|
If you want to run events in transactions run them in a transaction block:
|
194
187
|
|
195
|
-
user.transaction { user.invite }
|
188
|
+
user.transaction { user.invite! }
|
196
189
|
|
197
190
|
== Tools
|
198
191
|
|
@@ -207,6 +200,15 @@ For details run:
|
|
207
200
|
A Googlechart example:
|
208
201
|
http://tinyurl.com/79xztr6
|
209
202
|
|
203
|
+
== Installation
|
204
|
+
|
205
|
+
Use gem install:
|
206
|
+
|
207
|
+
gem install simple_state_machine
|
208
|
+
|
209
|
+
Or add it to your Gemfile:
|
210
|
+
|
211
|
+
gem 'simple_state_machine'
|
210
212
|
|
211
213
|
== Note on Patches/Pull Requests
|
212
214
|
|
data/examples/conversation.rb
CHANGED
@@ -2,19 +2,19 @@
|
|
2
2
|
#
|
3
3
|
# class Conversation
|
4
4
|
# include AASM
|
5
|
-
#
|
5
|
+
#
|
6
6
|
# aasm_column :current_state # defaults to aasm_state
|
7
|
-
#
|
7
|
+
#
|
8
8
|
# aasm_initial_state :unread
|
9
|
-
#
|
9
|
+
#
|
10
10
|
# aasm_state :unread
|
11
11
|
# aasm_state :read
|
12
12
|
# aasm_state :closed
|
13
|
-
#
|
13
|
+
#
|
14
14
|
# aasm_event :view do
|
15
15
|
# transitions :to => :read, :from => [:unread]
|
16
16
|
# end
|
17
|
-
#
|
17
|
+
#
|
18
18
|
# aasm_event :close do
|
19
19
|
# transitions :to => :closed, :from => [:read, :unread]
|
20
20
|
# end
|
data/examples/lamp.rb
CHANGED
@@ -1,21 +1,21 @@
|
|
1
1
|
class Lamp
|
2
|
-
|
2
|
+
|
3
3
|
extend SimpleStateMachine
|
4
4
|
|
5
5
|
def initialize
|
6
6
|
self.state = 'off'
|
7
7
|
end
|
8
|
-
|
8
|
+
|
9
9
|
def push_button1
|
10
10
|
puts "click1: #{state}"
|
11
11
|
end
|
12
|
-
event :push_button1, :off => :on,
|
12
|
+
event :push_button1, :off => :on,
|
13
13
|
:on => :off
|
14
|
-
|
14
|
+
|
15
15
|
def push_button2
|
16
16
|
puts "click2: #{state}"
|
17
17
|
end
|
18
18
|
event :push_button2, :off => :on,
|
19
19
|
:on => :off
|
20
20
|
|
21
|
-
end
|
21
|
+
end
|
data/examples/relationship.rb
CHANGED
@@ -2,23 +2,23 @@
|
|
2
2
|
#
|
3
3
|
# class Relationship
|
4
4
|
# include AASM
|
5
|
-
#
|
5
|
+
#
|
6
6
|
# aasm_column :status
|
7
|
-
#
|
7
|
+
#
|
8
8
|
# aasm_initial_state Proc.new { |relationship| relationship.strictly_for_fun? ? :intimate : :dating }
|
9
|
-
#
|
9
|
+
#
|
10
10
|
# aasm_state :dating, :enter => :make_happy, :exit => :make_depressed
|
11
11
|
# aasm_state :intimate, :enter => :make_very_happy, :exit => :never_speak_again
|
12
12
|
# aasm_state :married, :enter => :give_up_intimacy, :exit => :buy_exotic_car_and_wear_a_combover
|
13
|
-
#
|
13
|
+
#
|
14
14
|
# aasm_event :get_intimate do
|
15
15
|
# transitions :to => :intimate, :from => [:dating], :guard => :drunk?
|
16
16
|
# end
|
17
|
-
#
|
17
|
+
#
|
18
18
|
# aasm_event :get_married do
|
19
19
|
# transitions :to => :married, :from => [:dating, :intimate], :guard => :willing_to_give_up_manhood?
|
20
20
|
# end
|
21
|
-
#
|
21
|
+
#
|
22
22
|
# def strictly_for_fun?; end
|
23
23
|
# def drunk?; end
|
24
24
|
# def willing_to_give_up_manhood?; end
|
@@ -32,7 +32,7 @@
|
|
32
32
|
|
33
33
|
class Relationship
|
34
34
|
extend SimpleStateMachine
|
35
|
-
|
35
|
+
|
36
36
|
def initialize
|
37
37
|
self.state = relationship.strictly_for_fun? ? get_intimate : start_dating
|
38
38
|
end
|
@@ -84,4 +84,4 @@ class Relationship
|
|
84
84
|
def never_speak_again; end
|
85
85
|
def give_up_intimacy; end
|
86
86
|
def buy_exotic_car_and_wear_a_combover; end
|
87
|
-
end
|
87
|
+
end
|
data/examples/traffic_light.rb
CHANGED
data/examples/user.rb
CHANGED
@@ -0,0 +1,9 @@
|
|
1
|
+
# A sample Gemfile
|
2
|
+
source "http://rubygems.org"
|
3
|
+
|
4
|
+
group :test do
|
5
|
+
gem "rspec"
|
6
|
+
gem "activerecord", git: "https://github.com/rails/rails.git", branch: "main"
|
7
|
+
gem "sqlite3", :platform => [:ruby, :mswin, :mingw]
|
8
|
+
gem "activerecord-jdbcsqlite3-adapter", :platform => :jruby
|
9
|
+
end
|
@@ -20,19 +20,14 @@ module SimpleStateMachine
|
|
20
20
|
event_name_and_save = "#{event_name}_and_save"
|
21
21
|
unless @subject.method_defined?(event_name_and_save)
|
22
22
|
@subject.send(:define_method, event_name_and_save) do |*args|
|
23
|
-
result = false
|
24
23
|
old_state = self.send(self.class.state_machine_definition.state_method)
|
25
24
|
send "#{event_name}_with_managed_state", *args
|
26
|
-
if
|
27
|
-
|
25
|
+
if self.errors.entries.empty? && save
|
26
|
+
true
|
28
27
|
else
|
29
|
-
|
30
|
-
|
31
|
-
else
|
32
|
-
self.send("#{self.class.state_machine_definition.state_method}=", old_state)
|
33
|
-
end
|
28
|
+
self.send("#{self.class.state_machine_definition.state_method}=", old_state)
|
29
|
+
false
|
34
30
|
end
|
35
|
-
return result
|
36
31
|
end
|
37
32
|
@subject.send :alias_method, "#{event_name}", event_name_and_save
|
38
33
|
end
|
@@ -42,20 +37,19 @@ module SimpleStateMachine
|
|
42
37
|
event_name_and_save_bang = "#{event_name}_and_save!"
|
43
38
|
unless @subject.method_defined?(event_name_and_save_bang)
|
44
39
|
@subject.send(:define_method, event_name_and_save_bang) do |*args|
|
45
|
-
result = nil
|
46
40
|
old_state = self.send(self.class.state_machine_definition.state_method)
|
47
41
|
send "#{event_name}_with_managed_state", *args
|
48
|
-
if
|
42
|
+
if self.errors.entries.empty?
|
43
|
+
begin
|
44
|
+
save!
|
45
|
+
rescue ::ActiveRecord::RecordInvalid
|
46
|
+
self.send("#{self.class.state_machine_definition.state_method}=", old_state)
|
47
|
+
raise #re raise
|
48
|
+
end
|
49
|
+
else
|
49
50
|
self.send("#{self.class.state_machine_definition.state_method}=", old_state)
|
50
51
|
raise ::ActiveRecord::RecordInvalid.new(self)
|
51
52
|
end
|
52
|
-
begin
|
53
|
-
result = save!
|
54
|
-
rescue ::ActiveRecord::RecordInvalid
|
55
|
-
self.send("#{self.class.state_machine_definition.state_method}=", old_state)
|
56
|
-
raise #re raise
|
57
|
-
end
|
58
|
-
return result
|
59
53
|
end
|
60
54
|
@subject.send :alias_method, "#{event_name}!", event_name_and_save_bang
|
61
55
|
end
|
@@ -4,13 +4,17 @@ module SimpleStateMachine
|
|
4
4
|
module Graphviz
|
5
5
|
# Graphviz dot format for rendering as a directional graph
|
6
6
|
def to_graphviz_dot
|
7
|
-
|
7
|
+
"digraph G {\n" +
|
8
|
+
transitions.map { |t| t.to_graphviz_dot }.sort.join(";\n") +
|
9
|
+
"\n}"
|
8
10
|
end
|
9
11
|
|
10
12
|
# Generates a url that renders states and events as a directional graph.
|
11
13
|
# See http://code.google.com/apis/chart/docs/gallery/graphviz.html
|
12
14
|
def google_chart_url
|
13
|
-
|
15
|
+
graph = transitions.map { |t| t.to_graphviz_dot }.sort.join(";")
|
16
|
+
puts graph
|
17
|
+
"http://chart.googleapis.com/chart?cht=gv&chl=digraph{#{::CGI.escape graph}}"
|
14
18
|
end
|
15
19
|
end
|
16
20
|
end
|
data/lib/simple_state_machine.rb
CHANGED
@@ -5,9 +5,10 @@ require 'simple_state_machine/tools/inspector'
|
|
5
5
|
require 'simple_state_machine/state_machine_definition'
|
6
6
|
require 'simple_state_machine/transition'
|
7
7
|
require 'simple_state_machine/decorator/default'
|
8
|
-
|
8
|
+
if defined?(ActiveRecord)
|
9
9
|
require 'simple_state_machine/active_record'
|
10
10
|
require 'simple_state_machine/decorator/active_record'
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
end
|
12
|
+
if defined?(Rails::Railtie)
|
13
|
+
require "simple_state_machine/railtie"
|
14
|
+
end
|
data/lib/tasks/graphviz.rake
CHANGED
@@ -1,5 +1,15 @@
|
|
1
1
|
namespace :ssm do
|
2
2
|
namespace :graph do
|
3
|
+
|
4
|
+
desc 'Outputs a dot file. You must specify class=ClassNAME'
|
5
|
+
task :dot => :environment do
|
6
|
+
if clazz = ENV['class']
|
7
|
+
puts clazz.constantize.state_machine_definition.to_graphviz_dot
|
8
|
+
else
|
9
|
+
puts "Missing argument: class. Please specify class=ClassName"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
3
13
|
desc 'Generate a url for a google chart. You must specify class=ClassName'
|
4
14
|
task :url => :environment do
|
5
15
|
if clazz = ENV['class']
|
@@ -3,30 +3,19 @@ $:.push File.expand_path("../lib", __FILE__)
|
|
3
3
|
require "simple_state_machine/version"
|
4
4
|
|
5
5
|
Gem::Specification.new do |s|
|
6
|
-
s.name
|
7
|
-
s.version
|
8
|
-
s.platform
|
9
|
-
|
10
|
-
s.
|
11
|
-
s.
|
12
|
-
s.
|
13
|
-
s.
|
14
|
-
s.
|
15
|
-
|
16
|
-
"
|
17
|
-
]
|
18
|
-
s.
|
19
|
-
s.
|
20
|
-
s.rdoc_options = ["--charset=UTF-8"]
|
21
|
-
s.require_paths = ["lib"]
|
22
|
-
s.rubygems_version = %q{1.3.7}
|
23
|
-
s.summary = %q{A simple DSL to decorate existing methods with logic that guards state transitions.}
|
24
|
-
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
25
|
-
s.add_development_dependency "rake"
|
26
|
-
s.add_development_dependency "ZenTest"
|
27
|
-
s.add_development_dependency "rspec"
|
28
|
-
s.add_development_dependency "activerecord", "~>2.3.5"
|
29
|
-
s.add_development_dependency "sqlite3-ruby"
|
30
|
-
s.add_development_dependency "ruby-debug"
|
6
|
+
s.name = %q{simple_state_machine}
|
7
|
+
s.version = SimpleStateMachine::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Marek de Heus", "Petrik de Heus"]
|
10
|
+
s.description = %q{A simple DSL to decorate existing methods with state transition guards.}
|
11
|
+
s.email = ["FIX@example.com"]
|
12
|
+
s.homepage = %q{http://github.com/mdh/ssm}
|
13
|
+
s.extra_rdoc_files = ["LICENSE","README.rdoc"]
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
16
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
s.summary = %q{A simple DSL to decorate existing methods with logic that guards state transitions.}
|
19
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
31
20
|
end
|
32
21
|
|