baby_bots 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/baby_bots/baby_bot.rb +47 -16
- data/lib/baby_bots/state.rb +34 -7
- data/lib/baby_bots/version.rb +2 -1
- data/spec/baby_bot_spec.rb +32 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/state_spec.rb +80 -0
- metadata +8 -2
data/lib/baby_bots/baby_bot.rb
CHANGED
@@ -1,42 +1,63 @@
|
|
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
|
+
|
1
6
|
module BabyBots
|
2
7
|
|
8
|
+
# Error to handle attempts to transition to a state that does not exist.
|
3
9
|
class NoSuchStateException < Exception
|
4
10
|
end
|
5
11
|
|
12
|
+
# Error to handle attempts to use a transition that does not exist.
|
6
13
|
class NoSuchTransitionException < Exception
|
7
14
|
end
|
8
15
|
|
16
|
+
# A tiny finite-state automata class.
|
9
17
|
class BabyBot
|
10
18
|
attr_accessor :curr, :states, :start
|
11
19
|
|
20
|
+
# Accepts an optional hash of states.
|
12
21
|
def initialize(states={})
|
22
|
+
# Hash of state names to state objects
|
13
23
|
@states = states
|
24
|
+
# Initial state
|
14
25
|
@start = nil
|
26
|
+
# Current state
|
15
27
|
@curr = nil
|
16
28
|
end
|
17
29
|
|
18
|
-
#
|
30
|
+
# Adds a new state to the finite-state automata, also accepts
|
31
|
+
# an optional start flag, which will set the supplied state as the initial
|
32
|
+
# state. Note that this machine may only have one start state. Additionally,
|
33
|
+
# adding the first start state will set @curr to be this state, after this
|
34
|
+
# has been done @curr will remain on whatever state it is currently residing
|
35
|
+
# on, and must be reset using the reset method.
|
19
36
|
def add_state(state, start=nil)
|
20
|
-
# key on state names to the actual state
|
37
|
+
# key on state names to the actual state object
|
21
38
|
@states[state.state] = state
|
39
|
+
|
40
|
+
# if this is a start state
|
22
41
|
if start
|
23
42
|
@start = state
|
24
|
-
|
43
|
+
|
44
|
+
# only set the current state to the start state if @curr is nil.
|
45
|
+
if @curr.nil? then @curr = @start end
|
25
46
|
end
|
26
47
|
end
|
27
48
|
|
49
|
+
# Build up this machine's states with a given state table. The format of
|
50
|
+
# this table is assumed to be {state_name => {event1 => transition1, ...}}.
|
51
|
+
# build assumes that the first state provided is the start state, although
|
52
|
+
# the optional no_first parameter may be set to override this.
|
53
|
+
def build(table, no_first=false)
|
54
|
+
first_state = !no_first
|
28
55
|
|
29
|
-
|
30
|
-
# structure, and adds these states to the state machine, assumes first state
|
31
|
-
# is the starting state
|
32
|
-
def build(table)
|
33
|
-
first_state = true
|
34
|
-
|
35
|
-
# iterate through the provided {state => {event => transition, ...}, ...}
|
56
|
+
# iterate through each provided state table entries
|
36
57
|
table.each do |state, state_table|
|
37
58
|
temp_state = State.new(state)
|
38
|
-
|
39
|
-
# iterate through
|
59
|
+
|
60
|
+
# iterate through the current state table, adding events => transition
|
40
61
|
state_table.each do |event, transition|
|
41
62
|
temp_state.add_transition(event, transition)
|
42
63
|
end
|
@@ -48,13 +69,20 @@ module BabyBots
|
|
48
69
|
end
|
49
70
|
end
|
50
71
|
|
72
|
+
# Return the current state's actual state.
|
51
73
|
def state
|
52
74
|
@curr.state
|
53
75
|
end
|
54
76
|
|
55
|
-
#
|
56
|
-
# will
|
57
|
-
#
|
77
|
+
# Process the finite state automata with the given event.
|
78
|
+
# This will first see if the finite state automata has a defined method
|
79
|
+
# named "pre_{current_state}", and if so "cook" the input event with this
|
80
|
+
# method. Process will then use the cooked input if available, or the raw
|
81
|
+
# input if there is none to compute the transition. Before actually
|
82
|
+
# transitioning, it will then send the raw output to a method of
|
83
|
+
# "post_{current_state}", which will be the return value. If a method named
|
84
|
+
# "post_cook_{current_state}" is supplied, it will send the cooked event to
|
85
|
+
# this method instead of using "post_{current_state}".
|
58
86
|
def process(event=nil)
|
59
87
|
# get the current state
|
60
88
|
curr_state = @curr
|
@@ -64,12 +92,15 @@ module BabyBots
|
|
64
92
|
cooked_event = send("pre_#{@curr.state}", event)
|
65
93
|
end
|
66
94
|
|
95
|
+
# calculate the next state
|
67
96
|
if cooked_event
|
68
97
|
next_state = @states[curr.table[cooked_event]]
|
69
98
|
else
|
70
99
|
next_state = @states[curr.table[event]]
|
71
100
|
end
|
72
101
|
|
102
|
+
# if there is no such transition, see if there is an a translation
|
103
|
+
# known as else, and use that as the next state
|
73
104
|
if next_state.nil?
|
74
105
|
next_state = @states[curr.table[:else]]
|
75
106
|
end
|
@@ -89,7 +120,6 @@ module BabyBots
|
|
89
120
|
ret_val = send("post_#{@curr.state}", cooked_event)
|
90
121
|
end
|
91
122
|
|
92
|
-
|
93
123
|
# actually transition, and make sure such a transition exists
|
94
124
|
@curr = next_state
|
95
125
|
if @curr.nil?
|
@@ -100,6 +130,7 @@ module BabyBots
|
|
100
130
|
return ret_val
|
101
131
|
end
|
102
132
|
|
133
|
+
# Reset the current state to be the start state.
|
103
134
|
def restart
|
104
135
|
@curr = @start
|
105
136
|
end
|
data/lib/baby_bots/state.rb
CHANGED
@@ -1,22 +1,49 @@
|
|
1
1
|
module BabyBots
|
2
|
+
# Transition state used to remove events from a transition table.
|
3
|
+
NOWHERE = :nowhere__
|
2
4
|
|
5
|
+
# The state contained within the BabyBots Finite State Automata.
|
6
|
+
# States have an event that transitions to a new state. They may also
|
7
|
+
# have an event named :else which will be the transition used by the
|
8
|
+
# containing BabyBot when calculating transitions where no such supplied
|
9
|
+
# events exist.
|
3
10
|
class State
|
4
11
|
attr_reader :state, :table
|
5
12
|
|
13
|
+
# Sets the state name, as well as an optionally supplied transition table.
|
6
14
|
def initialize(state, table={})
|
15
|
+
# state name
|
7
16
|
@state = state
|
17
|
+
|
18
|
+
# transition table
|
8
19
|
@table = table
|
9
20
|
end
|
10
21
|
|
11
|
-
|
12
|
-
|
22
|
+
# Adds a transition to the transition table. Table format is
|
23
|
+
# event => transition, where event is the "input" into the state.
|
24
|
+
#
|
25
|
+
# Transitions are allowed to be deleted by being set to NOWHERE.
|
26
|
+
def add_transition(event, transition)
|
27
|
+
if transition == NOWHERE
|
28
|
+
remove_transition(event)
|
29
|
+
else
|
30
|
+
@table[event] = transition
|
31
|
+
end
|
13
32
|
end
|
14
33
|
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
|
19
|
-
|
34
|
+
# Provided a table, merge the state's current transition table
|
35
|
+
# with the supplied one. Note that since this is part of a
|
36
|
+
# finite state machine, supplying events that already exist
|
37
|
+
# in the transition table override this transition.
|
38
|
+
def build(table)
|
39
|
+
table.each { |k,v| add_transition(k, v) }
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
# Delete an entry from the transition table.
|
44
|
+
def remove_transition(event)
|
45
|
+
@table.delete(event)
|
20
46
|
end
|
21
47
|
end
|
48
|
+
|
22
49
|
end
|
data/lib/baby_bots/version.rb
CHANGED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'spec_helper.rb'
|
2
|
+
|
3
|
+
$loading = BabyBots::State.new(:loading, {1 => :ready, :else => :loading})
|
4
|
+
$ready = BabyBots::State.new(:ready, {1 => :run, :else => :loading})
|
5
|
+
$run = BabyBots::State.new(:run, {:else => :run})
|
6
|
+
|
7
|
+
|
8
|
+
TEST_MACHINE1 = { :loading => {1 => :ready, :else => :loading},
|
9
|
+
:ready => {1 => :run, :else => :loading},
|
10
|
+
:run => {:else => :run} }
|
11
|
+
|
12
|
+
describe BabyBots::BabyBot do
|
13
|
+
it "should be able to be initiated with no additional initialization" do
|
14
|
+
test = BabyBots::BabyBot.new
|
15
|
+
test.states.should == {}
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should be able to be initialized with a state table" do
|
19
|
+
test = BabyBots::BabyBot.new(TEST_MACHINE1)
|
20
|
+
test.states.should == TEST_MACHINE1
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should have states be able to be added using add_state" do
|
24
|
+
test = BabyBots::BabyBot.new
|
25
|
+
test.add_state($loading)
|
26
|
+
test.states.should == {:loading => $loading}
|
27
|
+
test.add_state($ready)
|
28
|
+
test.states.should == {:loading => $loading, :ready => $ready}
|
29
|
+
test.add_state($run)
|
30
|
+
test.states.should == {:loading => $loading, :ready => $ready, :run => $run}
|
31
|
+
end
|
32
|
+
end
|
data/spec/spec_helper.rb
ADDED
data/spec/state_spec.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'spec_helper.rb'
|
2
|
+
|
3
|
+
TEST_NAME = :test
|
4
|
+
TEST_TRANS1_1 = {0 => :test}
|
5
|
+
TEST_TRANS1_2 = {1 => :done}
|
6
|
+
TEST_TRANS1_3 = {:else => :test}
|
7
|
+
TEST_TABLE1 = TEST_TRANS1_1.merge TEST_TRANS1_2.merge TEST_TRANS1_3
|
8
|
+
TEST_TABLE2 = {2 => :two, 3 => :three}
|
9
|
+
|
10
|
+
|
11
|
+
describe BabyBots::State do
|
12
|
+
it "should require a state name" do
|
13
|
+
test = BabyBots::State.new(TEST_NAME)
|
14
|
+
test.state.should == TEST_NAME
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should provide an empty transition table if none is supplied" do
|
18
|
+
test = BabyBots::State.new(TEST_NAME)
|
19
|
+
test.state.should == TEST_NAME
|
20
|
+
test.table.should == {}
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should accept a state name and transition table" do
|
24
|
+
test = BabyBots::State.new(TEST_NAME, TEST_TABLE1)
|
25
|
+
test.state.should == TEST_NAME
|
26
|
+
test.table.should == TEST_TABLE1
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should be able to have its transition table built after construction" do
|
30
|
+
test = BabyBots::State.new(TEST_NAME)
|
31
|
+
test.table.should == {}
|
32
|
+
test.build(TEST_TABLE1)
|
33
|
+
test.table.should == TEST_TABLE1
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should merge changes between uses of build" do
|
37
|
+
test = BabyBots::State.new(TEST_NAME, TEST_TABLE2)
|
38
|
+
test.table.should == TEST_TABLE2
|
39
|
+
test.build(TEST_TABLE1)
|
40
|
+
test.table.should == TEST_TABLE2.merge(TEST_TABLE1)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should allow transitions to be added piece-by-piece via add_transition" do
|
44
|
+
test = BabyBots::State.new(TEST_NAME)
|
45
|
+
test.table.should == {}
|
46
|
+
test.add_transition(0, :test)
|
47
|
+
test.table.should == {0 => :test}
|
48
|
+
test.add_transition(1, :done)
|
49
|
+
test.table.should == {0 => :test, 1 => :done}
|
50
|
+
test.add_transition(:else, :test)
|
51
|
+
test.table.should == TEST_TABLE1
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should allow transitions to be overwritten using add_transition" do
|
55
|
+
test = BabyBots::State.new(TEST_NAME, TEST_TABLE1)
|
56
|
+
test.table.should == TEST_TABLE1
|
57
|
+
test.add_transition(:else, :error)
|
58
|
+
test.table.should == TEST_TRANS1_1.merge(TEST_TRANS1_2.merge({:else => :error}))
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should allow transitions to be overwritten using build" do
|
62
|
+
test = BabyBots::State.new(TEST_NAME, TEST_TABLE1)
|
63
|
+
test.build({:else => :error})
|
64
|
+
test.table.should == TEST_TRANS1_1.merge(TEST_TRANS1_2.merge({:else => :error}))
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should allow transitions to be removed using remove_transition" do
|
68
|
+
test = BabyBots::State.new(TEST_NAME, TEST_TABLE1)
|
69
|
+
test.remove_transition(:else)
|
70
|
+
test.table.should == TEST_TRANS1_1.merge(TEST_TRANS1_2)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should allow transitions to be deleted by setting their transition to NOWHERE" do
|
74
|
+
test = BabyBots::State.new(TEST_NAME, TEST_TABLE1)
|
75
|
+
test.add_transition(:else, BabyBots::NOWHERE)
|
76
|
+
test.table.should == TEST_TRANS1_1.merge(TEST_TRANS1_2)
|
77
|
+
test.build({0 => BabyBots::NOWHERE, 1 => BabyBots::NOWHERE})
|
78
|
+
test.table.should == {}
|
79
|
+
end
|
80
|
+
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.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -27,6 +27,9 @@ files:
|
|
27
27
|
- lib/baby_bots/baby_bot.rb
|
28
28
|
- lib/baby_bots/state.rb
|
29
29
|
- lib/baby_bots/version.rb
|
30
|
+
- spec/baby_bot_spec.rb
|
31
|
+
- spec/spec_helper.rb
|
32
|
+
- spec/state_spec.rb
|
30
33
|
homepage: https://github.com/jamiltron/BabyBots
|
31
34
|
licenses: []
|
32
35
|
post_install_message:
|
@@ -53,4 +56,7 @@ specification_version: 3
|
|
53
56
|
summary: While there are many fsa libraries out there, I wanted to implement my own
|
54
57
|
so I could learn how to create a module/gem, as I am not really a Ruby guy and have
|
55
58
|
no idea how.
|
56
|
-
test_files:
|
59
|
+
test_files:
|
60
|
+
- spec/baby_bot_spec.rb
|
61
|
+
- spec/spec_helper.rb
|
62
|
+
- spec/state_spec.rb
|