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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +35 -0
  3. data/.travis.yml +11 -0
  4. data/Gemfile +65 -0
  5. data/LICENSE +20 -0
  6. data/README.rdoc +102 -0
  7. data/Rakefile +4 -0
  8. data/ardm-is-state_machine.gemspec +24 -0
  9. data/lib/ardm-is-state_machine.rb +1 -0
  10. data/lib/dm-is-state_machine.rb +10 -0
  11. data/lib/dm-is-state_machine/is/data/event.rb +25 -0
  12. data/lib/dm-is-state_machine/is/data/machine.rb +90 -0
  13. data/lib/dm-is-state_machine/is/data/state.rb +21 -0
  14. data/lib/dm-is-state_machine/is/dsl/event_dsl.rb +81 -0
  15. data/lib/dm-is-state_machine/is/dsl/state_dsl.rb +40 -0
  16. data/lib/dm-is-state_machine/is/state_machine.rb +139 -0
  17. data/lib/dm-is-state_machine/is/version.rb +7 -0
  18. data/spec/examples/invalid_events.rb +20 -0
  19. data/spec/examples/invalid_states.rb +20 -0
  20. data/spec/examples/invalid_transitions_1.rb +22 -0
  21. data/spec/examples/invalid_transitions_2.rb +22 -0
  22. data/spec/examples/light_switch.rb +25 -0
  23. data/spec/examples/slot_machine.rb +48 -0
  24. data/spec/examples/traffic_light.rb +48 -0
  25. data/spec/integration/inheritance_spec.rb +11 -0
  26. data/spec/integration/invalid_events_spec.rb +11 -0
  27. data/spec/integration/invalid_states_spec.rb +11 -0
  28. data/spec/integration/invalid_transitions_spec.rb +21 -0
  29. data/spec/integration/slot_machine_spec.rb +86 -0
  30. data/spec/integration/traffic_light_spec.rb +181 -0
  31. data/spec/rcov.opts +6 -0
  32. data/spec/spec.opts +4 -0
  33. data/spec/spec_helper.rb +11 -0
  34. data/spec/unit/data/event_spec.rb +27 -0
  35. data/spec/unit/data/machine_spec.rb +102 -0
  36. data/spec/unit/data/state_spec.rb +21 -0
  37. data/spec/unit/dsl/event_dsl_spec.rb +55 -0
  38. data/spec/unit/dsl/state_dsl_spec.rb +24 -0
  39. data/spec/unit/state_machine_spec.rb +28 -0
  40. data/tasks/spec.rake +38 -0
  41. data/tasks/yard.rake +9 -0
  42. data/tasks/yardstick.rake +19 -0
  43. 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,7 @@
1
+ module DataMapper
2
+ module Is
3
+ module StateMachine
4
+ VERSION = '1.2.0'
5
+ end
6
+ end
7
+ end
@@ -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,11 @@
1
+ require 'spec_helper'
2
+ require 'examples/light_switch'
3
+
4
+ describe 'Inherited state machine' do
5
+
6
+ it 'should not explode' do
7
+ dimmer = Dimmer.new
8
+ lambda { dimmer.switch! }.should_not raise_error(NoMethodError)
9
+ end
10
+
11
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe "InvalidEvents" do
4
+
5
+ it "should get InvalidContext when requiring" do
6
+ lambda {
7
+ require 'examples/invalid_events'
8
+ }.should raise_error(DataMapper::Is::StateMachine::InvalidContext)
9
+ end
10
+
11
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe "InvalidStates" do
4
+
5
+ it "should get InvalidContext when requiring" do
6
+ lambda {
7
+ require 'examples/invalid_states'
8
+ }.should raise_error(DataMapper::Is::StateMachine::InvalidContext)
9
+ end
10
+
11
+ end
@@ -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