state_pattern 1.0.1

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/.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