baby_bots 0.0.6 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/baby_bots.rb +2 -1
- data/lib/baby_bots/baby_bot.rb +56 -19
- data/lib/baby_bots/state.rb +5 -0
- data/lib/baby_bots/version.rb +1 -1
- data/spec/baby_bot_spec.rb +105 -61
- metadata +2 -2
data/lib/baby_bots.rb
CHANGED
data/lib/baby_bots/baby_bot.rb
CHANGED
@@ -1,10 +1,5 @@
|
|
1
|
-
# A tiny finite-state automata library.
|
2
|
-
#
|
3
|
-
# Author:: Justin Hamilton (mailto:justinanthonyhamilton@gmail.com)
|
4
|
-
# Copyright :: BSD2 (see LICENSE for more details)
|
5
|
-
|
6
1
|
module BabyBots
|
7
|
-
|
2
|
+
|
8
3
|
# Error to handle attempts to transition to a state that does not exist.
|
9
4
|
class NoSuchStateException < Exception
|
10
5
|
end
|
@@ -13,24 +8,33 @@ module BabyBots
|
|
13
8
|
class NoSuchTransitionException < Exception
|
14
9
|
end
|
15
10
|
|
16
|
-
# # Error to handle incorrect arity on the post_state methods.
|
17
|
-
# class PostMethodArgumentError < ArgumentError
|
18
|
-
# end
|
19
|
-
|
20
11
|
# A tiny finite-state automata class.
|
21
12
|
class BabyBot
|
22
|
-
attr_accessor :curr, :
|
13
|
+
attr_accessor :curr, :start
|
23
14
|
|
24
15
|
# Accepts an optional hash of states.
|
25
16
|
def initialize(states={})
|
26
17
|
# Hash of state names to state objects
|
27
18
|
@states = {}
|
28
19
|
if !states.empty? then build states end
|
20
|
+
|
21
|
+
# Add our default error state to provide user-based 'error' handling.
|
22
|
+
add_state(ERRSTATE)
|
23
|
+
|
29
24
|
# Initial state
|
30
25
|
@start = nil
|
31
26
|
# Current state
|
32
27
|
@curr = nil
|
33
28
|
end
|
29
|
+
|
30
|
+
# Provides the states, minus the default error state (because it isn't
|
31
|
+
# really a state in the true sense). Note that this is NOT a reference
|
32
|
+
# to the BabyBot's actual states, but rather a duplicate.
|
33
|
+
def states
|
34
|
+
new_states = @states.dup
|
35
|
+
new_states.delete(ERR)
|
36
|
+
return new_states
|
37
|
+
end
|
34
38
|
|
35
39
|
# Adds a new state to the finite-state automata, also accepts
|
36
40
|
# an optional start flag, which will set the supplied state as the initial
|
@@ -51,6 +55,12 @@ module BabyBots
|
|
51
55
|
end
|
52
56
|
end
|
53
57
|
|
58
|
+
# Since we implement a default error state, this empty will check
|
59
|
+
# if there are any other states aside from that one.
|
60
|
+
def empty?
|
61
|
+
@states.keys.find_all { |k| k != ERR }.empty?
|
62
|
+
end
|
63
|
+
|
54
64
|
# Build up this machine's states with a given state table. The format of
|
55
65
|
# this table is assumed to be {state_name => {event1 => transition1, ...}}.
|
56
66
|
# build assumes that the first state provided is the start state, although
|
@@ -79,6 +89,11 @@ module BabyBots
|
|
79
89
|
@curr.state
|
80
90
|
end
|
81
91
|
|
92
|
+
# Return the name of the current state
|
93
|
+
def state_name
|
94
|
+
@curr.state.state
|
95
|
+
end
|
96
|
+
|
82
97
|
# Process the finite state automata with the given event.
|
83
98
|
# This will first see if the finite state automata has a defined method
|
84
99
|
# named "pre_{current_state}", and if so "cook" the input event with this
|
@@ -113,23 +128,30 @@ module BabyBots
|
|
113
128
|
# if the event is nil, and there is no :else clause,
|
114
129
|
# throw an exception
|
115
130
|
if next_state.nil?
|
116
|
-
|
117
|
-
"No valid transition #{event} for #{@curr.state}"
|
131
|
+
transition_error(event, @curr.state)
|
118
132
|
end
|
119
|
-
|
120
|
-
# check if we need to
|
133
|
+
|
134
|
+
# first, check if we need to handle an error function for this state,
|
135
|
+
# then check if we need to postprocess the event, this will act
|
121
136
|
# as the "return" from any state transition (even self-looping transitions)
|
122
|
-
if
|
137
|
+
if next_state == ERRSTATE
|
138
|
+
if respond_to?("error_#{@curr.state}")
|
139
|
+
ret_val = my_send("error_#{@curr.state}", event)
|
140
|
+
else
|
141
|
+
raise NoSuchTransitionException,
|
142
|
+
"No valid transition #{event} for #{@curr.state}"
|
143
|
+
end
|
144
|
+
elsif respond_to?("post_#{@curr.state}")
|
123
145
|
ret_val = my_send("post_#{@curr.state}", event)
|
124
146
|
elsif respond_to?("post_cooked_#{curr.state}")
|
125
147
|
ret_val = my_send("post_#{@curr.state}", cooked_event)
|
126
148
|
end
|
127
149
|
|
128
150
|
# actually transition, and make sure such a transition exists
|
129
|
-
@curr = next_state
|
151
|
+
@curr = next_state unless next_state == ERRSTATE
|
152
|
+
|
130
153
|
if @curr.nil?
|
131
|
-
|
132
|
-
"No valid state #{@curr} for transition #{event} from #{curr_state}"
|
154
|
+
transition_error(event, @curr.state)
|
133
155
|
end
|
134
156
|
|
135
157
|
return ret_val
|
@@ -154,8 +176,23 @@ module BabyBots
|
|
154
176
|
return true
|
155
177
|
end
|
156
178
|
|
179
|
+
# Check if a method call is checking that state name.
|
180
|
+
def method_missing(m, *a, &b)
|
181
|
+
if m =~ /^([a-zA-Z_]+)\?$/
|
182
|
+
state == m.to_s.gsub("?","").to_sym
|
183
|
+
else
|
184
|
+
raise NoMethodError
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
157
188
|
private
|
158
189
|
|
190
|
+
# Wrapper for our NoSuchTransitionException
|
191
|
+
def transition_error(event, state)
|
192
|
+
raise NoSuchTransitionException,
|
193
|
+
"No valid transition #{event} for #{state}"
|
194
|
+
end
|
195
|
+
|
159
196
|
# A wrapper around send, using the arity restrictions assumed by BabyBots.
|
160
197
|
def my_send(method_name, event=nil)
|
161
198
|
m_arity = method(method_name).arity
|
data/lib/baby_bots/state.rb
CHANGED
@@ -2,6 +2,9 @@ module BabyBots
|
|
2
2
|
# Transition state used to remove events from a transition table.
|
3
3
|
NOWHERE = :nowhere__
|
4
4
|
|
5
|
+
# Defualt Error Transition
|
6
|
+
ERR = :err__
|
7
|
+
|
5
8
|
# The state contained within the BabyBots Finite State Automata.
|
6
9
|
# States have an event that transitions to a new state. They may also
|
7
10
|
# have an event named :else which will be the transition used by the
|
@@ -50,4 +53,6 @@ module BabyBots
|
|
50
53
|
end
|
51
54
|
end
|
52
55
|
|
56
|
+
ERRSTATE = State.new(:err__)
|
57
|
+
|
53
58
|
end
|
data/lib/baby_bots/version.rb
CHANGED
data/spec/baby_bot_spec.rb
CHANGED
@@ -6,8 +6,25 @@ $run = BabyBots::State.new(:run, {:else => :run})
|
|
6
6
|
|
7
7
|
|
8
8
|
TEST_MACHINE1 = { :loading => {1 => :ready, :else => :loading},
|
9
|
-
|
10
|
-
|
9
|
+
:ready => {1 => :run, :else => :loading},
|
10
|
+
:run => {:else => :run} }
|
11
|
+
|
12
|
+
class ERRTEST < BabyBots::BabyBot
|
13
|
+
def initialize
|
14
|
+
super
|
15
|
+
build({ :loading => {1 => :ready, :else => BabyBots::ERR},
|
16
|
+
:ready => {0 => :loading, 1 => :run, :else => BabyBots::ERR},
|
17
|
+
:run => {:else => :run} })
|
18
|
+
end
|
19
|
+
|
20
|
+
def error_loading
|
21
|
+
"Error loading"
|
22
|
+
end
|
23
|
+
|
24
|
+
def error_ready
|
25
|
+
"Error ready"
|
26
|
+
end
|
27
|
+
end
|
11
28
|
|
12
29
|
class BB < BabyBots::BabyBot
|
13
30
|
def initialize
|
@@ -52,117 +69,144 @@ end
|
|
52
69
|
|
53
70
|
|
54
71
|
describe BabyBots::BabyBot do
|
72
|
+
before(:each) do
|
73
|
+
@test = nil
|
74
|
+
@test2 = nil
|
75
|
+
end
|
76
|
+
|
77
|
+
after(:each) do
|
78
|
+
@test = nil
|
79
|
+
@test2 = nil
|
80
|
+
end
|
81
|
+
|
55
82
|
it "should be able to be initiated with no additional initialization" do
|
56
|
-
test = BabyBots::BabyBot.new
|
57
|
-
test.
|
83
|
+
@test = BabyBots::BabyBot.new
|
84
|
+
@test.empty?.should == true
|
85
|
+
@test = nil
|
58
86
|
end
|
59
87
|
|
60
88
|
it "should be able to be initialized with a state table" do
|
61
|
-
test = BabyBots::BabyBot.new(TEST_MACHINE1)
|
62
|
-
test.states.should == {:loading => $loading, :ready => $ready, :run => $run}
|
89
|
+
@test = BabyBots::BabyBot.new(TEST_MACHINE1)
|
90
|
+
@test.states.should == {:loading => $loading, :ready => $ready, :run => $run}
|
63
91
|
end
|
64
92
|
|
65
93
|
it "should have states be able to be added using add_state" do
|
66
|
-
test = BabyBots::BabyBot.new
|
67
|
-
test.add_state($loading)
|
68
|
-
test.states.should == {:loading => $loading}
|
69
|
-
test.add_state($ready)
|
70
|
-
test.states.should == {:loading => $loading, :ready => $ready}
|
71
|
-
test.add_state($run)
|
72
|
-
test.states.should == {:loading => $loading, :ready => $ready, :run => $run}
|
94
|
+
@test = BabyBots::BabyBot.new
|
95
|
+
@test.add_state($loading)
|
96
|
+
@test.states.should == {:loading => $loading}
|
97
|
+
@test.add_state($ready)
|
98
|
+
@test.states.should == {:loading => $loading, :ready => $ready}
|
99
|
+
@test.add_state($run)
|
100
|
+
@test.states.should == {:loading => $loading, :ready => $ready, :run => $run}
|
73
101
|
end
|
74
102
|
|
75
103
|
it "should be able to have states built using the build method" do
|
76
|
-
test = BabyBots::BabyBot.new
|
77
|
-
test.build(TEST_MACHINE1)
|
104
|
+
@test = BabyBots::BabyBot.new
|
105
|
+
@test.build(TEST_MACHINE1)
|
78
106
|
end
|
79
107
|
|
80
108
|
it "should allow states to be overwritten using build" do
|
81
|
-
test = BabyBots::BabyBot.new(TEST_MACHINE1)
|
82
|
-
test.build({ :loading => {1 => :run, 2 => :loading} })
|
109
|
+
@test = BabyBots::BabyBot.new(TEST_MACHINE1)
|
110
|
+
@test.build({ :loading => {1 => :run, 2 => :loading} })
|
83
111
|
|
84
|
-
test2 = BabyBots::BabyBot.new({ :loading => {1 => :run, 2 => :loading},
|
112
|
+
@test2 = BabyBots::BabyBot.new({ :loading => {1 => :run, 2 => :loading},
|
85
113
|
:ready => {1 => :run, :else => :loading},
|
86
114
|
:run => {:else => :run} })
|
87
|
-
test.states.should == test2.states
|
115
|
+
@test.states.should == @test2.states
|
88
116
|
end
|
89
117
|
|
90
|
-
it "should
|
91
|
-
test =
|
92
|
-
test.
|
118
|
+
it "should provide state-based error handling" do
|
119
|
+
@test = ERRTEST.new
|
120
|
+
msg = @test.process(42)
|
121
|
+
msg.should == "Error loading"
|
122
|
+
@test.state.should == :loading
|
123
|
+
@test.process(1)
|
124
|
+
@test.state.should == :ready
|
125
|
+
msg = @test.process(42)
|
126
|
+
msg.should == "Error ready"
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should run the example, with the starting state being 'loading'" do
|
130
|
+
@test = BB.new
|
131
|
+
@test.start.state.should == :loading
|
93
132
|
end
|
94
133
|
|
95
134
|
it "should run the example, with the current state being loading" do
|
96
|
-
test = BB.new
|
97
|
-
test.state.should == :loading
|
135
|
+
@test = BB.new
|
136
|
+
@test.state.should == :loading
|
98
137
|
end
|
99
138
|
|
100
139
|
it "should run the example, iterating from loading to ready on inputs 1" do
|
101
|
-
test = BB.new
|
102
|
-
test.process(1)
|
103
|
-
test.state.should == :ready
|
140
|
+
@test = BB.new
|
141
|
+
@test.process(1)
|
142
|
+
@test.state.should == :ready
|
104
143
|
end
|
105
144
|
|
106
145
|
it "should run the example, using pre_loading to convert \"1\" to 1" do
|
107
|
-
test = BB.new
|
108
|
-
test.process("1")
|
109
|
-
test.state.should == :ready
|
146
|
+
@test = BB.new
|
147
|
+
@test.process("1")
|
148
|
+
@test.state.should == :ready
|
110
149
|
end
|
111
150
|
|
112
151
|
it "should run the example, using pre_loading to convert \"1\" to 1" do
|
113
|
-
test = BB.new
|
114
|
-
test.process("1")
|
115
|
-
test.state.should == :ready
|
152
|
+
@test = BB.new
|
153
|
+
@test.process("1")
|
154
|
+
@test.state.should == :ready
|
116
155
|
end
|
117
156
|
|
118
157
|
it "should run the example, use pre_loading to convert \"1\" to 1, but use post_loading to return \"1\"" do
|
119
|
-
test = BB.new
|
120
|
-
final_var = test.process("1")
|
121
|
-
test.state.should == :ready
|
158
|
+
@test = BB.new
|
159
|
+
final_var = @test.process("1")
|
160
|
+
@test.state.should == :ready
|
122
161
|
final_var.should == "1"
|
123
162
|
end
|
124
163
|
|
125
164
|
it "should run the example, and be able to be reset using the restart method" do
|
126
|
-
test = BB.new
|
127
|
-
test.process(1)
|
128
|
-
test.state.should == :ready
|
129
|
-
test.restart
|
130
|
-
test.state.should == :loading
|
165
|
+
@test = BB.new
|
166
|
+
@test.process(1)
|
167
|
+
@test.state.should == :ready
|
168
|
+
@test.restart
|
169
|
+
@test.state.should == :loading
|
131
170
|
end
|
132
171
|
|
133
172
|
it "should run the example, where anything other than \"1\" or \"0\" looping in :ready" do
|
134
|
-
test = BB.new
|
135
|
-
test.process(1)
|
136
|
-
test.state.should == :ready
|
137
|
-
test.process("banana phone")
|
138
|
-
test.state.should == :ready
|
139
|
-
test.process(99)
|
140
|
-
test.state.should == :ready
|
141
|
-
test.process
|
142
|
-
test.state.should == :ready
|
173
|
+
@test = BB.new
|
174
|
+
@test.process(1)
|
175
|
+
@test.state.should == :ready
|
176
|
+
@test.process("banana phone")
|
177
|
+
@test.state.should == :ready
|
178
|
+
@test.process(99)
|
179
|
+
@test.state.should == :ready
|
180
|
+
@test.process
|
181
|
+
@test.state.should == :ready
|
143
182
|
end
|
144
183
|
|
145
184
|
it "should run the example, lacking a pre_run should not have any ill side-effects" do
|
146
|
-
test = BB.new
|
147
|
-
test.process(1)
|
148
|
-
test.process(1)
|
149
|
-
ret1 = test.process("chocolate rain")
|
150
|
-
ret2 = test.process("gournal")
|
185
|
+
@test = BB.new
|
186
|
+
@test.process(1)
|
187
|
+
@test.process(1)
|
188
|
+
ret1 = @test.process("chocolate rain")
|
189
|
+
ret2 = @test.process("gournal")
|
151
190
|
ret1.should == ret2
|
152
191
|
ret2.should == true
|
153
192
|
end
|
154
193
|
|
155
194
|
it "should run the example, and not have issue with a zero-arity method" do
|
156
|
-
test = BB2.new
|
157
|
-
test.process(1)
|
158
|
-
test.process(1)
|
159
|
-
test.state.should == :run
|
195
|
+
@test = BB2.new
|
196
|
+
@test.process(1)
|
197
|
+
@test.process(1)
|
198
|
+
@test.state.should == :run
|
160
199
|
end
|
161
200
|
|
162
201
|
it "should run the example, and raise an ArgumentError on arity error" do
|
163
|
-
test = BB3.new
|
164
|
-
lambda{ test.process(1)}.should raise_error ArgumentError
|
202
|
+
@test = BB3.new
|
203
|
+
lambda{ @test.process(1)}.should raise_error ArgumentError
|
165
204
|
end
|
166
205
|
|
206
|
+
it "should allow the current state to be checked with the message {state_name}?" do
|
207
|
+
@test = BB.new
|
208
|
+
@test.loading?.should == true
|
209
|
+
@test.not_ready?.should == false
|
210
|
+
end
|
167
211
|
|
168
212
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: baby_bots
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.7
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-05-
|
12
|
+
date: 2012-05-08 00:00:00.000000000Z
|
13
13
|
dependencies: []
|
14
14
|
description: A tiny finite-state automata library.
|
15
15
|
email: justinanthonyhamilton@gmail.com
|