baby_bots 0.0.3 → 0.0.4

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.
@@ -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