solid_state 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/README.textile +104 -0
- data/Rakefile +26 -0
- data/VERSION +1 -0
- data/lib/solid_state.rb +83 -0
- data/test/solid_state_test.rb +151 -0
- metadata +69 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
.*.swp
|
data/README.textile
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
h2. Solid State - Stateful Ruby objects with a twist
|
2
|
+
|
3
|
+
Most stateful libraries that exist for Ruby deal with keeping track of a state variable, which is a symbol or string stating what state said object is currently in. This works well with libraries like ActiveRecord where you're usually simply interested in data. But what if you want to change the functionality according to what state the object is in? With tools like ActsAsStateMachine, you'll still need to pepper your methods with checks on which state the object is in:
|
4
|
+
|
5
|
+
<pre><code>
|
6
|
+
if state == :this
|
7
|
+
elsif state == :that
|
8
|
+
else
|
9
|
+
...
|
10
|
+
end
|
11
|
+
</code></pre>
|
12
|
+
|
13
|
+
Enter *solid_state*. The Ruby state machine library that lets you define state-specific functionality. But enough yammering, nothing can describe a system like a simple example.
|
14
|
+
|
15
|
+
Please note: this library is *not* a full state machine. See the *Notes* section below.
|
16
|
+
|
17
|
+
h3. Example
|
18
|
+
|
19
|
+
Lets say you have a simple AI that needs to act differently according to what state it's currently in. We'll define the states :persue, :scared, and :idle.
|
20
|
+
|
21
|
+
<pre><code>
|
22
|
+
class AI
|
23
|
+
include SolidState
|
24
|
+
|
25
|
+
state :persue do
|
26
|
+
def update
|
27
|
+
# Chase the target!
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
state :scared do
|
32
|
+
def update
|
33
|
+
# Run away from the target!
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
state :idle do
|
38
|
+
def update
|
39
|
+
# Look for a new target
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
starting_state :idle
|
44
|
+
end
|
45
|
+
</code></pre>
|
46
|
+
|
47
|
+
This one example shows almost the entirity of solid_state's simple API. Include the SolidState module, define your states, and optionally set a starting state. To using this class is simple:
|
48
|
+
|
49
|
+
<pre><code>
|
50
|
+
ai = AI.new
|
51
|
+
ai.current_state # => :idle
|
52
|
+
ai.update # => looking for a target...
|
53
|
+
|
54
|
+
# Target found!
|
55
|
+
ai.change_state! :persue
|
56
|
+
ai.update # => Rawr! Chasing target
|
57
|
+
|
58
|
+
# I'm hurt!
|
59
|
+
ai.change_state! :scared
|
60
|
+
ai.update # => Run away and find help!
|
61
|
+
|
62
|
+
ai.change_state! :dead # => InvalidStateError ... aw
|
63
|
+
</code></pre>
|
64
|
+
|
65
|
+
This also works seemlessly with subclasses. For example:
|
66
|
+
|
67
|
+
<pre><code>
|
68
|
+
class Scavenger < AI
|
69
|
+
|
70
|
+
state :scavange do
|
71
|
+
def update
|
72
|
+
# Scavange!
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
ai = Scavanger.new
|
79
|
+
ai.current_state # => :idle
|
80
|
+
ai.change_state! :scavange
|
81
|
+
ai.update # => Scavaging!
|
82
|
+
|
83
|
+
a.change_state! :idle
|
84
|
+
a.update
|
85
|
+
</code></pre>
|
86
|
+
|
87
|
+
h3. Notes
|
88
|
+
|
89
|
+
To be fair, other state machine libraries do offer this functionality. I'm do this to be a learning experience and to make as bare-bones a state machine system as I can, for when you don't need a full state machine (transitions, validation, transition direction enforcement, etc).
|
90
|
+
|
91
|
+
If you're looking for a fully comprehensive state machine library, "state_machine":http://github.com/pluginaweek/state_machine is the most detailed I've found yet and probably can do anything you need to do.
|
92
|
+
|
93
|
+
h3. Possible Issues
|
94
|
+
|
95
|
+
Individual states are implemented underneath as inner Classes that subclass the current Class. This means they get access to all public and protected methods in the outer Class, but at the same time if there are state methods with the same name as methods on the outer class, the state methods will never get called.
|
96
|
+
|
97
|
+
h3. Project Info
|
98
|
+
|
99
|
+
Install via gems: gem install solid_state --source http://gemcutter.org
|
100
|
+
|
101
|
+
Code hosted on Github: http://github.com/jameskilton/solid_state
|
102
|
+
|
103
|
+
Issues on Github: http://github.com/jameskilton/solid_state/issues
|
104
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
|
3
|
+
task :default => :test
|
4
|
+
|
5
|
+
Rake::TestTask.new do |t|
|
6
|
+
t.libs << "test"
|
7
|
+
t.test_files = Dir["test/*_test.rb"]
|
8
|
+
t.verbose = true
|
9
|
+
end
|
10
|
+
|
11
|
+
begin
|
12
|
+
require 'jeweler'
|
13
|
+
Jeweler::Tasks.new do |gem|
|
14
|
+
gem.name = "solid_state"
|
15
|
+
gem.summary = %Q{Stateful Ruby objects}
|
16
|
+
gem.description = %Q{Add simple states to your classes with different functionality across states.}
|
17
|
+
gem.email = "jameskilton@gmail.com"
|
18
|
+
gem.homepage = "http://github.com/jameskilton/solid_state"
|
19
|
+
gem.authors = ["Jason Roelofs"]
|
20
|
+
gem.add_development_dependency "test-spec"
|
21
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
22
|
+
end
|
23
|
+
Jeweler::GemcutterTasks.new
|
24
|
+
rescue LoadError
|
25
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
26
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
data/lib/solid_state.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
module SolidState
|
2
|
+
|
3
|
+
class InvalidStateError < RuntimeError; end
|
4
|
+
|
5
|
+
# On include, setup all required methods
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
base.send(:include, InstanceMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
|
13
|
+
# Define a state
|
14
|
+
def state(name, &block)
|
15
|
+
klass = const_set("State_#{name}", Class.new(self))
|
16
|
+
klass.send(:define_method, :state_name) { name }
|
17
|
+
klass.class_eval(&block)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Define the starting state
|
21
|
+
def starting_state(name)
|
22
|
+
self.send(:define_method, :__start_state) { name }
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
module InstanceMethods
|
28
|
+
|
29
|
+
# What's the current state
|
30
|
+
def current_state
|
31
|
+
got = @__current_state ||= (self.respond_to?(:__start_state) ?
|
32
|
+
self._find_state(self.__start_state) : nil)
|
33
|
+
got ? got.state_name : nil
|
34
|
+
end
|
35
|
+
|
36
|
+
# Change the current state
|
37
|
+
def change_state!(name)
|
38
|
+
found = _find_state(name)
|
39
|
+
raise InvalidStateError.new("No state defined with name #{name}") if found.nil?
|
40
|
+
@__current_state = found
|
41
|
+
end
|
42
|
+
|
43
|
+
# Proxy off any unknown method into the current state object
|
44
|
+
def method_missing(name, *args)
|
45
|
+
current_state
|
46
|
+
|
47
|
+
if @__current_state.respond_to?(name)
|
48
|
+
@__current_state.send(name, *args)
|
49
|
+
else
|
50
|
+
super
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
def _known_states
|
57
|
+
@__known_states ||= {}
|
58
|
+
end
|
59
|
+
|
60
|
+
def _find_state(name)
|
61
|
+
const_name = "State_#{name}"
|
62
|
+
self._known_states ||= {}
|
63
|
+
|
64
|
+
self._known_states[name] ||= _get_state_const(const_name)
|
65
|
+
end
|
66
|
+
|
67
|
+
# To allow for subclasses to use superclass states, we need
|
68
|
+
# to traverse up the heirarchy looking for the constants
|
69
|
+
# defined. This works because klass.ancectors[0] == klass,
|
70
|
+
# so if the const is defined on this klass, we get it quickly.
|
71
|
+
def _get_state_const(const_name)
|
72
|
+
self.class.ancestors.each do |klass|
|
73
|
+
if klass.const_defined?(const_name)
|
74
|
+
return klass.const_get(const_name).new
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
$:.unshift File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'test/spec'
|
5
|
+
|
6
|
+
require "solid_state"
|
7
|
+
|
8
|
+
class Stateful
|
9
|
+
include SolidState
|
10
|
+
|
11
|
+
def helper
|
12
|
+
14
|
13
|
+
end
|
14
|
+
|
15
|
+
def outer
|
16
|
+
10
|
17
|
+
end
|
18
|
+
|
19
|
+
state :start do
|
20
|
+
|
21
|
+
def add(a, b)
|
22
|
+
a + b
|
23
|
+
end
|
24
|
+
|
25
|
+
def use_helper
|
26
|
+
helper
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
state :next do
|
32
|
+
|
33
|
+
def add(a, b)
|
34
|
+
a - b
|
35
|
+
end
|
36
|
+
|
37
|
+
# This method will never get called
|
38
|
+
# because of #outer defined outside
|
39
|
+
# of this state
|
40
|
+
def outer
|
41
|
+
20
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
state :last do
|
47
|
+
|
48
|
+
def add(a, b)
|
49
|
+
a * b
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class SubState < Stateful
|
56
|
+
|
57
|
+
state :another do
|
58
|
+
|
59
|
+
def add(a, b)
|
60
|
+
a % b
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
starting_state :another
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
context "SolidState" do
|
70
|
+
|
71
|
+
before do
|
72
|
+
@stateful = Stateful.new
|
73
|
+
end
|
74
|
+
|
75
|
+
context "Query and changing state" do
|
76
|
+
|
77
|
+
specify "can get current state" do
|
78
|
+
@stateful.current_state.should.be nil
|
79
|
+
@stateful.change_state! :start
|
80
|
+
@stateful.current_state.should.equal :start
|
81
|
+
end
|
82
|
+
|
83
|
+
specify "can change states" do
|
84
|
+
@stateful.change_state! :next
|
85
|
+
@stateful.current_state.should.equal :next
|
86
|
+
end
|
87
|
+
|
88
|
+
specify "errors out on invalid state choice" do
|
89
|
+
should.raise SolidState::InvalidStateError do
|
90
|
+
@stateful.change_state! :fail_state
|
91
|
+
end
|
92
|
+
|
93
|
+
@stateful.current_state.should.not.equal :fail_state
|
94
|
+
end
|
95
|
+
|
96
|
+
specify "can have a start state" do
|
97
|
+
Stateful.starting_state :start
|
98
|
+
Stateful.new.current_state.should.equal :start
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
context "Per-state functionality" do
|
104
|
+
|
105
|
+
specify "properly uses methods defined in the current state" do
|
106
|
+
@stateful.change_state! :start
|
107
|
+
@stateful.add(2, 4).should.equal 6
|
108
|
+
|
109
|
+
@stateful.change_state! :next
|
110
|
+
@stateful.add(2, 4).should.equal -2
|
111
|
+
|
112
|
+
@stateful.change_state! :last
|
113
|
+
@stateful.add(2, 4).should.equal 8
|
114
|
+
end
|
115
|
+
|
116
|
+
specify "states have access to methods defined outside of states" do
|
117
|
+
@stateful.change_state! :start
|
118
|
+
@stateful.use_helper.should.equal 14
|
119
|
+
@stateful.helper.should.equal 14
|
120
|
+
end
|
121
|
+
|
122
|
+
specify "state's don't have to have the same methods defined across" do
|
123
|
+
@stateful.change_state! :next
|
124
|
+
should.raise NoMethodError do
|
125
|
+
@stateful.use_helper
|
126
|
+
end
|
127
|
+
@stateful.helper.should.equal 14
|
128
|
+
end
|
129
|
+
|
130
|
+
specify "WARNING: helper methods named the same as state methods use helper methods" do
|
131
|
+
@stateful.change_state! :next
|
132
|
+
@stateful.outer.should.equal 10
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
context "Subclassing" do
|
138
|
+
|
139
|
+
specify "subclasses can use parent class states" do
|
140
|
+
state = SubState.new
|
141
|
+
state.current_state.should.equal :another
|
142
|
+
state.add(5, 2).should.equal 1
|
143
|
+
|
144
|
+
should.not.raise SolidState::InvalidStateError do
|
145
|
+
state.change_state! :next
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
metadata
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: solid_state
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jason Roelofs
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-09-12 00:00:00 -04:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: test-spec
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
description: Add simple states to your classes with different functionality across states.
|
26
|
+
email: jameskilton@gmail.com
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files:
|
32
|
+
- README.textile
|
33
|
+
files:
|
34
|
+
- .gitignore
|
35
|
+
- README.textile
|
36
|
+
- Rakefile
|
37
|
+
- VERSION
|
38
|
+
- lib/solid_state.rb
|
39
|
+
- test/solid_state_test.rb
|
40
|
+
has_rdoc: true
|
41
|
+
homepage: http://github.com/jameskilton/solid_state
|
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:
|
64
|
+
rubygems_version: 1.3.5
|
65
|
+
signing_key:
|
66
|
+
specification_version: 3
|
67
|
+
summary: Stateful Ruby objects
|
68
|
+
test_files:
|
69
|
+
- test/solid_state_test.rb
|