state_pattern 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,40 +2,69 @@
2
2
 
3
3
  A Ruby state pattern implementation.
4
4
 
5
+ == Example
6
+
7
+ Let's use one nice example from the AASM documentation and translate it to state_pattern (but keep in mind state_pattern is a generic gem, AASM is a Rails plugin):
8
+
5
9
  require 'rubygems'
6
10
  require 'state_pattern'
7
11
 
8
- class On < StatePattern::State
9
- def press
10
- transition_to(Off)
11
- "#{stateable.button_name} is off"
12
+ class Dating < StatePattern::State
13
+ def get_intimate
14
+ transition_to(Intimate) if stateable.drunk?
15
+ end
16
+
17
+ def get_married
18
+ transition_to(Married) if stateable.willing_to_give_up_manhood?
19
+ end
20
+
21
+ def enter
22
+ stateable.make_happy
23
+ end
24
+
25
+ def exit
26
+ stateable.make_depressed
12
27
  end
13
28
  end
14
-
15
- class Off < StatePattern::State
16
- def press
17
- transition_to(On)
18
- "#{stateable.button_name} is on"
29
+
30
+ class Intimate < StatePattern::State
31
+ def get_married
32
+ transition_to(Married) if stateable.willing_to_give_up_manhood?
33
+ end
34
+
35
+ def enter
36
+ stateable.make_very_happy
37
+ end
38
+
39
+ def exit
40
+ stateable.never_speak_again
19
41
  end
20
42
  end
21
-
22
- class Button
23
- include StatePattern
24
- set_initial_state Off
25
- valid_transitions [On, :press] => Off, [Off, :press] => On
26
-
27
- def button_name
28
- "Light button"
43
+
44
+ class Married < StatePattern::State
45
+ def enter
46
+ stateable.give_up_intimacy
47
+ end
48
+
49
+ def exit
50
+ stateable.buy_exotic_car_and_wear_a_combover
29
51
  end
30
52
  end
31
53
 
32
- button = Button.new
33
- puts button.press # => "Light button is on"
34
- puts button.press # => "Light button is off"
35
- puts button.press # => "Light button is on"
54
+ class Relationship
55
+ include StatePattern
56
+ set_initial_state Dating
57
+ valid_transitions [Dating, :get_intimate] => Intimate, [Dating, :get_married] => Married, [Intimate, :get_married] => Married
36
58
 
37
- == Installation
38
- sudo gem install state_pattern
59
+ def drunk?; @drunk; end
60
+ def willing_to_give_up_manhood?; @give_up_manhood; end
61
+ def make_happy; end
62
+ def make_depressed; end
63
+ def make_very_happy; end
64
+ def never_speak_again; end
65
+ def give_up_intimacy; end
66
+ def buy_exotic_car_and_wear_a_combover; end
67
+ end
39
68
 
40
69
  == Validations
41
70
 
@@ -53,6 +82,21 @@ With more than one target state
53
82
  Using event names to gain more detail
54
83
  valid_transitions [Up, :switch] => [Middle, Down], [Down, :switch] => Middle, [Middle, :switch] => Up
55
84
 
85
+ == Enter and exit hooks
86
+
87
+ Inside your state classes, any code that you put inside the enter method will be executed when the state is instantiated.
88
+ You can also use the exit hook which is triggered when a successfull transition to another state takes place.
89
+
90
+ == Querying
91
+
92
+ The state pattern is a very dynamic way of representing a state machine, very few things are hard-coded and everything can change on runtime.
93
+ This means that the only way (apart from parsing ruby code) to get a list of the state classes and events that are used, is inspecting the valid_transitions array.
94
+ So assuming that you completely draw your state machine with valid_transitions (which is always recommended) you can use the class methods state_classes and state_events to get a list of states and events respectively.
95
+
96
+ == Installation
97
+
98
+ sudo gem install state_pattern
99
+
56
100
  == Collaborate
57
101
 
58
102
  http://github.com/dcadenas/state_pattern
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.0
1
+ 1.2.0
@@ -7,87 +7,104 @@ module StatePattern
7
7
  end
8
8
 
9
9
  module ClassMethods
