state_pattern 1.1.0 → 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.
@@ -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