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.
@@ -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
- # add a new state to the state machine
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 hash
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
- @curr = @start
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
- # accepts a hash of hashes consiting of {state => {event => transition, ...} ...}
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 each event and transition, building up the transitions
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
- # this is the driving function behind the fsa, process
56
- # will check the current state, doing any processing necessary
57
- # on the input based on whether or not this
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
@@ -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
- def add_transition(event, transition, &callback)
12
- @table[event] = transition
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
- # the idea behind build is that you can call multiple
16
- # :event :transition and the build method
17
- # will parse them out and add them to state
18
- def build(*args)
19
- args.map {|k,v| add_transition(k, v) }
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
@@ -1,3 +1,4 @@
1
1
  module BabyBots
2
- VERSION = "0.0.3"
2
+ # Current development version
3
+ VERSION = "0.0.4"
3
4
  end
@@ -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
@@ -0,0 +1,4 @@
1
+ require_relative '../lib/baby_bots/baby_bot.rb'
2
+ require_relative '../lib/baby_bots/state.rb'
3
+
4
+
@@ -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.3
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