10
- def state_classes
11
- state_classes_in_transisions_hash = []
12
-
13
- if transitions_hash
14
- state_classes_in_transisions_hash = transitions_hash.map do |from, to|
15
- from_class = from.respond_to?(:to_ary) ? from.first : from
16
- to_classes = to.respond_to?(:to_ary) ? to : [to]
17
- to_classes + [from_class]
18
- end.flatten
19
- end
20
-
21
- state_classes_in_transisions_hash << initial_state_class
22
- state_classes_in_transisions_hash.uniq
23
- end
24
-
25
10
  def initial_state_class
26
11
  @initial_state_class
27
12
  end
28
13
 
29
14
  def set_initial_state(state_class)
30
15
  @initial_state_class = state_class
16
+ create_methods
17
+ end
18
+
19
+ def state_methods
20
+ state_classes_with_their_bases = state_classes.map{|c| c.ancestors.select{|a| a.ancestors.include?(StatePattern::State) && a != StatePattern::State}}.flatten.uniq
21
+ (state_classes_with_their_bases.map{|state_class| state_class.public_instance_methods(false)}.flatten.uniq - ["enter", "exit"]).map{|s| s.to_sym}
22
+ end
23
+
24
+ def state_events
25
+ transitions_hash.to_a.flatten.uniq.select{|t| t.respond_to?(:to_sym)}.map{|s| s.to_sym}
26
+ end
27
+
28
+ def state_classes
29
+ (transitions_hash.to_a << initial_state_class).flatten.uniq.select{|t| t.respond_to?(:ancestors) && t.ancestors.include?(StatePattern::State)}
31
30
  end
32
31
 
33
32
  def valid_transitions(transitions_hash)
34
33
  @transitions_hash = transitions_hash
35
34
  @transitions_hash.each do |key, value|
36
- if !value.respond_to?(:to_ary)
37
- @transitions_hash[key] = [value]
38
- end
35
+ @transitions_hash[key] = [value] if !value.respond_to?(:to_ary)
39
36
  end
37
+ create_methods
40
38
  end
41
39
 
42
40
  def transitions_hash
43
41
  @transitions_hash
44
42
  end
45
43
 
46
- def delegate_all_state_events
44
+ private
45
+ def create_methods
46
+ delegate_all_state_methods
47
+ create_query_methods
48
+ end
49
+
50
+ def delegate_all_state_methods
47
51
  state_methods.each do |state_method|
48
52
  define_method state_method do |*args|
49
- delegate_to_event(state_method)
50
- end
53
+ delegate_to_state(state_method, *args)
54
+ end unless respond_to?(state_method)
51
55
  end
52
56
  end
53
57
 
54
- def state_methods
55
- state_classes.map{|state_class| state_class.public_instance_methods(false)}.flatten.uniq
58
+ def create_query_methods
59
+ state_classes.each do |state_class|
60
+ method_name = "#{underscore(state_class.name.split("::").last)}?"
61
+ define_method method_name do
62
+ current_state_instance.class == state_class
63
+ end unless respond_to?(method_name)
64
+ end
56
65
  end
57
- end
58
66
 
59
- attr_accessor :current_state, :current_event
60
- def initialize(*args)
61
- super(*args)
62
- set_state(self.class.initial_state_class)
63
- self.class.delegate_all_state_events
67
+ def underscore(camel_cased_word)
68
+ camel_cased_word.to_s.gsub(/\W/, "_")
69
+ camel_cased_word.to_s.gsub(/::/, '/').
70
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
71
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
72
+ tr("-", "_").
73
+ downcase
74
+ end
64
75
  end
65
76
 
66
- def set_state(state_class)
67
- self.current_state = state_class.new(self)
77
+ def set_state(state_class = nil)
78
+ state_class ||= self.class.initial_state_class
79
+ return @current_state_instance if @current_state_instance.class == state_class
80
+ @current_state_instance = state_class.new(self, @current_state_instance)
68
81
  end
69
-
70
- def delegate_to_event(method_name, *args)
71
- self.current_event = method_name.to_sym
72
- self.current_state.send(current_event, *args)
82
+
83
+ def current_state_instance
84
+ set_state if @current_state_instance.nil?
85
+ @current_state_instance
73
86
  end
