simplificator-fsm 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 pascalbetz
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,7 @@
1
+ = fsm
2
+
3
+ Description goes here.
4
+
5
+ == Copyright
6
+
7
+ Copyright (c) 2009 pascalbetz. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "fsm"
8
+ gem.summary = %Q{A simple finite state machine (FSM) gem.}
9
+ gem.email = "info@simplificator.com"
10
+ gem.homepage = "http://github.com/simplificator/fsm"
11
+ gem.authors = ["simplificator"]
12
+
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ rescue LoadError
16
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
17
+ end
18
+
19
+ require 'rake/testtask'
20
+ Rake::TestTask.new(:test) do |test|
21
+ test.libs << 'lib' << 'test'
22
+ test.pattern = 'test/**/*_test.rb'
23
+ test.verbose = true
24
+ end
25
+
26
+ begin
27
+ require 'rcov/rcovtask'
28
+ Rcov::RcovTask.new do |test|
29
+ test.libs << 'test'
30
+ test.pattern = 'test/**/*_test.rb'
31
+ test.verbose = true
32
+ end
33
+ rescue LoadError
34
+ task :rcov do
35
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
36
+ end
37
+ end
38
+
39
+
40
+ task :default => :test
41
+
42
+ require 'rake/rdoctask'
43
+ Rake::RDocTask.new do |rdoc|
44
+ if File.exist?('VERSION.yml')
45
+ config = YAML.load(File.read('VERSION.yml'))
46
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
47
+ else
48
+ version = ""
49
+ end
50
+
51
+ rdoc.rdoc_dir = 'rdoc'
52
+ rdoc.title = "fsm #{version}"
53
+ rdoc.rdoc_files.include('README*')
54
+ rdoc.rdoc_files.include('lib/**/*.rb')
55
+ end
56
+
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 0
data/lib/fsm.rb ADDED
@@ -0,0 +1,41 @@
1
+ require File.join(File.dirname(__FILE__), 'fsm', 'errors')
2
+ require File.join(File.dirname(__FILE__), 'fsm', 'machine')
3
+ require File.join(File.dirname(__FILE__), 'fsm', 'machine_config')
4
+ require File.join(File.dirname(__FILE__), 'fsm', 'state')
5
+ require File.join(File.dirname(__FILE__), 'fsm', 'transition')
6
+ require File.join(File.dirname(__FILE__), 'fsm', 'executable')
7
+
8
+ module FSM
9
+ module ClassMethods
10
+ def define_fsm(&block)
11
+ raise "FSM is already defined. Call define_fsm only once" if Machine[self]
12
+ machine = Machine.new(self)
13
+ Machine[self] = machine
14
+ config = MachineConfig.new(machine)
15
+ config.process(&block)
16
+ machine.post_process
17
+ end
18
+
19
+
20
+ end
21
+
22
+ module InstanceMethods
23
+ #
24
+ # Which states are reachable from the current state
25
+ def reachable_state_names
26
+ Machine[self.class].reachable_states.map() {|item| item.name}
27
+ end
28
+
29
+ def available_transition_names
30
+ Machine[self.class].available_transitions.map() {|item| item.name}
31
+ end
32
+ end
33
+
34
+ def self.included(receiver)
35
+ receiver.class_eval do
36
+ extend(ClassMethods)
37
+ include(InstanceMethods)
38
+ end
39
+ end
40
+
41
+ end
data/lib/fsm/errors.rb ADDED
@@ -0,0 +1,6 @@
1
+ module FSM
2
+ class InvalidStateTransition < RuntimeError
3
+ end
4
+ class UnknownState < RuntimeError
5
+ end
6
+ end
@@ -0,0 +1,28 @@
1
+ module FSM
2
+ #
3
+ # Execute an action specified by either String, Sylbol or Proc.
4
+ # Symbol and String represent methods which are called on the target object, Proc will get executed
5
+ # and receives at least the target as parameter
6
+ class Executable
7
+ # Create a new Executable
8
+ # if args is true, then arguments are passed on to the target method or the Proc, if false nothing
9
+ # will get passed
10
+ def initialize(thing, args = false)
11
+ @thing = thing
12
+ @has_arguments = args
13
+ end
14
+
15
+ # execute this executable on the given target
16
+ def execute(target, args)
17
+ case @thing
18
+ when String, Symbol:
19
+ @has_arguments ? target.send(@thing, *args) : target.send(@thing)
20
+ when Proc:
21
+ @has_arguments ? @thing.call(target, *args) : @thing.call(target)
22
+ when Nil:
23
+ else
24
+ raise "Unknown Thing #{@thing}"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,98 @@
1
+ module FSM
2
+ class Machine
3
+ attr_accessor(:current_state, :states, :state_attribute)
4
+
5
+ def initialize(klass)
6
+ @klass = klass
7
+ self.states = {}
8
+ self.state_attribute = :state
9
+ end
10
+
11
+ def self.[](includer)
12
+ (@machines ||= {})[includer]
13
+ end
14
+
15
+ def self.[]=(*args)
16
+ (@machines ||= {})[args.first] = args.last
17
+ end
18
+
19
+ def state(name, options = {})
20
+ raise "State is already defined: '#{name}'" if self.states[name]
21
+ self.states[name] = State.new(name, options)
22
+ initial(name) unless self.current_state
23
+ nil
24
+ end
25
+
26
+ def initial(name)
27
+ self.current_state = self.states[name]
28
+ raise UnknownState.new("Unknown state '#{name}'. Define states first with state(name)") unless self.current_state
29
+ nil
30
+ end
31
+
32
+ def transition(name, from_name, to_name, options = {})
33
+ raise ArgumentError.new("name, from_name and to_name are required") if name.nil? || from_name.nil? || to_name.nil?
34
+ raise UnknownState.new("Unknown source state '#{from}'") unless self.states[from_name]
35
+ raise UnknownState.new("Unknown target state '#{to}'") unless self.states[to_name]
36
+ define_transition_method(name, to_name)
37
+ from_state = self.states[from_name]
38
+ to_state = self.states[to_name]
39
+ transition = Transition.new(name, from_state, to_state, options)
40
+ from_state.transitions[to_state.name] = transition
41
+ nil
42
+ end
43
+
44
+ def attribute(name)
45
+ self.state_attribute = name
46
+ end
47
+
48
+ def reachable_states()
49
+ self.states.map do |name, state|
50
+ state.to_states if state == current_state
51
+ end.flatten.compact
52
+ end
53
+
54
+ def available_transitions()
55
+ current_state.transitions.values
56
+ end
57
+
58
+
59
+ def post_process
60
+ define_state_attribute_methods(self.state_attribute)
61
+ end
62
+
63
+
64
+
65
+ private
66
+
67
+ def define_state_attribute_methods(name)
68
+ @klass.instance_eval do
69
+ define_method("#{name}") do
70
+ Machine[self.class].current_state.name
71
+ end
72
+
73
+ define_method("#{name}=") do |value|
74
+ Machine[self.class].current_state = Machine[self.class].states[value]
75
+ raise("Unknown State #{value}") unless Machine[self.class].current_state
76
+ end
77
+ end
78
+ end
79
+
80
+ def define_transition_method(name, to_name)
81
+ @klass.instance_eval do
82
+ define_method(name) do |*args|
83
+ from_state = Machine[self.class].current_state
84
+ to_state = Machine[self.class].states[to_name]
85
+ transition = from_state.transitions[to_name]
86
+ raise InvalidStateTransition.new("No transition defined from #{from_state.name} -> #{to_state.name}") unless transition
87
+
88
+ from_state.exit(self)
89
+ transition.fire_event(self, args)
90
+ to_state.enter(self)
91
+ Machine[self.class].current_state = to_state
92
+ end
93
+ end
94
+ end
95
+
96
+
97
+ end
98
+ end
@@ -0,0 +1,21 @@
1
+ module FSM
2
+ class MachineConfig
3
+ CONFIG_METHODS = %w[state transition initial attribute]
4
+ instance_methods.each do |m|
5
+ undef_method m unless m == '__send__' || m == '__id__' || m == 'instance_eval'
6
+ end
7
+
8
+ def initialize(target)
9
+ @target = target
10
+ end
11
+
12
+ def process(&block)
13
+ instance_eval(&block)
14
+ end
15
+
16
+ def method_missing(sym, *args, &block)
17
+ raise "Unknown config method '#{sym}'. Only #{CONFIG_METHODS.map() {|item| "'#{item}'" }.join(', ')} are known" unless CONFIG_METHODS.include?(sym.to_s)
18
+ @target.__send__(sym, *args, &block)
19
+ end
20
+ end
21
+ end
data/lib/fsm/state.rb ADDED
@@ -0,0 +1,38 @@
1
+ module FSM
2
+ #
3
+ # A State has a name and a list of outgoing transitions.
4
+ #
5
+ class State
6
+ attr_reader(:name, :transitions)
7
+
8
+ # name: a symbol which identifies this state
9
+ # options
10
+ #  * :enter : a symbol or string or Proc
11
+ #  * :exit : a symbol or string or Proc
12
+ def initialize(name, options = {})
13
+ @name = name
14
+ @enter = Executable.new options[:enter]
15
+ @exit = Executable.new options[:exit]
16
+ @transitions = {}
17
+ end
18
+
19
+ # Called when this state is entered
20
+ def enter(target)
21
+ @enter.execute(target, nil)
22
+ end
23
+ # Called when this state is exited
24
+ def exit(target)
25
+ @exit.execute(target, nil)
26
+ end
27
+
28
+ # All states that are reachable form this state by one hop
29
+ def to_states
30
+ self.transitions.map { |to_name, transition| transition.to}
31
+ end
32
+
33
+ def to_s
34
+ "State '#{self.name}' (#{self.object_id})"
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,18 @@
1
+ module FSM
2
+ class Transition
3
+ attr_accessor(:name, :from, :to, :event)
4
+ def initialize(name, from, to, options = {})
5
+ self.name = name
6
+ self.from = from
7
+ self.to = to
8
+ self.event = Executable.new options[:event], true
9
+ end
10
+
11
+ def fire_event(target, args)
12
+ self.event.execute(target, args)
13
+ end
14
+ def to_s
15
+ "Transition from #{self.from.name} -> #{self.to.name} with event #{self.event}"
16
+ end
17
+ end
18
+ end
data/test/fsm_test.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'test_helper'
2
+
3
+ class FsmTest < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'fsm'
8
+
9
+ class Test::Unit::TestCase
10
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simplificator-fsm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - simplificator
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-05-14 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: info@simplificator.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - LICENSE
24
+ - README.rdoc
25
+ files:
26
+ - LICENSE
27
+ - README.rdoc
28
+ - Rakefile
29
+ - VERSION.yml
30
+ - lib/fsm.rb
31
+ - lib/fsm/errors.rb
32
+ - lib/fsm/executable.rb
33
+ - lib/fsm/machine.rb
34
+ - lib/fsm/machine_config.rb
35
+ - lib/fsm/state.rb
36
+ - lib/fsm/transition.rb
37
+ - test/fsm_test.rb
38
+ - test/test_helper.rb
39
+ has_rdoc: true
40
+ homepage: http://github.com/simplificator/fsm
41
+ post_install_message:
42
+ rdoc_options:
43
+ - --charset=UTF-8
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ version:
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ version:
58
+ requirements: []
59
+
60
+ rubyforge_project:
61
+ rubygems_version: 1.2.0
62
+ signing_key:
63
+ specification_version: 3
64
+ summary: A simple finite state machine (FSM) gem.
65
+ test_files:
66
+ - test/fsm_test.rb
67
+ - test/test_helper.rb