simple_state_machine 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|
|