74
87
 
75
- def transition_to(state_class)
76
- raise InvalidTransitionException.new(self.current_state.class, state_class, self.current_event) unless self.valid_transition?(self.current_state.class, state_class)
77
- set_state(state_class)
88
+ def delegate_to_state(state_method_name, *args, &block)
89
+ @current_method = state_method_name.to_sym
90
+ self.current_state_instance.send(@current_method, *args, &block)
78
91
  end
79
92
 
80
- def valid_transition?(from_module, to_module)
81
- trans = self.class.transitions_hash
82
- return true if trans.nil?
83
-
84
- valid_transition_targets = trans[from_module] || trans[[from_module, current_event]]
85
- valid_transition_targets && valid_transition_targets.include?(to_module)
93
+ def transition_to(next_state_class)
94
+ raise_invalid_transition_to(next_state_class) unless valid_transition?(current_state_instance.class, next_state_class)
95
+ current_state_instance.exit
96
+ set_state(next_state_class)
86
97
  end
87
98
 
88
- def state
89
- self.current_state.state
99
+ def raise_invalid_transition_to(state_class)
100
+ raise InvalidTransitionException.new(current_state_instance.class, state_class, @current_method)
90
101
  end
91
- end
92
102
 
103
+ def valid_transition?(from_state, to_state)
104
+ transitions = self.class.transitions_hash
105
+ return true if transitions.nil?
93
106
 
107
+ valid_transition_targets = transitions[from_state] || transitions[[from_state, @current_method]]
108
+ valid_transition_targets && valid_transition_targets.include?(to_state)
109
+ end
110
+ end
@@ -1,16 +1,20 @@
1
1
  module StatePattern
2
2
  class State
3
- attr_reader :stateable
4
- def initialize(stateable)
3
+ attr_reader :stateable, :previous_state
4
+ def initialize(stateable, previous_state)
5
5
  @stateable = stateable
6
+ @previous_state = previous_state
7
+ enter
6
8
  end
7
9
 
8
10
  def transition_to(state_class)
9
11
  @stateable.transition_to(state_class)
10
12
  end
11
13
 
12
- def state
13
- self.class.to_s
14
+ def enter
15
+ end
16
+
17
+ def exit
14
18
  end
15
19
  end
16
20
  end
@@ -2,11 +2,11 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{state_pattern}
5
- s.version = "1.1.0"
5
+ s.version = "1.2.0"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Daniel Cadenas"]
9
- s.date = %q{2009-06-08}
9
+ s.date = %q{2009-10-12}
10
10
  s.email = %q{dcadenas@gmail.com}
