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 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
- module James
6
+ require 'rubygems'
7
+ require 'state_pattern'
8
+
9
+ module On
7
10
  protected
8
11
 
9
- def name
10
- transition_to(Lynn)
11
- "James #{last_name}"
12
+ def press
13
+ transition_to(Off)
14
+ "#{button_name} is off"
12
15
  end
13
16
  end
14
17
 
15
- module Lynn
18
+ module Off
16
19
  protected
17
20
 
18
- def name
19
- transition_to(James)
20
- "Lynn #{last_name}"
21
+ def press
22
+ transition_to(On)
23
+ "#{button_name} is on"
21
24
  end
22
25
  end
23
26
 
24
- class FamilyMember
27
+ class Button
25
28
  include StatePattern
26
- add_states James, Lynn
27
- set_initial_state Lynn
28
-
29
- def name
30
- state_instance.name
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 last_name
34
- "Holbrook"
39
+ def button_name
40
+ "Light button"
35
41
  end
36
42
  end
37
43
 
38
- member = FamilyMember.new
39
- puts member.name # => "Lynn Holbrook"
40
- puts member.name # => "James Holbrook"
41
- puts member.name # => "Lynn Holbrook"
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
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.class_eval do
6
- attr_accessor :state
7
- def self.add_states(*state_classes)
8
- state_classes.each do |state_class|
9
- include state_class
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 self.set_initial_state(state_class)
14
- @@initial_state = state_class
34
+ def valid_transitions(transitions_hash)
35
+ @transitions_hash = transitions_hash
15
36
  end
16
37
 
17
- def initialize(*args)
18
- super(*args)
19
- self.state = @@initial_state
38
+ def transitions_hash
39
+ @transitions_hash
20
40
  end
21
41
 
22
- def state_instance
23
- as(state)
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 transition_to(state_module)
27
- self.state = state_module
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
 
@@ -2,10 +2,11 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{state_pattern}
5
- s.version = "0.1.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-05}
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/test_helper.rb"
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/test_helper.rb"
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
@@ -1,55 +1,94 @@
1
1
  require 'test_helper'
2
2
 
3
- module James
4
- protected
5
-
6
- def name
7
- transition_to(Lynn)
8
- "James #{last_name}"
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
- protected
14
-
15
- def name
16
- transition_to(James)
17
- "Lynn #{last_name}"
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 FamilyMember
22
- include StatePattern
23
- add_states James, Lynn
24
- set_initial_state Lynn
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
- def name
27
- state_instance.name
28
- end
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
- def last_name
31
- "Holbrook"
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 = FamilyMember.new
39
+ member = Family::Member.new
38
40
  member.name
39
41
  end
40
42
 
41
43
  expect "James Holbrook" do
42
- member = FamilyMember.new
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 = FamilyMember.new
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.1.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-05 00:00:00 -07:00
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