dcadenas-state_pattern 0.1.0 → 0.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.
- data/README.rdoc +26 -20
- data/VERSION +1 -1
- data/lib/state_pattern.rb +74 -14
- data/state_pattern.gemspec +9 -4
- data/test/state_pattern_test.rb +65 -26
- data/test/test_class_creation_helper.rb +58 -0
- data/test/test_helper.rb +4 -0
- data/test/transition_validations_test.rb +43 -0
- metadata +6 -2
data/README.rdoc
CHANGED
@@ -3,42 +3,48 @@
|
|
3
3
|
A Ruby state pattern implementation.
|
4
4
|
The idea comes from this nice Jay Field's post http://blog.jayfields.com/2007/08/ruby-state-pattern-using-modules-and.html
|
5
5
|
|
6
|
-
|
6
|
+
require 'rubygems'
|
7
|
+
require 'state_pattern'
|
8
|
+
|
9
|
+
module On
|
7
10
|
protected
|
8
11
|
|
9
|
-
def
|
10
|
-
transition_to(
|
11
|
-
"
|
12
|
+
def press
|
13
|
+
transition_to(Off)
|
14
|
+
"#{button_name} is off"
|
12
15
|
end
|
13
16
|
end
|
14
17
|
|
15
|
-
module
|
18
|
+
module Off
|
16
19
|
protected
|
17
20
|
|
18
|
-
def
|
19
|
-
transition_to(
|
20
|
-
"
|
21
|
+
def press
|
22
|
+
transition_to(On)
|
23
|
+
"#{button_name} is on"
|
21
24
|
end
|
22
25
|
end
|
23
26
|
|
24
|
-
class
|
27
|
+
class Button
|
25
28
|
include StatePattern
|
26
|
-
add_states
|
27
|
-
set_initial_state
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
add_states On, Off
|
30
|
+
set_initial_state Off
|
31
|
+
valid_transitions [On, :press] => Off, [Off, :press] => On
|
32
|
+
|
33
|
+
#this method can be removed as it will be mapped automatically anyways
|
34
|
+
#but is good to leave the option to do the delegation yourself in case you want to do more things
|
35
|
+
def press
|
36
|
+
delegate_to_event(:press)
|
31
37
|
end
|
32
38
|
|
33
|
-
def
|
34
|
-
"
|
39
|
+
def button_name
|
40
|
+
"Light button"
|
35
41
|
end
|
36
42
|
end
|
37
43
|
|
38
|
-
|
39
|
-
puts
|
40
|
-
puts
|
41
|
-
puts
|
44
|
+
button = Button.new
|
45
|
+
puts button.press # => "Light button is on"
|
46
|
+
puts button.press # => "Light button is off"
|
47
|
+
puts button.press # => "Light button is on"
|
42
48
|
|
43
49
|
== Requirements
|
44
50
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
data/lib/state_pattern.rb
CHANGED
@@ -1,33 +1,93 @@
|
|
1
1
|
require 'facets'
|
2
2
|
|
3
3
|
module StatePattern
|
4
|
+
class InvalidTransitionException < RuntimeError
|
5
|
+
attr_reader :from_module, :to_module
|
6
|
+
def initialize(from_module, to_module)
|
7
|
+
@from_module = from_module
|
8
|
+
@to_module = to_module
|
9
|
+
end
|
10
|
+
|
11
|
+
def message
|
12
|
+
"Cannot transition from #{@from_module} to #{@to_module}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
4
16
|
def self.included(base)
|
5
|
-
base.
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
17
|
+
base.instance_eval do
|
18
|
+
def initial_state
|
19
|
+
@initial_state
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_initial_state(state_module)
|
23
|
+
@initial_state = state_module
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_states(*state_modules)
|
27
|
+
@state_modules = state_modules
|
28
|
+
@state_modules.each do |state_module|
|
29
|
+
include state_module
|
10
30
|
end
|
31
|
+
delegate_all_events
|
11
32
|
end
|
12
33
|
|
13
|
-
def
|
14
|
-
|
34
|
+
def valid_transitions(transitions_hash)
|
35
|
+
@transitions_hash = transitions_hash
|
15
36
|
end
|
16
37
|
|
17
|
-
def
|
18
|
-
|
19
|
-
self.state = @@initial_state
|
38
|
+
def transitions_hash
|
39
|
+
@transitions_hash
|
20
40
|
end
|
21
41
|
|
22
|
-
def
|
23
|
-
|
42
|
+
def delegate_all_events
|
43
|
+
state_methods.each do |method_name|
|
44
|
+
define_method method_name do |*args|
|
45
|
+
delegate_to_event(method_name, *args)
|
46
|
+
end
|
47
|
+
end
|
24
48
|
end
|
25
49
|
|
26
|
-
def
|
27
|
-
|
50
|
+
def state_methods
|
51
|
+
@state_modules.map{|state_module| state_module.__send__(:instance_methods)}.flatten.uniq
|
28
52
|
end
|
29
53
|
end
|
30
54
|
end
|
55
|
+
|
56
|
+
attr_accessor :current_state_module, :current_event
|
57
|
+
def initialize(*args)
|
58
|
+
super(*args)
|
59
|
+
self.current_state_module = self.class.initial_state
|
60
|
+
end
|
61
|
+
|
62
|
+
def current_state_instance
|
63
|
+
as(current_state_module)
|
64
|
+
end
|
65
|
+
|
66
|
+
def transition_to(state_module)
|
67
|
+
raise InvalidTransitionException.new(self.current_state_module, state_module) unless self.valid_transition?(self.current_state_module, state_module)
|
68
|
+
self.current_state_module = state_module
|
69
|
+
end
|
70
|
+
|
71
|
+
def valid_transition?(from_module, to_module)
|
72
|
+
trans = self.class.transitions_hash
|
73
|
+
return true if trans.nil?
|
74
|
+
|
75
|
+
#TODO: ugly
|
76
|
+
trans.has_key?(from_module) &&
|
77
|
+
(trans[from_module] == to_module ||
|
78
|
+
trans[from_module].include?(to_module)) ||
|
79
|
+
trans.has_key?([from_module, current_event]) &&
|
80
|
+
(trans[[from_module, current_event]] == to_module || trans[[from_module, current_event]].include?(to_module))
|
81
|
+
end
|
82
|
+
|
83
|
+
def state
|
84
|
+
self.current_state_module.to_s
|
85
|
+
end
|
86
|
+
|
87
|
+
def delegate_to_event(method_name, *args)
|
88
|
+
self.current_event = method_name.to_sym
|
89
|
+
self.current_state_instance.__send__(current_event, *args)
|
90
|
+
end
|
31
91
|
end
|
32
92
|
|
33
93
|
|
data/state_pattern.gemspec
CHANGED
@@ -2,10 +2,11 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = %q{state_pattern}
|
5
|
-
s.version = "0.
|
5
|
+
s.version = "0.2.0"
|
6
|
+
|
6
7
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
7
8
|
s.authors = ["Daniel Cadenas"]
|
8
|
-
s.date = %q{2009-06-
|
9
|
+
s.date = %q{2009-06-07}
|
9
10
|
s.email = %q{dcadenas@gmail.com}
|
10
11
|
s.extra_rdoc_files = [
|
11
12
|
"LICENSE",
|
@@ -21,7 +22,9 @@ Gem::Specification.new do |s|
|
|
21
22
|
"lib/state_pattern.rb",
|
22
23
|
"state_pattern.gemspec",
|
23
24
|
"test/state_pattern_test.rb",
|
24
|
-
"test/
|
25
|
+
"test/test_class_creation_helper.rb",
|
26
|
+
"test/test_helper.rb",
|
27
|
+
"test/transition_validations_test.rb"
|
25
28
|
]
|
26
29
|
s.homepage = %q{http://github.com/dcadenas/state_pattern}
|
27
30
|
s.rdoc_options = ["--charset=UTF-8"]
|
@@ -30,7 +33,9 @@ Gem::Specification.new do |s|
|
|
30
33
|
s.summary = %q{A Ruby state pattern implementation}
|
31
34
|
s.test_files = [
|
32
35
|
"test/state_pattern_test.rb",
|
33
|
-
"test/
|
36
|
+
"test/test_class_creation_helper.rb",
|
37
|
+
"test/test_helper.rb",
|
38
|
+
"test/transition_validations_test.rb"
|
34
39
|
]
|
35
40
|
|
36
41
|
if s.respond_to? :specification_version then
|
data/test/state_pattern_test.rb
CHANGED
@@ -1,55 +1,94 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
|
-
module
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
module Family
|
4
|
+
module James
|
5
|
+
protected
|
6
|
+
def name
|
7
|
+
transition_to(Lynn)
|
8
|
+
"James #{last_name}"
|
9
|
+
end
|
9
10
|
end
|
10
|
-
end
|
11
11
|
|
12
|
-
module Lynn
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
12
|
+
module Lynn
|
13
|
+
protected
|
14
|
+
def name
|
15
|
+
transition_to(James)
|
16
|
+
"Lynn #{last_name}"
|
17
|
+
end
|
18
18
|
end
|
19
|
-
end
|
20
19
|
|
21
|
-
class
|
22
|
-
|
23
|
-
|
24
|
-
|
20
|
+
class Member
|
21
|
+
include StatePattern
|
22
|
+
add_states James, Lynn
|
23
|
+
set_initial_state Lynn
|
24
|
+
valid_transitions [James, :name] => Lynn, [Lynn, :name] => James
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
|
26
|
+
#notice this method is optional, it will be delegated automatically if removed
|
27
|
+
def name
|
28
|
+
delegate_to_event :name
|
29
|
+
end
|
29
30
|
|
30
|
-
|
31
|
-
|
31
|
+
def last_name
|
32
|
+
"Holbrook"
|
33
|
+
end
|
32
34
|
end
|
33
35
|
end
|
34
36
|
|
35
37
|
Expectations do
|
36
38
|
expect "Lynn Holbrook" do
|
37
|
-
member =
|
39
|
+
member = Family::Member.new
|
38
40
|
member.name
|
39
41
|
end
|
40
42
|
|
41
43
|
expect "James Holbrook" do
|
42
|
-
member =
|
44
|
+
member = Family::Member.new
|
43
45
|
member.name
|
44
46
|
member.name
|
45
47
|
end
|
46
48
|
|
47
49
|
expect "Lynn Holbrook" do
|
48
|
-
member =
|
50
|
+
member = Family::Member.new
|
49
51
|
member.name
|
50
52
|
member.name
|
51
53
|
member.name
|
52
54
|
end
|
55
|
+
|
56
|
+
expect "on" do
|
57
|
+
with_test_class("Button", :states => ["On", "Off"], :initial_state => "Off",
|
58
|
+
:transitions => {["On", :press] => "Off", ["Off", :press] => "On"}) do
|
59
|
+
button = Button.new
|
60
|
+
button.press
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
expect "off" do
|
65
|
+
with_test_class("Button", :states => ["On", "Off"], :initial_state => "Off",
|
66
|
+
:transitions => {["On", :press] => "Off", ["Off", :press] => "On"}) do
|
67
|
+
button = Button.new
|
68
|
+
button.press
|
69
|
+
button.press
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
expect "on" do
|
74
|
+
with_test_class("Button", :states => ["On", "Off"], :initial_state => "Off",
|
75
|
+
:transitions => {["On", :press] => "Off", ["Off", :press] => "On"}) do
|
76
|
+
button = Button.new
|
77
|
+
button.press
|
78
|
+
button.press
|
79
|
+
button.press
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
expect "on" do
|
84
|
+
with_test_class("Button", :states => ["On", "Off"], :initial_state => "Off",
|
85
|
+
:transitions => {["On", :press] => "Off", ["Off", :press] => "On"}) do
|
86
|
+
button1 = Button.new
|
87
|
+
button2 = Button.new
|
88
|
+
button1.press
|
89
|
+
button2.press
|
90
|
+
end
|
91
|
+
end
|
53
92
|
end
|
54
93
|
|
55
94
|
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module TestClassCreationHelper
|
2
|
+
def with_test_class(main_state_module_name, options = {})
|
3
|
+
created_consts = []
|
4
|
+
transitions = options[:transitions] || {}
|
5
|
+
state_methods = transitions.keys.map{|t| t.last}.uniq || []
|
6
|
+
|
7
|
+
if options.has_key?(:states)
|
8
|
+
options[:states].each do |state_name|
|
9
|
+
created_consts << create_module(state_name) do
|
10
|
+
state_methods.each do |method_name|
|
11
|
+
define_method method_name do
|
12
|
+
next_state_name = transitions[[state_name, method_name]]
|
13
|
+
next_state_module = next_state_name && Object.const_get(next_state_name)
|
14
|
+
transition_to next_state_module
|
15
|
+
next_state_name.downcase
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
created_consts << create_class(main_state_module_name) do
|
23
|
+
include StatePattern
|
24
|
+
add_states *options[:states].map{|s| Object.const_get(s)} if options.has_key?(:states)
|
25
|
+
set_initial_state Object.const_get(options[:initial_state]) if options.has_key?(:initial_state)
|
26
|
+
if options.has_key?(:valid_transitions)
|
27
|
+
valid_transitions_with_constants = {}
|
28
|
+
options[:valid_transitions].each do |from_module_string_or_array, to_module_string|
|
29
|
+
if from_module_string_or_array.respond_to?(:to_ary)
|
30
|
+
valid_transitions_with_constants[[Object.const_get(from_module_string_or_array.first), from_module_string_or_array.last]] = Object.const_get(to_module_string)
|
31
|
+
else
|
32
|
+
valid_transitions_with_constants[Object.const_get(from_module_string_or_array)] = Object.const_get(to_module_string)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
valid_transitions valid_transitions_with_constants
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
begin
|
40
|
+
yield
|
41
|
+
ensure
|
42
|
+
created_consts.each do |created_const|
|
43
|
+
Object.send(:remove_const, created_const.to_s.to_sym)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def create_module(module_name, module_or_class = Module, &block)
|
51
|
+
new_module = module_or_class.new &block
|
52
|
+
Object.const_set(module_name, new_module) unless Object.const_defined? module_name
|
53
|
+
end
|
54
|
+
|
55
|
+
def create_class(class_name, &block)
|
56
|
+
create_module(class_name, Class, &block)
|
57
|
+
end
|
58
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -4,6 +4,10 @@ require 'expectations'
|
|
4
4
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
5
5
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
6
6
|
require 'state_pattern'
|
7
|
+
require 'test_class_creation_helper'
|
8
|
+
|
9
|
+
include TestClassCreationHelper
|
7
10
|
|
8
11
|
class Test::Unit::TestCase
|
9
12
|
end
|
13
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
Expectations do
|
4
|
+
expect "up" do
|
5
|
+
with_test_class("Switch", :states => ["Up", "Down", "Middle"], :initial_state => "Middle",
|
6
|
+
:transitions => {["Up", :push_down] => "Middle",
|
7
|
+
["Down", :push_up] => "Middle",
|
8
|
+
["Middle", :push_up] => "Up",
|
9
|
+
["Middle", :push_down] => "Down"}) do
|
10
|
+
switch = Switch.new
|
11
|
+
switch.push_up
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
expect "up" do
|
16
|
+
with_test_class("Switch", :states => ["Up", "Down", "Middle"], :initial_state => "Middle",
|
17
|
+
:transitions => {["Up", :push_down] => "Middle",
|
18
|
+
["Down", :push_up] => "Middle",
|
19
|
+
["Middle", :push_up] => "Up",
|
20
|
+
["Middle", :push_down] => "Down"},
|
21
|
+
:valid_transitions => {["Up", :push_down] => "Middle",
|
22
|
+
["Down", :push_up] => "Middle",
|
23
|
+
["Middle", :push_up] => "Up",
|
24
|
+
["Middle", :push_down] => "Down"}) do
|
25
|
+
switch = Switch.new
|
26
|
+
switch.push_up
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
expect StatePattern::InvalidTransitionException do
|
31
|
+
with_test_class("Switch", :states => ["Up", "Down", "Middle"], :initial_state => "Middle",
|
32
|
+
:transitions => {["Up", :push_down] => "Middle",
|
33
|
+
["Down", :push_up] => "Middle",
|
34
|
+
["Middle", :push_up] => "Up",
|
35
|
+
["Middle", :push_down] => "Down"},
|
36
|
+
:valid_transitions => {["Up", :push_down] => "Middle",
|
37
|
+
["Down", :push_up] => "Middle",
|
38
|
+
["Middle", :push_down] => "Down"}) do
|
39
|
+
switch = Switch.new
|
40
|
+
switch.push_up
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dcadenas-state_pattern
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Cadenas
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-06-
|
12
|
+
date: 2009-06-07 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -32,7 +32,9 @@ files:
|
|
32
32
|
- lib/state_pattern.rb
|
33
33
|
- state_pattern.gemspec
|
34
34
|
- test/state_pattern_test.rb
|
35
|
+
- test/test_class_creation_helper.rb
|
35
36
|
- test/test_helper.rb
|
37
|
+
- test/transition_validations_test.rb
|
36
38
|
has_rdoc: false
|
37
39
|
homepage: http://github.com/dcadenas/state_pattern
|
38
40
|
post_install_message:
|
@@ -61,4 +63,6 @@ specification_version: 3
|
|
61
63
|
summary: A Ruby state pattern implementation
|
62
64
|
test_files:
|
63
65
|
- test/state_pattern_test.rb
|
66
|
+
- test/test_class_creation_helper.rb
|
64
67
|
- test/test_helper.rb
|
68
|
+
- test/transition_validations_test.rb
|