baby_bots 0.0.6 → 0.0.7
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/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
|