ardm-is-state_machine 1.2.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 +7 -0
- data/.gitignore +35 -0
- data/.travis.yml +11 -0
- data/Gemfile +65 -0
- data/LICENSE +20 -0
- data/README.rdoc +102 -0
- data/Rakefile +4 -0
- data/ardm-is-state_machine.gemspec +24 -0
- data/lib/ardm-is-state_machine.rb +1 -0
- data/lib/dm-is-state_machine.rb +10 -0
- data/lib/dm-is-state_machine/is/data/event.rb +25 -0
- data/lib/dm-is-state_machine/is/data/machine.rb +90 -0
- data/lib/dm-is-state_machine/is/data/state.rb +21 -0
- data/lib/dm-is-state_machine/is/dsl/event_dsl.rb +81 -0
- data/lib/dm-is-state_machine/is/dsl/state_dsl.rb +40 -0
- data/lib/dm-is-state_machine/is/state_machine.rb +139 -0
- data/lib/dm-is-state_machine/is/version.rb +7 -0
- data/spec/examples/invalid_events.rb +20 -0
- data/spec/examples/invalid_states.rb +20 -0
- data/spec/examples/invalid_transitions_1.rb +22 -0
- data/spec/examples/invalid_transitions_2.rb +22 -0
- data/spec/examples/light_switch.rb +25 -0
- data/spec/examples/slot_machine.rb +48 -0
- data/spec/examples/traffic_light.rb +48 -0
- data/spec/integration/inheritance_spec.rb +11 -0
- data/spec/integration/invalid_events_spec.rb +11 -0
- data/spec/integration/invalid_states_spec.rb +11 -0
- data/spec/integration/invalid_transitions_spec.rb +21 -0
- data/spec/integration/slot_machine_spec.rb +86 -0
- data/spec/integration/traffic_light_spec.rb +181 -0
- data/spec/rcov.opts +6 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/unit/data/event_spec.rb +27 -0
- data/spec/unit/data/machine_spec.rb +102 -0
- data/spec/unit/data/state_spec.rb +21 -0
- data/spec/unit/dsl/event_dsl_spec.rb +55 -0
- data/spec/unit/dsl/state_dsl_spec.rb +24 -0
- data/spec/unit/state_machine_spec.rb +28 -0
- data/tasks/spec.rake +38 -0
- data/tasks/yard.rake +9 -0
- data/tasks/yardstick.rake +19 -0
- metadata +132 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Is
|
3
|
+
module StateMachine
|
4
|
+
# State DSL (Domain Specific Language)
|
5
|
+
module StateDsl
|
6
|
+
|
7
|
+
# Define a state of the system.
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
#
|
11
|
+
# class TrafficLight
|
12
|
+
# include DataMapper::Resource
|
13
|
+
# property :id, Serial
|
14
|
+
# is :state_machine do
|
15
|
+
# state :green, :enter => Proc.new { |o| o.log("G") }
|
16
|
+
# state :yellow, :enter => Proc.new { |o| o.log("Y") }
|
17
|
+
# state :red, :enter => Proc.new { |o| o.log("R") }
|
18
|
+
#
|
19
|
+
# # event definitions go here...
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# def log(string)
|
23
|
+
# Merb::Logger.info(string)
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
def state(name, options = {})
|
27
|
+
unless state_machine_context?(:is)
|
28
|
+
raise InvalidContext, "Valid only in 'is :state_machine' block"
|
29
|
+
end
|
30
|
+
|
31
|
+
# ===== Setup context =====
|
32
|
+
machine = @is_state_machine[:machine]
|
33
|
+
state = Data::State.new(name, machine, options)
|
34
|
+
machine.states << state
|
35
|
+
end
|
36
|
+
|
37
|
+
end # StateDsl
|
38
|
+
end # StateMachine
|
39
|
+
end # Is
|
40
|
+
end # DataMapper
|
@@ -0,0 +1,139 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Is
|
3
|
+
module StateMachine
|
4
|
+
|
5
|
+
class InvalidContext < RuntimeError; end
|
6
|
+
class InvalidState < RuntimeError; end
|
7
|
+
class InvalidEvent < RuntimeError; end
|
8
|
+
class EventConfusion < RuntimeError; end
|
9
|
+
class DuplicateStates < RuntimeError; end
|
10
|
+
class NoInitialState < RuntimeError; end
|
11
|
+
|
12
|
+
##
|
13
|
+
# Makes a column ('state' by default) act as a state machine. It will
|
14
|
+
# define the property if it does not exist.
|
15
|
+
#
|
16
|
+
# @example [Usage]
|
17
|
+
# is :state_machine
|
18
|
+
# is :state_machine, :initial => :internal
|
19
|
+
# is :state_machine, :column => :availability
|
20
|
+
# is :state_machine, :column => :availability, :initial => :external
|
21
|
+
#
|
22
|
+
# @param options<Hash> a hash of options
|
23
|
+
#
|
24
|
+
# @option :column<Symbol> the name of the custom column
|
25
|
+
#
|
26
|
+
def is_state_machine(options = {}, &block)
|
27
|
+
extend DataMapper::Is::StateMachine::EventDsl
|
28
|
+
extend DataMapper::Is::StateMachine::StateDsl
|
29
|
+
include DataMapper::Is::StateMachine::InstanceMethods
|
30
|
+
|
31
|
+
# ===== Setup context =====
|
32
|
+
options = { :column => :state, :initial => nil }.merge(options)
|
33
|
+
column = options[:column]
|
34
|
+
initial = options[:initial].to_s
|
35
|
+
unless properties.detect { |p| p.name == column }
|
36
|
+
property column, String, :default => initial
|
37
|
+
end
|
38
|
+
machine = Data::Machine.new(column, initial)
|
39
|
+
@is_state_machine = { :machine => machine }
|
40
|
+
|
41
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
42
|
+
def #{column}=(value)
|
43
|
+
value = value.to_s if value.kind_of?(Symbol)
|
44
|
+
attribute_set(#{column.inspect}, value)
|
45
|
+
end
|
46
|
+
RUBY
|
47
|
+
|
48
|
+
# ===== Define callbacks =====
|
49
|
+
# TODO: define callbacks
|
50
|
+
# before :save do
|
51
|
+
# if self.new_record?
|
52
|
+
# # ...
|
53
|
+
# else
|
54
|
+
# # ...
|
55
|
+
# end
|
56
|
+
# end
|
57
|
+
|
58
|
+
before :destroy do
|
59
|
+
# Do we need to do anything here?
|
60
|
+
end
|
61
|
+
|
62
|
+
# ===== Setup context =====
|
63
|
+
push_state_machine_context(:is)
|
64
|
+
|
65
|
+
yield if block_given?
|
66
|
+
|
67
|
+
# ===== Teardown context =====
|
68
|
+
pop_state_machine_context
|
69
|
+
end
|
70
|
+
|
71
|
+
def inherited(base)
|
72
|
+
super
|
73
|
+
base.class_eval do
|
74
|
+
@is_state_machine = superclass.instance_variable_get(:@is_state_machine)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
protected
|
79
|
+
|
80
|
+
def push_state_machine_context(label)
|
81
|
+
@is_state_machine ||= {}
|
82
|
+
@is_state_machine[:context] ||= []
|
83
|
+
@is_state_machine[:context] << label
|
84
|
+
|
85
|
+
# Compacted, but barely readable for humans
|
86
|
+
# ((@is_state_machine ||= {})[:context] ||= []) << label
|
87
|
+
end
|
88
|
+
|
89
|
+
def pop_state_machine_context
|
90
|
+
@is_state_machine[:context].pop
|
91
|
+
end
|
92
|
+
|
93
|
+
def state_machine_context?(label)
|
94
|
+
(i = @is_state_machine) && (c = i[:context]) &&
|
95
|
+
c.respond_to?(:include?) && c.include?(label)
|
96
|
+
end
|
97
|
+
|
98
|
+
module InstanceMethods
|
99
|
+
|
100
|
+
def initialize(*args)
|
101
|
+
super
|
102
|
+
# ===== Run :enter hook if present =====
|
103
|
+
return unless is_sm = model.instance_variable_get(:@is_state_machine)
|
104
|
+
return unless machine = is_sm[:machine]
|
105
|
+
return unless initial = machine.initial
|
106
|
+
return unless initial_state = machine.find_state(initial)
|
107
|
+
run_hook_if_present initial_state.options[:enter]
|
108
|
+
end
|
109
|
+
|
110
|
+
# hook may be either a Proc or symbol
|
111
|
+
def run_hook_if_present(hook)
|
112
|
+
return unless hook
|
113
|
+
if hook.respond_to?(:call)
|
114
|
+
hook.call(self)
|
115
|
+
else
|
116
|
+
__send__(hook)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def transition!(event_name)
|
121
|
+
machine = model.instance_variable_get(:@is_state_machine)[:machine]
|
122
|
+
column = machine.column
|
123
|
+
machine.current_state_name = __send__(column)
|
124
|
+
machine.fire_event(event_name, self)
|
125
|
+
__send__("#{column}=", machine.current_state_name)
|
126
|
+
end
|
127
|
+
|
128
|
+
end # InstanceMethods
|
129
|
+
|
130
|
+
end # StateMachine
|
131
|
+
end # Is
|
132
|
+
end # DataMapper
|
133
|
+
|
134
|
+
# Notes
|
135
|
+
# -----
|
136
|
+
#
|
137
|
+
# Since this gets mixed into a class, I try to keep the namespace pollution
|
138
|
+
# down to a minimum. This is why I only use the @is_state_machine instance
|
139
|
+
# variable.
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# An invalid example.
|
2
|
+
class InvalidEvents
|
3
|
+
include DataMapper::Resource
|
4
|
+
|
5
|
+
property :id, Serial
|
6
|
+
|
7
|
+
is :state_machine do
|
8
|
+
state :day
|
9
|
+
state :night
|
10
|
+
end
|
11
|
+
|
12
|
+
# The next lines are intentionally incorrect.
|
13
|
+
#
|
14
|
+
# 'event' only makes sense in a block under 'is :state_machine'
|
15
|
+
event :sunrise
|
16
|
+
event :sunset
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
InvalidEvents.auto_migrate!
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# An invalid example.
|
2
|
+
class InvalidStates
|
3
|
+
include DataMapper::Resource
|
4
|
+
|
5
|
+
property :id, Serial
|
6
|
+
|
7
|
+
is :state_machine do
|
8
|
+
event :sunrise
|
9
|
+
event :sunset
|
10
|
+
end
|
11
|
+
|
12
|
+
# The next lines are intentionally incorrect.
|
13
|
+
#
|
14
|
+
# 'state' only makes sense in a block under 'is :state_machine'
|
15
|
+
state :light
|
16
|
+
state :dark
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
InvalidStates.auto_migrate!
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# An invalid example.
|
2
|
+
class InvalidTransitions1
|
3
|
+
include DataMapper::Resource
|
4
|
+
|
5
|
+
property :id, Serial
|
6
|
+
|
7
|
+
is :state_machine do
|
8
|
+
state :happy
|
9
|
+
state :sad
|
10
|
+
|
11
|
+
event :toggle
|
12
|
+
|
13
|
+
# The next lines are intentionally incorrect.
|
14
|
+
#
|
15
|
+
# 'transition' is only valid when nested beneath 'event'
|
16
|
+
transition :to => :happy, :from => :sad
|
17
|
+
transition :to => :sad, :from => :happy
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
InvalidTransitions1.auto_migrate!
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# An invalid example.
|
2
|
+
class InvalidTransitions2
|
3
|
+
include DataMapper::Resource
|
4
|
+
|
5
|
+
property :id, Serial
|
6
|
+
|
7
|
+
is :state_machine do
|
8
|
+
state :happy
|
9
|
+
state :sad
|
10
|
+
|
11
|
+
event :toggle
|
12
|
+
end
|
13
|
+
|
14
|
+
# The next lines are intentionally incorrect.
|
15
|
+
#
|
16
|
+
# 'transition' is only valid when nested beneath 'event'
|
17
|
+
transition :to => :happy, :from => :sad
|
18
|
+
transition :to => :sad, :from => :happy
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
InvalidTransitions2.auto_migrate!
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class LightSwitch
|
2
|
+
include DataMapper::Resource
|
3
|
+
property :id, Serial
|
4
|
+
property :type, Discriminator
|
5
|
+
|
6
|
+
is :state_machine, :initial => :off do
|
7
|
+
state :off
|
8
|
+
state :on, :enter => :on_hook
|
9
|
+
|
10
|
+
event :switch do
|
11
|
+
transition :from => :on, :to => :off
|
12
|
+
transition :from => :off, :to => :on
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def on_hook
|
17
|
+
puts "Light turned on!"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Dimmer < LightSwitch
|
22
|
+
def on_hook
|
23
|
+
puts "Lights! Camera! Action! your're on!"
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# A valid example
|
2
|
+
class SlotMachine
|
3
|
+
include DataMapper::Resource
|
4
|
+
|
5
|
+
property :id, Serial
|
6
|
+
property :power_on, Boolean, :default => false
|
7
|
+
|
8
|
+
is :state_machine, :initial => :off, :column => :mode do
|
9
|
+
state :off,
|
10
|
+
:enter => :power_down,
|
11
|
+
:exit => :power_up
|
12
|
+
state :idle
|
13
|
+
state :spinning
|
14
|
+
state :report_loss
|
15
|
+
state :report_win
|
16
|
+
state :pay_out
|
17
|
+
|
18
|
+
event :pull_crank do
|
19
|
+
transition :from => :idle, :to => :spinning
|
20
|
+
end
|
21
|
+
|
22
|
+
event :turn_off do
|
23
|
+
transition :from => :idle, :to => :off
|
24
|
+
end
|
25
|
+
|
26
|
+
event :turn_on do
|
27
|
+
transition :from => :off, :to => :idle
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(attributes = {})
|
32
|
+
@log = []
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
def power_up
|
37
|
+
self.power_on = true
|
38
|
+
@log << [:power_up, Time.now]
|
39
|
+
end
|
40
|
+
|
41
|
+
def power_down
|
42
|
+
self.power_on = false
|
43
|
+
@log << [:power_down, Time.now]
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
SlotMachine.auto_migrate!
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# A valid example
|
2
|
+
class TrafficLight
|
3
|
+
include DataMapper::Resource
|
4
|
+
|
5
|
+
property :id, Serial # see note 1
|
6
|
+
|
7
|
+
is :state_machine, :initial => :green, :column => :color do
|
8
|
+
state :green, :enter => Proc.new { |o| o.log << "G" }
|
9
|
+
state :yellow, :enter => Proc.new { |o| o.log << "Y" }
|
10
|
+
state :red, :enter => Proc.new { |o| o.log << "R" }
|
11
|
+
|
12
|
+
event :forward do
|
13
|
+
transition :from => :green, :to => :yellow
|
14
|
+
transition :from => :yellow, :to => :red
|
15
|
+
transition :from => :red, :to => :green
|
16
|
+
end
|
17
|
+
|
18
|
+
event :backward do
|
19
|
+
transition :from => :green, :to => :red
|
20
|
+
transition :from => :yellow, :to => :green
|
21
|
+
transition :from => :red, :to => :yellow
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
before :transition!, :before_hook
|
26
|
+
after :transition!, :after_hook
|
27
|
+
|
28
|
+
def before_hook
|
29
|
+
before_hook_log << attribute_get(:color)
|
30
|
+
end
|
31
|
+
|
32
|
+
def after_hook
|
33
|
+
after_hook_log << attribute_get(:color)
|
34
|
+
end
|
35
|
+
|
36
|
+
def log; @log ||= [] end
|
37
|
+
def before_hook_log; @bh_log ||= [] end
|
38
|
+
def after_hook_log; @ah_log ||= [] end
|
39
|
+
|
40
|
+
attr_reader :init
|
41
|
+
def initialize(*args)
|
42
|
+
(@init ||= []) << :init
|
43
|
+
super
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
TrafficLight.auto_migrate!
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "InvalidTransitions1" do
|
4
|
+
|
5
|
+
it "should get InvalidContext when requiring" do
|
6
|
+
lambda {
|
7
|
+
require 'examples/invalid_transitions_1'
|
8
|
+
}.should raise_error(DataMapper::Is::StateMachine::InvalidContext)
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "InvalidTransitions2" do
|
14
|
+
|
15
|
+
it "should get InvalidContext when requiring" do
|
16
|
+
lambda {
|
17
|
+
require 'examples/invalid_transitions_2'
|
18
|
+
}.should raise_error(DataMapper::Is::StateMachine::InvalidContext)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|