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 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
@@ -0,0 +1,6 @@
1
+ %w(rubygems rake/gempackagetask spec/rake/spectask).each { |x| require x }
2
+
3
+ Spec::Rake::SpecTask.new("specs") do |t|
4
+ t.spec_opts << '--format' << 'specdoc' << '--colour'
5
+ t.spec_files = Dir.glob("specifications/**/*_spec.rb")
6
+ end
data/readme ADDED
File without changes
@@ -0,0 +1,4 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper"))
2
+
3
+ describe StateMachine do
4
+ end
@@ -0,0 +1,4 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper"))
2
+
3
+ describe StateMachine::State do
4
+ end
@@ -0,0 +1,2 @@
1
+ %w(rubygems spec).each { |x| require x }
2
+ require File.join(File.dirname(__FILE__), "..", "libraries", "fsm")
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
+