solid_state 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ .*.swp
@@ -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
+
@@ -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
@@ -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