state_pattern 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Daniel Cadenas
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,62 @@
1
+ = state_pattern
2
+
3
+ A Ruby state pattern implementation.
4
+
5
+ require 'rubygems'
6
+ require 'state_pattern'
7
+
8
+ class On < StatePattern::State
9
+ def press
10
+ transition_to(Off)
11
+ "#{stateable.button_name} is off"
12
+ end
13
+ end
14
+
15
+ class Off < StatePattern::State
16
+ def press
17
+ transition_to(On)
18
+ "#{stateable.button_name} is on"
19
+ end
20
+ end
21
+
22
+ class Button
23
+ include StatePattern
24
+ add_states On, Off
25
+ set_initial_state Off
26
+ valid_transitions [On, :press] => Off, [Off, :press] => On
27
+
28
+ #this method can be removed as it will be mapped automatically anyways
29
+ #but it is good to leave the option to do the delegation yourself in case you want more freedom
30
+ def press
31
+ delegate_to_event(:press)
32
+ end
33
+
34
+ def button_name
35
+ "Light button"
36
+ end
37
+ end
38
+
39
+ button = Button.new
40
+ puts button.press # => "Light button is on"
41
+ puts button.press # => "Light button is off"
42
+ puts button.press # => "Light button is on"
43
+
44
+ == Validations
45
+
46
+ One of the few drawbacks the state pattern has is that it can get difficult to see the global picture of your state machine when dealing with complex cases.
47
+ To deal with this problem you have the option of using the valid_transitions statement to "draw" your state diagram in code. Whenever a state transition is performed, the valid_transitions hash is checked and if the transition is not valid a StatePattern::InvalidTransitionException is thrown.
48
+
49
+ Examples:
50
+
51
+ The most basic notation
52
+ valid_transitions On => Off, Off => On
53
+
54
+ With more than one target state
55
+ valid_transitions Up => [Middle, Down], Down => Middle, Middle => Up
56
+
57
+ Using event names to gain more detail
58
+ valid_transitions [Up, :switch] => [Middle, Down], [Down, :switch] => Middle, [Middle, :switch] => Up
59
+
60
+ == Copyright
61
+
62
+ Copyright (c) 2009 Daniel Cadenas. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,83 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "state_pattern"
8
+ gem.summary = %Q{A Ruby state pattern implementation}
9
+ gem.email = "dcadenas@gmail.com"
10
+ gem.homepage = "http://github.com/dcadenas/state_pattern"
11
+ gem.authors = ["Daniel Cadenas"]
12
+ gem.rubyforge_project = "statepattern"
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
18
+ end
19
+
20
+ require 'rake/testtask'
21
+ Rake::TestTask.new(:test) do |test|
22
+ test.libs << 'lib' << 'test'
23
+ test.pattern = 'test/**/*_test.rb'
24
+ test.verbose = true
25
+ end
26
+
27
+ begin
28
+ require 'rcov/rcovtask'
29
+ Rcov::RcovTask.new do |test|
30
+ test.libs << 'test'
31
+ test.pattern = 'test/**/*_test.rb'
32
+ test.verbose = true
33
+ end
34
+ rescue LoadError
35
+ task :rcov do
36
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
37
+ end
38
+ end
39
+
40
+
41
+ task :default => :test
42
+
43
+ require 'rake/rdoctask'
44
+ Rake::RDocTask.new do |rdoc|
45
+ if File.exist?('VERSION.yml')
46
+ config = YAML.load(File.read('VERSION.yml'))
47
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
48
+ else
49
+ version = ""
50
+ end
51
+
52
+ rdoc.rdoc_dir = 'rdoc'
53
+ rdoc.title = "state_pattern #{version}"
54
+ rdoc.rdoc_files.include('README*')
55
+ rdoc.rdoc_files.include('lib/**/*.rb')
56
+ end
57
+
58
+ begin
59
+ require 'rake/contrib/sshpublisher'
60
+ namespace :rubyforge do
61
+
62
+ desc "Release gem and RDoc documentation to RubyForge"
63
+ task :release => ["rubyforge:release:gem", "rubyforge:release:docs"]
64
+
65
+ namespace :release do
66
+ desc "Publish RDoc to RubyForge."
67
+ task :docs => [:rdoc] do
68
+ config = YAML.load(
69
+ File.read(File.expand_path('~/.rubyforge/user-config.yml'))
70
+ )
71
+
72
+ host = "#{config['username']}@rubyforge.org"
73
+ remote_dir = "/var/www/gforge-projects/state_pattern/"
74
+ local_dir = 'rdoc'
75
+
76
+ Rake::SshDirPublisher.new(host, remote_dir, local_dir).upload
77
+ end
78
+ end
79
+ end
80
+ rescue LoadError
81
+ puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
82
+ end
83
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.1
@@ -0,0 +1,92 @@
1
+ require 'state_pattern/state'
2
+ require 'state_pattern/invalid_transition_exception'
3
+
4
+ module StatePattern
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ def state_classes
11
+ @state_classes ||= []
12
+ end
13
+
14
+ def initial_state_class
15
+ @initial_state_class
16
+ end
17
+
18
+ def set_initial_state(state_class)
19
+ @initial_state_class = state_class
20
+ end
21
+
22
+ def add_states(*state_classes)
23
+ state_classes.each do |state_class|
24
+ add_state_class(state_class)
25
+ end
26
+ end
27
+
28
+ def add_state_class(state_class)
29
+ state_classes << state_class
30
+ end
31
+
32
+ def valid_transitions(transitions_hash)
33
+ @transitions_hash = transitions_hash
34
+ @transitions_hash.each do |key, value|
35
+ if !value.respond_to?(:to_ary)
36
+ @transitions_hash[key] = [value]
37
+ end
38
+ end
39
+ end
40
+
41
+ def transitions_hash
42
+ @transitions_hash
43
+ end
44
+
45
+ def delegate_all_state_events
46
+ state_methods.each do |state_method|
47
+ define_method state_method do |*args|
48
+ delegate_to_event(state_method)
49
+ end
50
+ end
51
+ end
52
+
53
+ def state_methods
54
+ state_classes.map{|state_class| state_class.public_instance_methods(false)}.flatten.uniq
55
+ end
56
+ end
57
+
58
+ attr_accessor :current_state, :current_event
59
+ def initialize(*args)
60
+ super(*args)
61
+ set_state(self.class.initial_state_class)
62
+ self.class.delegate_all_state_events
63
+ end
64
+
65
+ def set_state(state_class)
66
+ self.current_state = state_class.new(self)
67
+ end
68
+
69
+ def delegate_to_event(method_name, *args)
70
+ self.current_event = method_name.to_sym
71
+ self.current_state.send(current_event, *args)
72
+ end
73
+
74
+ def transition_to(state_class)
75
+ raise InvalidTransitionException.new(self.current_state.class, state_class, self.current_event) unless self.valid_transition?(self.current_state.class, state_class)
76
+ set_state(state_class)
77
+ end
78
+
79
+ def valid_transition?(from_module, to_module)
80
+ trans = self.class.transitions_hash
81
+ return true if trans.nil?
82
+
83
+ valid_transition_targets = trans[from_module] || trans[[from_module, current_event]]
84
+ valid_transition_targets && valid_transition_targets.include?(to_module)
85
+ end
86
+
87
+ def state
88
+ self.current_state.state
89
+ end
90
+ end
91
+
92
+
@@ -0,0 +1,14 @@
1
+ module StatePattern
2
+ class InvalidTransitionException < RuntimeError
3
+ attr_reader :from_module, :to_module, :event
4
+ def initialize(from_module, to_module, event)
5
+ @from_module = from_module
6
+ @to_module = to_module
7
+ @event = event
8
+ end
9
+
10
+ def message
11
+ "Event #@event cannot transition from #@from_module to #@to_module"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ module StatePattern
2
+ class State
3
+ attr_reader :stateable
4
+ def initialize(stateable)
5
+ @stateable = stateable
6
+ end
7
+
8
+ def transition_to(state_class)
9
+ @stateable.transition_to(state_class)
10
+ end
11
+
12
+ def state
13
+ self.class.to_s
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,53 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{state_pattern}
5
+ s.version = "1.0.1"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Daniel Cadenas"]
9
+ s.date = %q{2009-06-08}
10
+ s.email = %q{dcadenas@gmail.com}
11
+ s.extra_rdoc_files = [
12
+ "LICENSE",
13
+ "README.rdoc"
14
+ ]
15
+ s.files = [
16
+ ".document",
17
+ ".gitignore",
18
+ "LICENSE",
19
+ "README.rdoc",
20
+ "Rakefile",
21
+ "VERSION",
22
+ "lib/state_pattern.rb",
23
+ "lib/state_pattern/invalid_transition_exception.rb",
24
+ "lib/state_pattern/state.rb",
25
+ "state_pattern.gemspec",
26
+ "test/state_pattern_test.rb",
27
+ "test/test_class_creation_helper.rb",
28
+ "test/test_helper.rb",
29
+ "test/transition_validations_test.rb"
30
+ ]
31
+ s.homepage = %q{http://github.com/dcadenas/state_pattern}
32
+ s.rdoc_options = ["--charset=UTF-8"]
33
+ s.require_paths = ["lib"]
34
+ s.rubyforge_project = %q{statepattern}
35
+ s.rubygems_version = %q{1.3.3}
36
+ s.summary = %q{A Ruby state pattern implementation}
37
+ s.test_files = [
38
+ "test/state_pattern_test.rb",
39
+ "test/test_class_creation_helper.rb",
40
+ "test/test_helper.rb",
41
+ "test/transition_validations_test.rb"
42
+ ]
43
+
44
+ if s.respond_to? :specification_version then
45
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
46
+ s.specification_version = 3
47
+
48
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
49
+ else
50
+ end
51
+ else
52
+ end
53
+ end
@@ -0,0 +1,102 @@
1
+ require 'test_helper'
2
+
3
+ module Family
4
+ class James < StatePattern::State
5
+ def name
6
+ transition_to(Lynn)
7
+ "James #{stateable.last_name}"
8
+ end
9
+ end
10
+
11
+ class Lynn < StatePattern::State
12
+ def name
13
+ transition_to(James)
14
+ "Lynn #{stateable.last_name}"
15
+ end
16
+ end
17
+
18
+ class Member
19
+ include StatePattern
20
+ add_states James, Lynn
21
+ set_initial_state Lynn
22
+ valid_transitions [James, :name] => Lynn, [Lynn, :name] => James
23
+
24
+ #notice this method is optional, it will be delegated automatically if removed
25
+ def name
26
+ delegate_to_event :name
27
+ end
28
+
29
+ def last_name
30
+ "Holbrook"
31
+ end
32
+ end
33
+ end
34
+
35
+ Expectations do
36
+ expect "Lynn Holbrook" do
37
+ member = Family::Member.new
38
+ member.name
39
+ end
40
+
41
+ expect "James Holbrook" do
42
+ member = Family::Member.new
43
+ member.name
44
+ member.name
45
+ end
46
+
47
+ expect "Lynn Holbrook" do
48
+ member = Family::Member.new
49
+ member.name
50
+ member.name
51
+ member.name
52
+ end
53
+
54
+ expect "on" do
55
+ with_test_class("Button", :states => ["On", "Off"], :initial_state => "Off",
56
+ :transitions => {["On", :press] => "Off", ["Off", :press] => "On"}) do
57
+ button = Button.new
58
+ button.press
59
+ end
60
+ end
61
+
62
+ expect "off" do
63
+ with_test_class("Button", :states => ["On", "Off"], :initial_state => "Off",
64
+ :transitions => {["On", :press] => "Off", ["Off", :press] => "On"}) do
65
+ button = Button.new
66
+ button.press
67
+ button.press
68
+ end
69
+ end
70
+
71
+ expect "on" do
72
+ with_test_class("Button", :states => ["On", "Off"], :initial_state => "Off",
73
+ :transitions => {["On", :press] => "Off", ["Off", :press] => "On"}) do
74
+ button = Button.new
75
+ button.press
76
+ button.press
77
+ button.press
78
+ end
79
+ end
80
+
81
+ expect "on" do
82
+ with_test_class("Button", :states => ["On", "Off"], :initial_state => "Off",
83
+ :transitions => {["On", :press] => "Off", ["Off", :press] => "On"}) do
84
+ button1 = Button.new
85
+ button2 = Button.new
86
+ button1.press
87
+ button2.press
88
+ end
89
+ end
90
+
91
+ expect ["ping", "on", "pong", "off"] do
92
+ with_test_class("PingPong", :states => ["Ping", "Pong"], :initial_state => "Pong", :transitions => {["Ping", :do_it] => "Pong", ["Pong", :do_it] => "Ping"}) do
93
+ with_test_class("Button", :states => ["On", "Off"], :initial_state => "Off", :transitions => {["On", :press] => "Off", ["Off", :press] => "On"}) do
94
+ pingpong = PingPong.new
95
+ button = Button.new
96
+ [pingpong.do_it, button.press, pingpong.do_it, button.press]
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+
@@ -0,0 +1,59 @@
1
+ module TestClassCreationHelper
2
+ #TODO: ugly
3
+ def with_test_class(main_state_module_name, options = {})
4
+ created_consts = []
5
+ transitions = options[:transitions] || {}
6
+ state_methods = transitions.keys.map{|t| t.last}.uniq || []
7
+
8
+ if options.has_key?(:states)
9
+ options[:states].each do |state_name|
10
+ created_consts << create_class(state_name, StatePattern::State) do
11
+ state_methods.each do |method_name|
12
+ define_method method_name do
13
+ next_state_name = transitions[[state_name, method_name]]
14
+ next_state_module = next_state_name && Object.const_get(next_state_name)
15
+ transition_to next_state_module
16
+ next_state_name.downcase
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ created_consts << create_class(main_state_module_name) do
24
+ include StatePattern
25
+ add_states *options[:states].map{|s| Object.const_get(s)} if options.has_key?(:states)
26
+ set_initial_state Object.const_get(options[:initial_state]) if options.has_key?(:initial_state)
27
+ if options.has_key?(:valid_transitions)
28
+ valid_transitions_with_constants = {}
29
+ options[:valid_transitions].each do |from_module_string_or_array, to_module_string|
30
+ if from_module_string_or_array.respond_to?(:to_ary)
31
+ 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)
32
+ else
33
+ valid_transitions_with_constants[Object.const_get(from_module_string_or_array)] = Object.const_get(to_module_string)
34
+ end
35
+ end
36
+ valid_transitions valid_transitions_with_constants
37
+ end
38
+ end
39
+
40
+ begin
41
+ yield
42
+ ensure
43
+ created_consts.each do |created_const|
44
+ Object.send(:remove_const, created_const.to_s.to_sym)
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def create_module(module_name, superklass = Object, module_or_class = Module, &block)
52
+ new_module = module_or_class.new(superklass, &block)
53
+ Object.const_set(module_name, new_module) unless Object.const_defined? module_name
54
+ end
55
+
56
+ def create_class(class_name, superklass = Object, &block)
57
+ create_module(class_name, superklass, Class, &block)
58
+ end
59
+ end
@@ -0,0 +1,13 @@
1
+ require 'rubygems'
2
+ require 'expectations'
3
+
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
5
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
6
+ require 'state_pattern'
7
+ require 'test_class_creation_helper'
8
+
9
+ include TestClassCreationHelper
10
+
11
+ class Test::Unit::TestCase
12
+ end
13
+
@@ -0,0 +1,55 @@
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
+
44
+ expect StatePattern::InvalidTransitionException do
45
+ with_test_class("Switch", :states => ["Up", "Down", "Middle"], :initial_state => "Middle",
46
+ :transitions => {["Up", :push_down] => "Middle",
47
+ ["Down", :push_up] => "Middle",
48
+ ["Middle", :push_up] => "Up",
49
+ ["Middle", :push_down] => "Down"},
50
+ :valid_transitions => {"Up" => "Middle", "Down" => "Middle", "Middle" => "Down"}) do
51
+ switch = Switch.new
52
+ switch.push_up
53
+ end
54
+ end
55
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: state_pattern
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Cadenas
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-06-08 00:00:00 -03:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: dcadenas@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - LICENSE
24
+ - README.rdoc
25
+ files:
26
+ - .document
27
+ - .gitignore
28
+ - LICENSE
29
+ - README.rdoc
30
+ - Rakefile
31
+ - VERSION
32
+ - lib/state_pattern.rb
33
+ - lib/state_pattern/invalid_transition_exception.rb
34
+ - lib/state_pattern/state.rb
35
+ - state_pattern.gemspec
36
+ - test/state_pattern_test.rb
37
+ - test/test_class_creation_helper.rb
38
+ - test/test_helper.rb
39
+ - test/transition_validations_test.rb
40
+ has_rdoc: true
41
+ homepage: http://github.com/dcadenas/state_pattern
42
+ licenses: []
43
+
44
+ post_install_message:
45
+ rdoc_options:
46
+ - --charset=UTF-8
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ requirements: []
62
+
63
+ rubyforge_project: statepattern
64
+ rubygems_version: 1.3.3
65
+ signing_key:
66
+ specification_version: 3
67
+ summary: A Ruby state pattern implementation
68
+ test_files:
69
+ - test/state_pattern_test.rb
70
+ - test/test_class_creation_helper.rb
71
+ - test/test_helper.rb
72
+ - test/transition_validations_test.rb