beawesomeinstead-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/fsm.gemspec +13 -0
- data/libraries/fsm.rb +11 -0
- data/libraries/fsm/active_record.rb +78 -0
- data/libraries/fsm/event.rb +31 -0
- data/libraries/fsm/state.rb +27 -0
- data/libraries/fsm/state_machine.rb +77 -0
- data/libraries/fsm/transition.rb +34 -0
- data/rakefile +6 -0
- data/readme +0 -0
- data/specifications/integration/fsm_spec.rb +4 -0
- data/specifications/integration/state_spec.rb +4 -0
- data/specifications/spec_helper.rb +2 -0
- metadata +66 -0
data/fsm.gemspec
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Gem::Specification.new do |s|
|
|
2
|
+
s.name = "fsm"
|
|
3
|
+
s.version = "0.1.0"
|
|
4
|
+
s.date = "2008-07-11"
|
|
5
|
+
s.summary = "Finite state machine."
|
|
6
|
+
s.email = "beawesomeinstead@yahoo.com"
|
|
7
|
+
s.homepage = "http://github.com/beawesomeinstead/fsm/wikis"
|
|
8
|
+
s.description = s.summary
|
|
9
|
+
s.authors = ["beawesomeinstead"]
|
|
10
|
+
s.has_rdoc = false
|
|
11
|
+
s.require_paths = ["libraries"]
|
|
12
|
+
s.files = ["libraries/fsm", "libraries/fsm/active_record.rb", "libraries/fsm/event.rb", "libraries/fsm/state.rb", "libraries/fsm/state_machine.rb", "libraries/fsm/transition.rb", "libraries/fsm.rb", "specifications/integration", "specifications/integration/fsm_spec.rb", "specifications/integration/state_spec.rb", "specifications/spec_helper.rb", "rakefile", "readme", "fsm.gemspec"]
|
|
13
|
+
end
|
data/libraries/fsm.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
|
3
|
+
|
|
4
|
+
require "activerecord"
|
|
5
|
+
|
|
6
|
+
%w(state_machine state transition event active_record).map { |x| require "fsm/#{x}" }
|
|
7
|
+
|
|
8
|
+
ActiveRecord::Base.class_eval do
|
|
9
|
+
include StateMachine
|
|
10
|
+
include StateMachine::ORM::ActiveRecord
|
|
11
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module StateMachine #:nodoc:
|
|
2
|
+
module ORM #:nodoc:
|
|
3
|
+
module ActiveRecord #:nodoc:
|
|
4
|
+
def self.included(recipient)
|
|
5
|
+
recipient.send(:extend, ActMethods)
|
|
6
|
+
recipient.send(:extend, ClassMethods)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module ActMethods #:nodoc:
|
|
10
|
+
# ==== Parameters
|
|
11
|
+
# options<Hash>:: An options hash (see below).
|
|
12
|
+
#
|
|
13
|
+
# ==== Options (options)
|
|
14
|
+
# :column<Symbol>:: specifies the column name to use for keeping the state (default: state).
|
|
15
|
+
# :initial<Symbol>:: specifies an initial state for newly created objects (required).
|
|
16
|
+
def state_machine(options)
|
|
17
|
+
raise NoInitialState unless options[:initial]
|
|
18
|
+
|
|
19
|
+
write_inheritable_attribute :states, {}
|
|
20
|
+
write_inheritable_attribute :initial_state, options[:initial]
|
|
21
|
+
write_inheritable_attribute :transition_table, {}
|
|
22
|
+
write_inheritable_attribute :event_table, {}
|
|
23
|
+
write_inheritable_attribute :state_column, options[:column] || "state"
|
|
24
|
+
|
|
25
|
+
class_inheritable_reader :initial_state
|
|
26
|
+
class_inheritable_reader :state_column
|
|
27
|
+
class_inheritable_reader :transition_table
|
|
28
|
+
class_inheritable_reader :event_table
|
|
29
|
+
|
|
30
|
+
before_create :set_initial_state
|
|
31
|
+
after_create :run_initial_state_actions
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module ClassMethods #:nodoc:
|
|
36
|
+
# Wraps ActiveRecord::Base.find to conveniently find all records in a given state.
|
|
37
|
+
# ==== Parameters
|
|
38
|
+
# number<Symbol>:: This is just :first or :all from ActiveRecord +find+.
|
|
39
|
+
# state<String, Symbol>:: The state to find.
|
|
40
|
+
# args:: The rest of the args are passed down to ActiveRecord +find+.
|
|
41
|
+
def find_in_state(number, state, *args)
|
|
42
|
+
with_state_scope state do
|
|
43
|
+
find(number, *args)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Wraps ActiveRecord::Base.count to conveniently count all records in a given state.
|
|
48
|
+
# ==== Parameters
|
|
49
|
+
# state<String, Symbol>:: The state to find.
|
|
50
|
+
# args:: The rest of the args are passed down to ActiveRecord +find+.
|
|
51
|
+
def count_in_state(state, *args)
|
|
52
|
+
with_state_scope state do
|
|
53
|
+
count(*args)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Wraps ActiveRecord::Base.calculate to conveniently calculate all records in a given state.
|
|
58
|
+
# ==== Parameters
|
|
59
|
+
# state<String, Symbol>:: The state to find.
|
|
60
|
+
# args:: The rest of the args are passed down to ActiveRecord +calculate+.
|
|
61
|
+
def calculate_in_state(state, *args)
|
|
62
|
+
with_state_scope state do
|
|
63
|
+
calculate(*args)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
protected
|
|
68
|
+
def with_state_scope(state)
|
|
69
|
+
raise InvalidState unless states.include?(state)
|
|
70
|
+
|
|
71
|
+
with_scope :find => {:conditions => ["#{table_name}.#{state_column} = ?", state.to_s]} do
|
|
72
|
+
yield if block_given?
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module StateMachine #:nodoc:
|
|
2
|
+
class Event #:nodoc:
|
|
3
|
+
attr_reader :name, :transitions, :options
|
|
4
|
+
|
|
5
|
+
def initialize(name, options, transition_table, &block)
|
|
6
|
+
@name = name.to_sym
|
|
7
|
+
@transitions = transition_table[@name] = []
|
|
8
|
+
instance_eval(&block) if block
|
|
9
|
+
@options = options
|
|
10
|
+
@options.freeze
|
|
11
|
+
@transitions.freeze
|
|
12
|
+
freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def next_states(record)
|
|
16
|
+
@transitions.select { |t| t.from == record.current_state }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def fire(record)
|
|
20
|
+
next_states(record).each do |transition|
|
|
21
|
+
break true if transition.perform(record)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def transitions(transition_options)
|
|
26
|
+
Array(transition_options[:from]).each do |s|
|
|
27
|
+
@transitions << Transition.new(transition_options.merge({:from => s.to_sym}))
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module StateMachine #:nodoc:
|
|
2
|
+
class State #:nodoc:
|
|
3
|
+
attr_reader :name
|
|
4
|
+
|
|
5
|
+
def initialize(name, options)
|
|
6
|
+
@name, @options = name, options
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def entering(record)
|
|
10
|
+
enteract = @options[:enter]
|
|
11
|
+
record.send(:run_transition_action, enteract) if enteract
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def entered(record)
|
|
15
|
+
afteractions = @options[:after]
|
|
16
|
+
return unless afteractions
|
|
17
|
+
Array(afteractions).each do |afteract|
|
|
18
|
+
record.send(:run_transition_action, afteract)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def exited(record)
|
|
23
|
+
exitact = @options[:exit]
|
|
24
|
+
record.send(:run_transition_action, exitact) if exitact
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module StateMachine #:nodoc:
|
|
2
|
+
class InvalidState < Exception; end
|
|
3
|
+
class NoInitialState < Exception; end
|
|
4
|
+
|
|
5
|
+
def self.included(recipient)
|
|
6
|
+
recipient.send(:extend, ClassMethods)
|
|
7
|
+
recipient.send(:include, InstanceMethods)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module InstanceMethods #:nodoc:
|
|
11
|
+
def set_initial_state
|
|
12
|
+
write_attribute(self.class.state_column, self.class.initial_state.to_s)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run_initial_state_actions
|
|
16
|
+
initial = self.class.read_inheritable_attribute(:states)[self.class.initial_state.to_sym]
|
|
17
|
+
initial.entering(self)
|
|
18
|
+
initial.entered(self)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Returns the current state the object is in, as a Ruby symbol.
|
|
22
|
+
def current_state
|
|
23
|
+
self.send(self.class.state_column).to_sym
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns what the next state for a given event would be, as a Ruby symbol.
|
|
27
|
+
def next_state_for_event(event)
|
|
28
|
+
ns = next_states_for_event(event)
|
|
29
|
+
ns.empty? ? nil : ns.first.to
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def next_states_for_event(event)
|
|
33
|
+
self.class.read_inheritable_attribute(:transition_table)[event.to_sym].select do |s|
|
|
34
|
+
s.from == current_state
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
def run_transition_action(action)
|
|
40
|
+
Symbol === action ? self.method(action).call : action.call(self)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
module ClassMethods #:nodoc:
|
|
45
|
+
# Returns an array of all known states.
|
|
46
|
+
def states
|
|
47
|
+
read_inheritable_attribute(:states).keys
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Define an event. This takes a block which describes all valid transitions for this event.
|
|
51
|
+
# This creates an instance method used for firing the event. The method created is the name of the event followed
|
|
52
|
+
# by an exclamation point (!).
|
|
53
|
+
# ==== Parameters
|
|
54
|
+
# transitions<Hash>:: takes a hash where <tt>:to</tt> is the state to transition to and <tt>:from</tt> is a state
|
|
55
|
+
# (or Array of states) from which this event can be fired.
|
|
56
|
+
def event(event, options = {}, &block)
|
|
57
|
+
tt = read_inheritable_attribute(:transition_table)
|
|
58
|
+
|
|
59
|
+
et = read_inheritable_attribute(:event_table)
|
|
60
|
+
e = et[event.to_sym] = Event.new(event, options, tt, &block)
|
|
61
|
+
define_method("#{event.to_s}!") { e.fire(self) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Define a state of the system. +state+ can take an optional Proc object which will be executed every time the
|
|
65
|
+
# system transitions into that state. The proc will be passed the current object.
|
|
66
|
+
def state(*args)
|
|
67
|
+
options = args.extract_options!
|
|
68
|
+
|
|
69
|
+
args.each do |name|
|
|
70
|
+
state = State.new(name.to_sym, options)
|
|
71
|
+
read_inheritable_attribute(:states)[name.to_sym] = state
|
|
72
|
+
|
|
73
|
+
define_method("#{state.name}?") { current_state == state.name }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module StateMachine #:nodoc:
|
|
2
|
+
class Transition #:nodoc:
|
|
3
|
+
attr_reader :from, :to, :options
|
|
4
|
+
|
|
5
|
+
def initialize(options)
|
|
6
|
+
@from, @to, @guard = options[:from], options[:to], options[:guard]
|
|
7
|
+
@options = options
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def guard(object)
|
|
11
|
+
@guard ? object.send(:run_transition_action, @guard) : true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def perform(record)
|
|
15
|
+
return false unless guard(record)
|
|
16
|
+
loopback = record.current_state == to
|
|
17
|
+
states = record.class.read_inheritable_attribute(:states)
|
|
18
|
+
next_state = states[to]
|
|
19
|
+
old_state = states[record.current_state]
|
|
20
|
+
|
|
21
|
+
next_state.entering(record) unless loopback
|
|
22
|
+
|
|
23
|
+
record.update_attribute(record.class.state_column, to.to_s)
|
|
24
|
+
|
|
25
|
+
next_state.entered(record) unless loopback
|
|
26
|
+
old_state.exited(record) unless loopback
|
|
27
|
+
true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ==(object)
|
|
31
|
+
@from == object.from && @to == object.to
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/rakefile
ADDED
data/readme
ADDED
|
File without changes
|
metadata
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: beawesomeinstead-fsm
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- beawesomeinstead
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2008-07-11 00:00:00 -07:00
|
|
13
|
+
default_executable:
|
|
14
|
+
dependencies: []
|
|
15
|
+
|
|
16
|
+
description: Finite state machine.
|
|
17
|
+
email: beawesomeinstead@yahoo.com
|
|
18
|
+
executables: []
|
|
19
|
+
|
|
20
|
+
extensions: []
|
|
21
|
+
|
|
22
|
+
extra_rdoc_files: []
|
|
23
|
+
|
|
24
|
+
files:
|
|
25
|
+
- libraries/fsm
|
|
26
|
+
- libraries/fsm/active_record.rb
|
|
27
|
+
- libraries/fsm/event.rb
|
|
28
|
+
- libraries/fsm/state.rb
|
|
29
|
+
- libraries/fsm/state_machine.rb
|
|
30
|
+
- libraries/fsm/transition.rb
|
|
31
|
+
- libraries/fsm.rb
|
|
32
|
+
- specifications/integration
|
|
33
|
+
- specifications/integration/fsm_spec.rb
|
|
34
|
+
- specifications/integration/state_spec.rb
|
|
35
|
+
- specifications/spec_helper.rb
|
|
36
|
+
- rakefile
|
|
37
|
+
- readme
|
|
38
|
+
- fsm.gemspec
|
|
39
|
+
has_rdoc: false
|
|
40
|
+
homepage: http://github.com/beawesomeinstead/fsm/wikis
|
|
41
|
+
post_install_message:
|
|
42
|
+
rdoc_options: []
|
|
43
|
+
|
|
44
|
+
require_paths:
|
|
45
|
+
- libraries
|
|
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: 2
|
|
64
|
+
summary: Finite state machine.
|
|
65
|
+
test_files: []
|
|
66
|
+
|