newflow 1.0.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/lib/newflow.rb +95 -0
- data/lib/newflow/state.rb +61 -0
- data/lib/newflow/transition.rb +46 -0
- data/lib/newflow/trigger.rb +20 -0
- data/lib/newflow/workflow.rb +103 -0
- data/spec/newflow_spec.rb +62 -0
- data/spec/newflow_state_spec.rb +72 -0
- data/spec/newflow_transition_spec.rb +109 -0
- data/spec/newflow_trigger_spec.rb +50 -0
- data/spec/newflow_workflow_spec.rb +97 -0
- data/spec/spec_helper.rb +5 -0
- metadata +70 -0
data/lib/newflow.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
# Require all the files
|
5
|
+
base_dir = File.dirname(__FILE__) + "/newflow"
|
6
|
+
Dir["#{base_dir}/*.rb"].each { |f| require f }
|
7
|
+
|
8
|
+
# TODO: Allow workflows to identify themselves (for logging and stuffs)
|
9
|
+
module Newflow
|
10
|
+
class InvalidStateDefinitionError < ArgumentError; end
|
11
|
+
class InvalidWorkflowStateError < ArgumentError;
|
12
|
+
def initialize(state)
|
13
|
+
@state = state
|
14
|
+
end
|
15
|
+
|
16
|
+
def message
|
17
|
+
"'#{@state}' is not a valid state"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
WITH_SIDE_EFFECTS = true
|
22
|
+
WITHOUT_SIDE_EFFECTS = false
|
23
|
+
|
24
|
+
def self.included(base)
|
25
|
+
base.send(:include, InstanceMethods)
|
26
|
+
if base.ancestors.map {|a| a.to_s }.include?("ActiveRecord::Base")
|
27
|
+
base.send(:include, ActiveRecordInstantiator)
|
28
|
+
else
|
29
|
+
base.send(:include, NonActiveRecordInstantiator)
|
30
|
+
end
|
31
|
+
base.send(:extend, ClassMethods, Forwardable)
|
32
|
+
base.def_delegators :workflow, :transition!, :transition_once!, :current_state, :current_state=,
|
33
|
+
:would_transition_to
|
34
|
+
end
|
35
|
+
|
36
|
+
module ActiveRecordInstantiator # TODO: TEST
|
37
|
+
def after_initialize_with_workflow
|
38
|
+
after_initialize_without_workflow if respond_to?(:after_initialize_without_workflow)
|
39
|
+
workflow # This will set the workflow_state
|
40
|
+
end
|
41
|
+
if respond_to?(:after_initialize)
|
42
|
+
alias_method :after_initialize_without_workflow, :after_initialize
|
43
|
+
end
|
44
|
+
alias_method :after_initialize, :after_initialize_with_workflow
|
45
|
+
end
|
46
|
+
|
47
|
+
module NonActiveRecordInstantiator
|
48
|
+
def initialize_with_workflow(*args, &block)
|
49
|
+
initialize_without_workflow(*args, &block)
|
50
|
+
workflow # This will set the workflow_state
|
51
|
+
end
|
52
|
+
alias_method :initialize_without_workflow, :initialize
|
53
|
+
alias_method :initialize, :initialize_with_workflow
|
54
|
+
end
|
55
|
+
|
56
|
+
module InstanceMethods
|
57
|
+
def workflow
|
58
|
+
@workflow ||= Workflow.new(self, self.class.__workflow_definition)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
module ClassMethods
|
63
|
+
def define_workflow(&workflow_definition)
|
64
|
+
@__workflow_definition = workflow_definition
|
65
|
+
__define_query_methods(workflow_definition)
|
66
|
+
end
|
67
|
+
|
68
|
+
def __define_query_methods(workflow_definition)
|
69
|
+
@state_catcher = Object.new
|
70
|
+
@state_catcher.instance_variable_set("@states", [])
|
71
|
+
def @state_catcher.states; @states; end
|
72
|
+
def @state_catcher.state(name, *args); @states << name; end
|
73
|
+
@state_catcher.instance_eval &workflow_definition
|
74
|
+
@state_catcher.states.each do |state|
|
75
|
+
self.send(:define_method, "#{state}?") do
|
76
|
+
workflow.send("#{state}?")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def __workflow_definition
|
82
|
+
@__workflow_definition
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.logger
|
87
|
+
return @logger if @logger
|
88
|
+
@logger = if defined?(Rails)
|
89
|
+
Rails.logger
|
90
|
+
else
|
91
|
+
Logger.new(File.open('/dev/null', 'w'))
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Newflow
|
2
|
+
class State
|
3
|
+
attr_reader :name, :transitions
|
4
|
+
|
5
|
+
def initialize(name, opts={}, &transitions_block)
|
6
|
+
logger.debug "State.initialize: name=#{name} opts=#{opts.inspect}"
|
7
|
+
@name = name
|
8
|
+
@opts = opts
|
9
|
+
@is_start = opts[:start]
|
10
|
+
@is_stop = opts[:stop]
|
11
|
+
@on_entry = Trigger.new(opts[:on_entry])
|
12
|
+
@transitions = []
|
13
|
+
check_validity
|
14
|
+
instance_eval &transitions_block if transitions_block
|
15
|
+
end
|
16
|
+
|
17
|
+
def transitions_to(target_state, opts={})
|
18
|
+
@transitions << Transition.new(target_state,opts)
|
19
|
+
end
|
20
|
+
|
21
|
+
def run(workflow, do_trigger=Newflow::WITH_SIDE_EFFECTS)
|
22
|
+
return @name unless @transitions
|
23
|
+
# We may want to consider looking at all transitions and letting user know
|
24
|
+
# that you can move in multiple directions
|
25
|
+
transition_to = @transitions.detect { |t| t.can_transition?(workflow) }
|
26
|
+
if transition_to
|
27
|
+
transition_to.trigger!(workflow) if do_trigger # TODO: TEST
|
28
|
+
transition_to.target_state
|
29
|
+
else
|
30
|
+
@name
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# TODO: use convention of name == :start instead of a :start opt, not same for stop
|
35
|
+
def start?
|
36
|
+
@opts[:start]
|
37
|
+
end
|
38
|
+
|
39
|
+
def stop?
|
40
|
+
@opts[:stop]
|
41
|
+
end
|
42
|
+
|
43
|
+
def run_on_entry(workflow, do_trigger=Newflow::WITH_SIDE_EFFECTS)
|
44
|
+
@on_entry.run!(workflow) if do_trigger && @on_entry
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
@name.to_s
|
49
|
+
end
|
50
|
+
|
51
|
+
def logger
|
52
|
+
Newflow.logger
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
def check_validity
|
57
|
+
raise "State #{name} cannot be both a start and a stop" if start? && stop?
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Newflow
|
2
|
+
class Transition
|
3
|
+
attr_reader :target_state, :predicate, :predicate_name, :trigger
|
4
|
+
|
5
|
+
def initialize(target_state,opts)
|
6
|
+
@target_state = target_state
|
7
|
+
if_meth = opts[:if]
|
8
|
+
unless_meth = opts[:unless]
|
9
|
+
@trigger = Trigger.new(opts[:trigger])
|
10
|
+
logger.debug "State.transitions_to: target_state=#{target_state} if=#{if_meth.inspect} unless=#{unless_meth.inspect} trigger=#{@trigger}"
|
11
|
+
unless @target_state \
|
12
|
+
&& (if_meth || unless_meth) \
|
13
|
+
&& !(if_meth && unless_meth)
|
14
|
+
raise "You must specify a target state(#@target_state) and (if_method OR unless_method)"
|
15
|
+
end
|
16
|
+
@predicate_name = (if_meth || "!#{unless_meth}").to_s
|
17
|
+
@predicate = if if_meth
|
18
|
+
# TODO: be smart
|
19
|
+
if if_meth.respond_to?(:call)
|
20
|
+
lambda { |wf| if_meth.call }
|
21
|
+
else
|
22
|
+
lambda { |wf| wf.send(if_meth) }
|
23
|
+
end
|
24
|
+
else
|
25
|
+
if unless_meth.respond_to?(:call)
|
26
|
+
lambda { |wf| !unless_meth.call }
|
27
|
+
else
|
28
|
+
lambda { |wf| !wf.send(unless_meth) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def can_transition?(workflow)
|
34
|
+
predicate.call(workflow)
|
35
|
+
end
|
36
|
+
|
37
|
+
def trigger!(workflow)
|
38
|
+
trigger.run!(workflow)
|
39
|
+
end
|
40
|
+
|
41
|
+
def logger
|
42
|
+
Newflow.logger
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Newflow
|
2
|
+
class Trigger
|
3
|
+
def initialize(trigger)
|
4
|
+
@trigger = trigger
|
5
|
+
end
|
6
|
+
|
7
|
+
def run!(workflow)
|
8
|
+
return false unless @trigger
|
9
|
+
case @trigger
|
10
|
+
when Symbol
|
11
|
+
workflow.send(@trigger)
|
12
|
+
when Array
|
13
|
+
@trigger.each {|t| Trigger.new(t).run!(workflow) }
|
14
|
+
else
|
15
|
+
@trigger.call
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Newflow
|
2
|
+
class Workflow
|
3
|
+
def initialize(extendee, definition)
|
4
|
+
@extendee = extendee
|
5
|
+
construct_workflow!(definition)
|
6
|
+
end
|
7
|
+
|
8
|
+
def validate_workflow!
|
9
|
+
# TODO: Validate that all transitions reach a valid state
|
10
|
+
# TODO: Validate that there is at least one stop state
|
11
|
+
raise InvalidStateDefinitionError.new("#{@extendee.class} needs at least two states") if states.size < 2
|
12
|
+
raise InvalidStateDefinitionError.new("#{@extendee.class} needs a start of the workflow") unless current_state
|
13
|
+
end
|
14
|
+
|
15
|
+
def states
|
16
|
+
@states ||= {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def state(name, opts={}, &block)
|
20
|
+
# TODO: Assert we're not overriding a state
|
21
|
+
states[name] = State.new(name, opts, &block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def construct_workflow!(definition)
|
25
|
+
instance_eval &definition
|
26
|
+
start_state = states.values.detect { |s| s.start? }
|
27
|
+
@extendee.workflow_state ||= start_state.name.to_s if start_state
|
28
|
+
validate_workflow!
|
29
|
+
define_state_query_methods
|
30
|
+
raise InvalidWorkflowStateError.new(current_state) unless states[current_state]
|
31
|
+
end
|
32
|
+
|
33
|
+
def define_state_query_methods
|
34
|
+
states.keys.each do |key|
|
35
|
+
instance_eval <<-EOS
|
36
|
+
def #{key}?; current_state == :#{key}; end
|
37
|
+
EOS
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def transition_once!(do_trigger=Newflow::WITH_SIDE_EFFECTS)
|
42
|
+
state = states[current_state]
|
43
|
+
raise InvalidWorkflowStateError.new(current_state) unless state # TODO: TEST
|
44
|
+
target_state = states[state.run(@extendee, do_trigger)]
|
45
|
+
if state != target_state
|
46
|
+
@extendee.workflow_state = target_state.to_s
|
47
|
+
target_state.run_on_entry(@extendee, do_trigger)
|
48
|
+
end
|
49
|
+
target_state
|
50
|
+
end
|
51
|
+
|
52
|
+
def transition!(do_trigger=Newflow::WITH_SIDE_EFFECTS)
|
53
|
+
# TODO: watch out for max # of transits
|
54
|
+
previous_state = current_state
|
55
|
+
previous_states = {}
|
56
|
+
num_transitions = 0
|
57
|
+
begin
|
58
|
+
if previous_states[current_state]
|
59
|
+
raise "Error: possible [infinite] loop in workflow, started in: #{previous_state}, currently in #{current_state}, been through all of (#{previous_states.keys.map(&:to_s).sort.join(", ")})" # TODO: TEST
|
60
|
+
end
|
61
|
+
previous_states[current_state] = true
|
62
|
+
the_state = current_state
|
63
|
+
transition_once!(do_trigger)
|
64
|
+
end while the_state != current_state && states[current_state]
|
65
|
+
previous_state == current_state ? nil : current_state
|
66
|
+
ensure
|
67
|
+
@extendee.workflow_state = previous_state unless do_trigger
|
68
|
+
end
|
69
|
+
|
70
|
+
def would_transition_to
|
71
|
+
transition!(Newflow::WITHOUT_SIDE_EFFECTS)
|
72
|
+
end
|
73
|
+
|
74
|
+
def current_state
|
75
|
+
@extendee.workflow_state.to_sym
|
76
|
+
end
|
77
|
+
|
78
|
+
def current_state=(state)
|
79
|
+
@extendee.workflow_state = state.to_s
|
80
|
+
end
|
81
|
+
|
82
|
+
def to_dotty
|
83
|
+
dot = ""
|
84
|
+
dot << "digraph {\n"
|
85
|
+
states.keys.each { |state_name|
|
86
|
+
state = states[state_name]
|
87
|
+
# it'd be nice to have the current state somehow shown visually
|
88
|
+
shape = "circle"
|
89
|
+
if state_name == current_state
|
90
|
+
puts "setting current shape to doublecircle #{state_name} vs #{current_state}"
|
91
|
+
shape = "doublecircle"
|
92
|
+
end
|
93
|
+
dot << %Q[ "#{state_name}" [ shape = #{shape} ]; \n]
|
94
|
+
state.transitions.each { |transition|
|
95
|
+
dot << " \"#{state_name}\" -> \"#{transition.target_state}\" [ label = \"#{transition.predicate_name}\" ];\n"
|
96
|
+
}
|
97
|
+
}
|
98
|
+
dot << "}\n"
|
99
|
+
return dot
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe "An object including Newflow" do
|
4
|
+
before do
|
5
|
+
klass = Class.new do
|
6
|
+
attr_accessor :workflow_state
|
7
|
+
include Newflow
|
8
|
+
|
9
|
+
define_workflow do
|
10
|
+
state :start, :start => true do
|
11
|
+
transitions_to :finish, :if => :go_to_finish?
|
12
|
+
end
|
13
|
+
|
14
|
+
state :finish, :stop => true
|
15
|
+
end
|
16
|
+
|
17
|
+
def go_to_finish?
|
18
|
+
true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
@obj = klass.new
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should begin in start state" do
|
25
|
+
@obj.should be_start
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should stop in the finish state" do
|
29
|
+
@obj.transition!
|
30
|
+
@obj.should be_finish
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should transition once" do
|
34
|
+
@obj.transition_once!
|
35
|
+
@obj.should be_finish
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should have a current state" do
|
39
|
+
@obj.current_state.should == :start
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should have a workflow state" do
|
43
|
+
@obj.workflow_state.should == "start"
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should have a way to manually change the current state" do
|
47
|
+
@obj.current_state = :finish
|
48
|
+
@obj.workflow_state.should == "finish"
|
49
|
+
@obj.should be_finish
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should not eat all missing methods" do
|
53
|
+
lambda { @obj.wammo! }.should raise_error(NoMethodError)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should keep the state even when the workflow is reset" do
|
57
|
+
@obj.workflow_state = "finish"
|
58
|
+
@obj.instance_variable_set("@workflow", nil)
|
59
|
+
@obj.should be_finish
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe "A valid start state" do
|
4
|
+
before do
|
5
|
+
@name = :start
|
6
|
+
@workflow = mock("workflow")
|
7
|
+
@state = Newflow::State.new(@name, :start => true) do
|
8
|
+
transitions_to :finish, :if => :go_to_finish?
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should have a name" do
|
13
|
+
@state.name.should == @name
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should be a start" do
|
17
|
+
@state.should be_start
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should turn itself into a string" do
|
21
|
+
@state.to_s.should == @name.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should should not bomb when running on entry" do
|
25
|
+
lambda { @state.run_on_entry(@extendee) }.should_not raise_error
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should return the transition state when the predicate is true" do
|
29
|
+
@workflow.should_receive(:go_to_finish?).and_return true
|
30
|
+
@state.run(@workflow).should == :finish
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should return the its own state when the predicate is false" do
|
34
|
+
@workflow.should_receive(:go_to_finish?).and_return false
|
35
|
+
@state.run(@workflow).should == :start
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "A state with on_entry" do
|
40
|
+
before do
|
41
|
+
@name = :start
|
42
|
+
@extendee = mock("extendee")
|
43
|
+
@state = Newflow::State.new(@name, :on_entry => :make_pizza)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should run on entry when triggers are on (default)" do
|
47
|
+
@extendee.should_receive(:make_pizza)
|
48
|
+
@state.run_on_entry(@extendee)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should not run on entry when triggers are off" do
|
52
|
+
@extendee.should_not_receive(:make_pizza)
|
53
|
+
@state.run_on_entry(@extendee, Newflow::WITHOUT_SIDE_EFFECTS)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "A valid stop state" do
|
58
|
+
before do
|
59
|
+
@name = :stop
|
60
|
+
@state = Newflow::State.new(@name, :stop => true)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should be a stop" do
|
64
|
+
@state.should be_stop
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "Invalid states" do
|
69
|
+
it "should not have both a start and a stop" do
|
70
|
+
lambda { @state = Newflow::State.new(:hi, :start => true, :stop => true) }.should raise_error
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe "An :if symbol transition with a symbol trigger" do
|
4
|
+
before do
|
5
|
+
@workflow = mock("workflow")
|
6
|
+
@transition = Newflow::Transition.new(:target_state, :if => :predicate?, :trigger => :some_action)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should not be able to transition if predicate is false" do
|
10
|
+
@workflow.should_receive(:predicate?).and_return false
|
11
|
+
@transition.can_transition?(@workflow).should be_false
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should be able to transition if predicate is true" do
|
15
|
+
@workflow.should_receive(:predicate?).and_return true
|
16
|
+
@transition.can_transition?(@workflow).should be_true
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should call the trigger if requested" do
|
20
|
+
@workflow.should_receive(:some_action)
|
21
|
+
@transition.trigger!(@workflow)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should have a predicate name" do
|
25
|
+
@transition.predicate_name.should == "predicate?"
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should have a target state" do
|
29
|
+
@transition.target_state.should == :target_state
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "An :unless symbol transition with a proc trigger" do
|
34
|
+
before do
|
35
|
+
@workflow = mock("workflow")
|
36
|
+
@trigger_ran = false
|
37
|
+
@trigger = lambda { @trigger_ran = true }
|
38
|
+
@transition = Newflow::Transition.new(:target_state, :unless => :predicate?, :trigger => @trigger)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should not be able to transition if predicate is true" do
|
42
|
+
@workflow.should_receive(:predicate?).and_return true
|
43
|
+
@transition.can_transition?(@workflow).should be_false
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should be able to transition if predicate is false" do
|
47
|
+
@workflow.should_receive(:predicate?).and_return false
|
48
|
+
@transition.can_transition?(@workflow).should be_true
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should call the trigger when requested" do
|
52
|
+
@transition.trigger!(@workflow)
|
53
|
+
@trigger_ran.should be_true
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should have a predicate name" do
|
57
|
+
@transition.predicate_name.should == "!predicate?"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "An :if proc transition with no trigger" do
|
62
|
+
before do
|
63
|
+
@workflow = mock("workflow")
|
64
|
+
@if_proc = lambda { @predicate_value }
|
65
|
+
@transition = Newflow::Transition.new(:target_state, :if => @if_proc)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should not be able to transition if predicate is false" do
|
69
|
+
@predicate_value = false
|
70
|
+
@transition.can_transition?(@workflow).should be_false
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should be able to transition if predicate is true" do
|
74
|
+
@predicate_value = true
|
75
|
+
@transition.can_transition?(@workflow).should be_true
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should do nothing when triggered" do
|
79
|
+
lambda { @transition.trigger!(@workflow) }.should_not raise_error
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe "An :unless proc transition with no trigger" do
|
84
|
+
before do
|
85
|
+
@workflow = mock("workflow")
|
86
|
+
@unless_proc = lambda { @predicate_value }
|
87
|
+
@transition = Newflow::Transition.new(:target_state, :unless => @unless_proc)
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should not be able to transition if predicate is true" do
|
91
|
+
@predicate_value = true
|
92
|
+
@transition.can_transition?(@workflow).should be_false
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should be able to transition if predicate is false" do
|
96
|
+
@predicate_value = false
|
97
|
+
@transition.can_transition?(@workflow).should be_true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe "Invalid triggers" do
|
102
|
+
it "should not be valid without an if or unless" do
|
103
|
+
lambda { @transition = Newflow::Transition.new(:target_state) }.should raise_error
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should not be valid with an if and an unless" do
|
107
|
+
lambda { @transition = Newflow::Transition.new(:target_state, :unless => :unless, :if => :if) }.should raise_error
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe "An empty trigger" do
|
4
|
+
before do
|
5
|
+
@extendee = mock("extendee")
|
6
|
+
@trigger = Newflow::Trigger.new(nil)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should do nothing when running" do
|
10
|
+
lambda { @trigger.run!(@extendee) }.should_not raise_error
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "A symbol trigger" do
|
15
|
+
before do
|
16
|
+
@extendee = mock("extendee")
|
17
|
+
@trigger = Newflow::Trigger.new(:make_pizza)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should run the trigger on the workflow" do
|
21
|
+
@extendee.should_receive(:make_pizza)
|
22
|
+
@trigger.run!(@extendee)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "An array trigger" do
|
27
|
+
before do
|
28
|
+
@extendee = mock("extendee")
|
29
|
+
@trigger = Newflow::Trigger.new([:make_pizza, :make_cake])
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should run the triggers on the workflow" do
|
33
|
+
@extendee.should_receive(:make_pizza)
|
34
|
+
@extendee.should_receive(:make_cake)
|
35
|
+
@trigger.run!(@extendee)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "A lambda trigger" do
|
40
|
+
before do
|
41
|
+
@extendee = mock("extendee")
|
42
|
+
@trigger = Newflow::Trigger.new(lambda { @extendee.make_pizza })
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should run the triggers on the workflow" do
|
46
|
+
@extendee.should_receive(:make_pizza)
|
47
|
+
@trigger.run!(@extendee)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe "A workflow" do
|
4
|
+
before do
|
5
|
+
@klass = Class.new do
|
6
|
+
attr_accessor :workflow_state
|
7
|
+
end
|
8
|
+
@obj = @klass.new
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "A workflow with no states" do
|
12
|
+
before do
|
13
|
+
@definition = lambda {}
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should raise an error on creation" do
|
17
|
+
lambda { Newflow::Workflow.new(@obj, @definition) }.should raise_error(Newflow::InvalidStateDefinitionError)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "A workflow with one state" do
|
22
|
+
before do
|
23
|
+
@definition = lambda {
|
24
|
+
state :start
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should raise an error on creation" do
|
29
|
+
lambda { Newflow::Workflow.new(@obj, @definition) }.should raise_error(Newflow::InvalidStateDefinitionError)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "The minimal valid workflow" do
|
34
|
+
before do
|
35
|
+
@definition = lambda {
|
36
|
+
state :start, :start => true do
|
37
|
+
transitions_to :finish, :if => :go_to_finish?
|
38
|
+
end
|
39
|
+
|
40
|
+
state :finish, :on_entry => :make_pizza, :stop => true
|
41
|
+
}
|
42
|
+
|
43
|
+
@klass.send(:define_method, :go_to_finish?) do
|
44
|
+
true
|
45
|
+
end
|
46
|
+
@klass.send(:define_method, :make_pizza) do
|
47
|
+
"yum"
|
48
|
+
end
|
49
|
+
@workflow = Newflow::Workflow.new(@obj, @definition)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should begin in start state" do
|
53
|
+
@workflow.should be_start
|
54
|
+
@obj.workflow_state.should == "start"
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should trigger the on_entry when going to finish" do
|
58
|
+
@obj.should_receive :make_pizza
|
59
|
+
@workflow.transition!
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should be able to transition to the finish state" do
|
63
|
+
state = @workflow.would_transition_to
|
64
|
+
state.should == :finish
|
65
|
+
@workflow.should be_start
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should stop in the finish state" do
|
69
|
+
@workflow.transition!
|
70
|
+
@workflow.should be_finish
|
71
|
+
@obj.workflow_state.should == "finish"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "A workflow with an invalid object" do
|
76
|
+
before do
|
77
|
+
@definition = lambda {
|
78
|
+
state :start, :start => true do
|
79
|
+
transitions_to :finish, :if => :go_to_finish?
|
80
|
+
end
|
81
|
+
|
82
|
+
state :finish, :stop => true
|
83
|
+
}
|
84
|
+
|
85
|
+
@klass.send(:define_method, :go_to_finish?) do
|
86
|
+
true
|
87
|
+
end
|
88
|
+
@obj.workflow_state = "invalid"
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should raise an error on instantiation" do
|
92
|
+
lambda { Newflow::Workflow.new(@obj, @definition) }.should raise_error(Newflow::InvalidWorkflowStateError)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: newflow
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Trotter Cashion
|
8
|
+
- Kyle Burton
|
9
|
+
- Aaron Feng
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
|
14
|
+
date: 2009-10-02 00:00:00 -04:00
|
15
|
+
default_executable:
|
16
|
+
dependencies: []
|
17
|
+
|
18
|
+
description: |
|
19
|
+
Newflow provides a way to add workflows to existing objects. It uses
|
20
|
+
a simple dsl to add guards and triggers to states and their transitions.
|
21
|
+
|
22
|
+
email: cashion@gmail.com
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files: []
|
28
|
+
|
29
|
+
files:
|
30
|
+
- lib/newflow/state.rb
|
31
|
+
- lib/newflow/transition.rb
|
32
|
+
- lib/newflow/trigger.rb
|
33
|
+
- lib/newflow/workflow.rb
|
34
|
+
- lib/newflow.rb
|
35
|
+
- spec/newflow_spec.rb
|
36
|
+
- spec/newflow_state_spec.rb
|
37
|
+
- spec/newflow_transition_spec.rb
|
38
|
+
- spec/newflow_trigger_spec.rb
|
39
|
+
- spec/newflow_workflow_spec.rb
|
40
|
+
- spec/spec_helper.rb
|
41
|
+
has_rdoc: true
|
42
|
+
homepage: http://trottercashion.com
|
43
|
+
licenses: []
|
44
|
+
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options: []
|
47
|
+
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: "0"
|
55
|
+
version:
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: "0"
|
61
|
+
version:
|
62
|
+
requirements: []
|
63
|
+
|
64
|
+
rubyforge_project:
|
65
|
+
rubygems_version: 1.3.5
|
66
|
+
signing_key:
|
67
|
+
specification_version: 3
|
68
|
+
summary: Add workflows (state transitions) to objects.
|
69
|
+
test_files: []
|
70
|
+
|