11
11
  s.extra_rdoc_files = [
12
12
  "LICENSE",
@@ -23,6 +23,9 @@ Gem::Specification.new do |s|
23
23
  "lib/state_pattern/invalid_transition_exception.rb",
24
24
  "lib/state_pattern/state.rb",
25
25
  "state_pattern.gemspec",
26
+ "test/arguments_of_event_delegation_test.rb",
27
+ "test/hook_test.rb",
28
+ "test/querying_test.rb",
26
29
  "test/state_pattern_test.rb",
27
30
  "test/test_class_creation_helper.rb",
28
31
  "test/test_helper.rb",
@@ -35,7 +38,10 @@ Gem::Specification.new do |s|
35
38
  s.rubygems_version = %q{1.3.3}
36
39
  s.summary = %q{A Ruby state pattern implementation}
37
40
  s.test_files = [
38
- "test/state_pattern_test.rb",
41
+ "test/arguments_of_event_delegation_test.rb",
42
+ "test/hook_test.rb",
43
+ "test/querying_test.rb",
44
+ "test/state_pattern_test.rb",
39
45
  "test/test_class_creation_helper.rb",
40
46
  "test/test_helper.rb",
41
47
  "test/transition_validations_test.rb"
@@ -0,0 +1,44 @@
1
+ require 'test_helper'
2
+
3
+ class SampleClass
4
+ def event(arg1, arg2)
5
+ block_value = block_given? ? yield : nil
6
+ [arg1, arg2, block_value].compact.join(", ")
7
+ end
8
+ end
9
+
10
+ class SampleState < StatePattern::State
11
+ def event(arg1, arg2)
12
+ block_value = block_given? ? yield : nil
13
+ "state event args == " + [arg1, arg2, block_value].compact.join(", ")
14
+ end
15
+ end
16
+
17
+ class SampleClassWithStatePattern < SampleClass
18
+ include StatePattern
19
+ set_initial_state SampleState
20
+ end
21
+
22
+ Expectations do
23
+ expect "arg1, arg2" do
24
+ sample_class_instance = ::SampleClass.new
25
+ sample_class_instance.event("arg1", "arg2")
26
+ end
27
+
28
+ expect "state event args == arg1, arg2" do
29
+ sample_class_instance = ::SampleClassWithStatePattern.new
30
+ sample_class_instance.event("arg1", "arg2")
31
+ end
32
+
33
+ =begin
34
+ PENDING
35
+ Currently using class_eval to define the delegation method (so we can pass a block)
36
+ is causing a segmention fault on 1.8.6 so we only use define_method to do the delegation
37
+ if you want to pass a block do so as a normal variable, do use yield
38
+
39
+ expect "state event args == arg1, arg2, arg3" do
40
+ sample_class_instance = ::SampleClassWithStatePattern.new
41
+ sample_class_instance.event("arg1", "arg2"){"arg3"}
42
+ end
43
+ =end
44
+ end
@@ -0,0 +1,83 @@
1
+ require 'test_helper'
2
+
3
+ module Hooks
4
+ class On < StatePattern::State
5
+ def press
6
+ transition_to(Off)
7
+ stateable.messages << "#{stateable.button_name} is off"
8
+ end
9
+
10
+ def enter
11
+ stateable.messages << "Entered the On state"
12
+ end
13
+
14
+ def exit
15
+ stateable.messages << "Exited the On state"
16
+ end
17
+
18
+ private
19
+ def private_method
20
+ end
21
+ end
22
+
23
+ class Off < StatePattern::State
24
+ def press
25
+ transition_to(On)
26
+ stateable.messages << "#{stateable.button_name} is on"
27
+ end
28
+
29
+ def enter
30
+ stateable.messages << "Entered the Off state"
31
+ end
32
+
33
+ def exit
34
+ stateable.messages << "Exited the Off state"
35
+ end
36
+ end
37
+
38
+ class Button
39
+ include StatePattern
40
+ set_initial_state Off
41
+ valid_transitions [On, :press] => Off, [Off, :press] => On
42
+
43
+ def button_name
44
+ "Button"
45
+ end
46
+
47
+ def messages
48
+ @messages ||= []
49
+ end
50
+ end
51
+ end
52
+
53
+ Expectations do
54
+ expect ["Entered the Off state", "Exited the Off state", "Entered the On state", "Button is on"] do
55
+ button = Hooks::Button.new
56
+ button.press
57
+ button.messages
58
+ end
59
+
60
+ expect ["Entered the Off state", "Exited the Off state", "Entered the On state", "Button is on", "Exited the On state", "Entered the Off state", "Button is off"] do
61
+ button = Hooks::Button.new
62
+ button.press
63
+ button.press
64
+ button.messages
65
+ end
66
+
67
+ expect false do
68
+ Hooks::Button.new.respond_to?(:enter)
69
+ end
70
+
71
+ expect false do
72
+ Hooks::Button.new.respond_to?(:exit)
73
+ end
74
+
75
+ expect false do
76
+ Hooks::Button.new.respond_to?(:private_method)
77
+ end
78
+
79
+ expect true do
80
+ Hooks::Button.new.respond_to?(:press)
81
+ end
82
+ end
83
+
@@ -0,0 +1,87 @@
1
+ require 'test_helper'
2
+
3
+ module Querying
4
+ class StateBaseBase < StatePattern::State
5
+ def common_event
6
+ end
7
+ end
8
+
9
+ class StateBase < StateBaseBase
10
+ def another_common_event
11
+ end
12
+ end
13
+
14
+ class On < StateBase
15
+ def press
16
+ transition_to(Off)
17
+ end
18
+
19
+ def one_event
20
+ end
21
+
22
+ def another_event
23
+ end
24
+
25
+ def enter
26
+ end
27
+
28
+ def exit
29
+ end
30
+ private
31
+ def not_an_event
32
+ end
33
+ end
34
+
35
+ class Off < StateBase
36
+ def press
37
+ transition_to(On)
38
+ end
39
+
40
+ def one_event
41
+ end
42
+
43
+ def another_event
44
+ end
45
+ end
46
+
47
+ class Button
48
+ include StatePattern
49
+ set_initial_state On
50
+ valid_transitions [On, :press] => Off, [Off, :press] => On
51
+ end
52
+ end
53
+
54
+ Expectations do
55
+ expect %w(Querying::Off Querying::On) do
56
+ Querying::Button.state_classes.map{|c| c.name}.sort
57
+ end
58
+
59
+ expect ["another_common_event", "another_event", "common_event", "one_event", "press"] do
60
+ Querying::Button.state_methods.map{|s| s.to_s}.sort
61
+ end
62
+
63
+ expect [:press] do
64
+ Querying::Button.state_events
65
+ end
66
+
67
+ expect true do
68
+ Querying::Button.new.on?
69
+ end
70
+
71
+ expect false do
72
+ Querying::Button.new.off?
73
+ end
74
+
75
+ expect false do
76
+ button = Querying::Button.new
77
+ button.press
78
+ button.on?
79
+ end
80
+
81
+ expect true do
82
+ button = Querying::Button.new
83
+ button.press
84
+ button.off?
85
+ end
86
+ end
87
+
@@ -6,6 +6,9 @@ module Family
6
6
  transition_to(Lynn)
7
7
  "James #{stateable.last_name}"
8
8
  end
9
+
10
+ def james_method
11
+ end
9
12
  end
10
13
 
11
14
  class Lynn < StatePattern::State
@@ -22,7 +25,7 @@ module Family
22
25
 
23
26
  #notice this method is optional, it will be delegated automatically if removed
24
27
  def name
25
- delegate_to_event :name
28
+ delegate_to_state :name
26
29
  end
27
30
 
28
31
  def last_name
@@ -50,6 +53,10 @@ Expectations do
50
53
  member.name
51
54
  end
52
55
 
56
+ expect true do
57
+ Family::Member.new.respond_to?(:james_method)
58
+ end
59
+
53
60
  expect "on" do
54
61
  with_test_class("Button", :states => ["On", "Off"], :initial_state => "Off",
55
62
  :transitions => {["On", :press] => "Off", ["Off", :press] => "On"}) do
@@ -39,7 +39,7 @@ module TestClassCreationHelper
39
39
  begin
40
40
  yield
41
41
  ensure
42
- created_consts.each do |created_const|
42
+ created_consts.compact.each do |created_const|
43
43
  Object.send(:remove_const, created_const.to_s.to_sym)
44
44
  end
45
45
  end
@@ -8,6 +8,3 @@ require 'test_class_creation_helper'
8
8
 
9
9
  include TestClassCreationHelper
10
10
 
11
- class Test::Unit::TestCase
12
- end
13
-
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: state_pattern
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.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-08 00:00:00 -03:00
12
+ date: 2009-10-12 00:00:00 -02:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -33,6 +33,9 @@ files:
33
33
  - lib/state_pattern/invalid_transition_exception.rb
34
34
  - lib/state_pattern/state.rb
35
35
  - state_pattern.gemspec
36
+ - test/arguments_of_event_delegation_test.rb
37
+ - test/hook_test.rb
38
+ - test/querying_test.rb
36
39
  - test/state_pattern_test.rb
37
40
  - test/test_class_creation_helper.rb
38
41
  - test/test_helper.rb
@@ -66,6 +69,9 @@ signing_key:
66
69
  specification_version: 3
67
70
  summary: A Ruby state pattern implementation
68
71
  test_files:
72
+ - test/arguments_of_event_delegation_test.rb
73
+ - test/hook_test.rb
74
+ - test/querying_test.rb
69
75
  - test/state_pattern_test.rb
70
76
  - test/test_class_creation_helper.rb
71
77
  - test/test_helper.rb