newflow 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|