flowchart 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE.txt ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2013 Shuddhashil Ray(rayshuddhashil@gmail.com)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,158 @@
1
+ = FlowChart
2
+
3
+ - Flowchart is a rubygem for State Machine(state-action-transition) workflow.
4
+ - Flowchart works out of the box both for ActiveRecord and Non-ActiveRecord Models.
5
+ - It provides an easy DSL to create a state machine flow for your model's object.
6
+
7
+ == INSTALLATION
8
+
9
+ gem install flowchart
10
+
11
+ # No other gem dependencies! Tested on Ruby 1.8.7 and Ruby 1.9.3
12
+
13
+ == SYNTAX & OPTIONS
14
+
15
+ flowchart do
16
+ init_flowstate :init
17
+
18
+ flowstate :init do
19
+ preprocess Proc.new { |o| p "blah blah" }
20
+ postprocess :some_method
21
+ end
22
+
23
+ flowstate :uploaded do
24
+ preprocess Proc.new :do_something
25
+ postprocess Proc.new { |o| p "File has been uploaded" }
26
+ end
27
+
28
+ action :upload do
29
+ transitions :from => :init, :to => :uploaded, :condition => :file_parsable?
30
+ end
31
+ end
32
+
33
+ # OPTIONS
34
+ 1. By Default flowchart assumes that the model attribute it works on is "process_status. If you want to override it then :
35
+ flowchart do
36
+ state_column :some_other_column
37
+ .....
38
+ end
39
+
40
+ 2. Every flowstate can have options preprocess and postprocess blocks. You can mention any instance methods or Procs/lamdas to execute.
41
+
42
+ 3. In Actions you can mention :condition with evaluate truth or falsity based on which the transition action(:from -> :to) is possible or not is determined. If :condition is not given transition is always evaluated to truth.
43
+
44
+ 4. In Actions you can also mention :branch, when you have multiple :to states from one single :from state then you can mention a ":branch" condition which you can execute and determine which branch state will be the next! Useful in decision trees.
45
+
46
+ # Methods
47
+
48
+ 1. obj.{state_name}? returns boolean true/false if object is in that that state
49
+
50
+ 2. obj.{action} returns the transtion to next state on executing the action
51
+
52
+ 3. obj.{action}! returns the transition to next state on executing the action and save after call
53
+
54
+ 4. obj.current_state returns the current_state of the object
55
+
56
+ 5. obj.flowprocessor returns the complete tree of flowchart for the object!
57
+
58
+
59
+
60
+ == Sample Use
61
+
62
+ # Example of a Non-ActiveRecord Class using FlowChart in Ruby
63
+ # in irb console
64
+
65
+ class SampleOne
66
+ include FlowChart
67
+ attr_accessor :process_status
68
+
69
+ flowchart do
70
+ init_flowstate :init
71
+
72
+ flowstate :init do
73
+ preprocess Proc.new { |o| p "Initializing File" }
74
+ postprocess :notify_user
75
+ end
76
+
77
+ flowstate :uploaded do
78
+ preprocess Proc.new { |o| p "Validating File" }
79
+ postprocess Proc.new { |o| p "File has been uploaded in system" }
80
+ end
81
+
82
+ flowstate :open do
83
+ postprocess :notify_user
84
+ postprocess Proc.new { |o| p "File has been closed" }
85
+ end
86
+
87
+ flowstate :closed do
88
+ preprocess Proc.new { |o| p "File closed" }
89
+ postprocess :notify_user
90
+ end
91
+
92
+ action :upload do
93
+ transitions :from => :init, :to => :uploaded, :condition => :file_parsable?
94
+ end
95
+
96
+ action :process do
97
+ transitions :from => :uploaded, :to => [:open, :closed], :branch => :choose_branch
98
+ end
99
+
100
+ action :close do
101
+ transitions :from => [:init,:uploaded,:open], :to => :closed, :condition => :file_close?
102
+ end
103
+
104
+ end
105
+
106
+ def choose_branch
107
+ 6 > 5 ? :open : :closed
108
+ end
109
+
110
+ def notify_user
111
+ p "Notifying User!"
112
+ end
113
+
114
+ def file_parsable?
115
+ 3 > 2 ? true : false
116
+ end
117
+
118
+ def file_close?
119
+ 1 > 0 ? true : false
120
+ end
121
+ end
122
+
123
+ t=SampleOne.new
124
+ #<SampleOne:0x9a3d500>
125
+
126
+ t.current_state
127
+ #<FlowChart::State:0x9a3ff1c @flowstatename=:init, @before_and_after_works={:preprocess=>#<Proc:0x09a41880@(irb):11>, :postprocess=>:notify_user}>
128
+
129
+ t.upload
130
+ "Notifying User!"
131
+ "Validating File"
132
+ true
133
+
134
+ t.current_state
135
+ #<FlowChart::State:0x9a3fe18 @flowstatename=:uploaded, @before_and_after_works={:preprocess=>#<Proc:0x09a415ec@(irb):16>, :postprocess=>#<Proc:0x09a414c0@(irb):17>}>
136
+
137
+ t.process
138
+ "File has been uploaded in system"
139
+
140
+ t.current_state
141
+ #<FlowChart::State:0x9a3fd00 @flowstatename=:open, @before_and_after_works={:postprocess=>#<Proc:0x09a41240@(irb):22>}>
142
+
143
+ t.closed?
144
+ false
145
+
146
+ t.open?
147
+ true
148
+
149
+ t.close
150
+ "File closed"
151
+ true
152
+
153
+ t.current_state
154
+ #<FlowChart::State:0x9a3fbfc @flowstatename=:closed, @before_and_after_works={:preprocess=>#<Proc:0x09a41024@(irb):26>, :postprocess=>:notify_user}>
155
+
156
+ == Save! in ActiveRecord Model
157
+
158
+ For any action if you choose to follow it up with a '!' then the model object is saved after action call. Similarly for an action if you choose to not follow it up with a '!' it will only update the attributes for the model object
data/flowchart.gemspec ADDED
@@ -0,0 +1,13 @@
1
+ Gem::Specification.new do |f|
2
+ f.name = 'flowchart'
3
+ f.version = '0.0.1'
4
+ f.date = %q{2013-09-27}
5
+ f.summary = %q{State Machine and State Flow for Ruby}
6
+ f.description = %q{Flowchart is a State Machine(state-action-transition) workflow for Non-ActiveRecord and ActiveRecord Model in Ruby or Rails}
7
+ f.authors = %q{Shuddhashil Ray}
8
+ f.email = %q{rayshuddhashil@gmail.com}
9
+ f.files = ["README.rdoc","LICENSE.txt","flowchart.gemspec","lib/flow_chart.rb","lib/flowchart/flow_processor.rb","sample/sample_one.rb"]
10
+ f.require_paths = ["lib"]
11
+ f.homepage = %q{http://github.com/raycoding/flowchart}
12
+ f.license = "MIT"
13
+ end
data/lib/flow_chart.rb ADDED
@@ -0,0 +1,71 @@
1
+ require 'flowchart/flow_processor'
2
+ module FlowChart
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ base.send :include, InstanceMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ attr_reader :flowprocessor
10
+
11
+ def flowchart(&block)
12
+ @flowprocessor = FlowChart::FlowProcessor.new(&block)
13
+ @flowprocessor.flowstates.values.each do |flowstate|
14
+ flowstate_name = flowstate.flowstatename
15
+ # Helper Methods to verify is this is the current_state, example object.uploaded? , object.closed? etc
16
+ define_method "#{flowstate_name}?" do
17
+ flowstate_name == current_state.flowstatename
18
+ end
19
+ end
20
+
21
+ #Driver Instance Methods for each evet for triggering the action action!
22
+ @flowprocessor.actions.keys.each do |key|
23
+ define_method "#{key}" do
24
+ process_action(key,:save_object=>false)
25
+ end
26
+
27
+ define_method "#{key}!" do
28
+ process_action(key,:save_object=>true)
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ module InstanceMethods
35
+ attr_accessor :previous_state
36
+
37
+ def flowprocessor
38
+ self.class.flowprocessor
39
+ end
40
+
41
+ def current_state
42
+ if (flowprocessor.state_column.to_sym.nil? or send(flowprocessor.state_column.to_sym).nil? or send(flowprocessor.state_column.to_sym)=="")
43
+ @current_state ||= flowprocessor.flowstates[flowprocessor.starting_flowstate.flowstatename]
44
+ else
45
+ @current_state ||= flowprocessor.flowstates[send(flowprocessor.state_column.to_sym).to_sym]
46
+ end
47
+ @current_state
48
+ end
49
+
50
+ def set_current_state(new_state, options = {})
51
+ send("#{flowprocessor.state_column}=".to_sym, new_state.flowstatename.to_s)
52
+ @current_state = new_state
53
+ send("#{flowprocessor.state_column.to_s}=".to_sym,"#{current_state.flowstatename.to_s.to_s}") #Updates the instance variable
54
+ # If asked to save action with bang! and if inherits from ActiveRecord in Rails then do save!
55
+ ## Important check to support both Non-ActiveRecord Models in Ruby and ActiveRecord Models in Rails
56
+ if options[:save_object] and !(defined?(ActiveRecord::Base).nil?) and self.class.ancestors.include? ActiveRecord::Base
57
+ self.save
58
+ end
59
+ end
60
+
61
+ private
62
+ def process_action(action_name,options_for_action = {})
63
+ action = flowprocessor.actions[action_name.to_sym]
64
+ if action.nil?
65
+ p "Error: #{action_name} not found!"
66
+ raise FlowChart::InvalidState.new("Error: #{action_name} not found!")
67
+ end
68
+ action.process_action!(self,current_state,options_for_action)
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,164 @@
1
+ module FlowChart
2
+ class InvalidTransition < NoMethodError; end
3
+ class InvalidState < NoMethodError; end
4
+ class InvalidAction < NoMethodError; end
5
+
6
+ # Driver for States in FlowChart
7
+ class State
8
+ attr_accessor :flowstatename, :before_and_after_works
9
+
10
+ def initialize(flowstatename, &flowblock)
11
+ @flowstatename,@before_and_after_works = flowstatename,Hash.new
12
+ instance_eval(&flowblock) if block_given?
13
+ end
14
+
15
+ ## Optional in State flow
16
+ ##Preprocess Things-To-Do before entering this state => Usecase Notifications
17
+ ## Takes a proc or instance method as block
18
+ def preprocess(method = nil, &block)
19
+ @before_and_after_works[:preprocess] = method.nil? ? block : method
20
+ end
21
+
22
+ ## Optional in State flow
23
+ #PostProcess Things-To-Do before entering this state => Usecase Notifications
24
+ ## Takes a proc or instance method as block
25
+ def postprocess(method = nil, &block)
26
+ @before_and_after_works[:postprocess] = method.nil? ? block : method
27
+ end
28
+
29
+ ##Defines how the flow for a State executes with preprocess and postprocess
30
+ def process_flow(action,base)
31
+ action = @before_and_after_works[action.to_sym]
32
+ case action
33
+ when Symbol, String
34
+ base.send(action)
35
+ when Proc
36
+ action.call(base)
37
+ end
38
+ end
39
+ end
40
+
41
+ # Driver for Transition in FlowChart
42
+ class Transition
43
+ attr_reader :condition #boolean condition to verify if transition can be done!
44
+ attr_reader :branch #selection from multiple branch transition based on condition!
45
+ attr_reader :from #transit from!
46
+ attr_reader :to #transit to!
47
+
48
+ def initialize(options)
49
+ @condition = options[:condition]
50
+ @branch = options[:branch]
51
+ @from = [options[:from]].flatten
52
+ @to = options[:to]
53
+ end
54
+
55
+ def get_me_next_state(base)
56
+ raise InvalidTransition.new("You cannot transit from multiple states without a decision condition!") if @to.is_a?(Array) && @branch.nil?
57
+ ## If to is a single state then just pass it on!
58
+ if !@to.is_a?(Array)
59
+ return @to
60
+ end
61
+ ## If to is an Array it means decision has to be made based on branching conditions to choose the to state!
62
+ to = process_flow(@branch, base)
63
+ ## If result to state coming from branching condition is not part of the set mentioned in to then next state transit is not possible!
64
+ raise InvalidState.new("Your decision condition did not lead to transition state mentioned in :to clause!") unless @to.include?(to)
65
+ to
66
+ end
67
+
68
+ def shall_i_trasit?(base)
69
+ return true unless @condition ## If no condition has been given then transition is possible!
70
+ ## If condition has been given then evaluate truth or falsity for transition to be possible!
71
+ process_flow(@condition, base)
72
+ end
73
+
74
+ private
75
+ def process_flow(action,base)
76
+ case action
77
+ when Symbol, String
78
+ base.send(action)
79
+ when Proc
80
+ action.call(base)
81
+ end
82
+ end
83
+ end
84
+
85
+ # Driver for Actions in FlowChart
86
+ class Action
87
+ attr_accessor :name
88
+ attr_accessor :transitions
89
+ attr_accessor :flowprocessor
90
+
91
+ def initialize(name, flowprocessor=nil, &transitions)
92
+ @name = name
93
+ @flowprocessor = flowprocessor
94
+ @transitions = Array.new
95
+ instance_eval(&transitions)
96
+ end
97
+
98
+ def process_action!(implementor_class,current_state,options_for_action)
99
+ transition = @transitions.select{ |t| t.from.include? current_state.flowstatename }.first
100
+ raise InvalidTransition.new("No transition found for action #{@name}") if transition.nil?
101
+ return false unless transition.shall_i_trasit?(implementor_class)
102
+ new_state = implementor_class.flowprocessor.flowstates[transition.get_me_next_state(implementor_class)]
103
+ raise InvalidState.new("Invalid state #{transition.to.to_s} for transition.") if new_state.nil?
104
+ current_state.process_flow(:postprocess, implementor_class)
105
+ implementor_class.previous_state = current_state.flowstatename.to_s
106
+ new_state.process_flow(:preprocess, implementor_class)
107
+ implementor_class.set_current_state(new_state,options_for_action)
108
+ true
109
+ end
110
+
111
+ private
112
+ def transitions(args = {})
113
+ transition = FlowChart::Transition.new(args)
114
+ @transitions << transition
115
+ end
116
+
117
+ def any
118
+ @flowprocessor.flowstates.keys
119
+ end
120
+ end
121
+
122
+ # Main Driver for FlowChart
123
+ class FlowProcessor
124
+ attr_accessor :flowstates
125
+ attr_accessor :starting_flowstate
126
+ attr_accessor :actions
127
+
128
+ def initialize(&flowprocessor)
129
+ @flowstates, @actions = Hash.new, Hash.new
130
+ begin
131
+ instance_eval(&flowprocessor)
132
+ rescue => e
133
+ p e.to_s
134
+ end
135
+ end
136
+
137
+ ## Default state_column is assumed to be process_status if not mentioned
138
+ # You can override the default state_column by mentioning in your class
139
+ # state_column :something_else
140
+ def state_column(name = :process_status)
141
+ @state_column ||= name
142
+ end
143
+
144
+ private
145
+ def init_flowstate(flowstate_name)
146
+ @starting_flowstate_name = flowstate_name
147
+ end
148
+
149
+ def flowstate(*flowstatenames, &options)
150
+ flowstatenames.each do |flowstatename|
151
+ flowstate = FlowChart::State.new(flowstatename, &options)
152
+ if @flowstates.empty? || @starting_flowstate_name == flowstatename
153
+ @starting_flowstate = flowstate
154
+ end
155
+ @flowstates[flowstatename.to_sym] = flowstate
156
+ end
157
+ end
158
+
159
+ def action(action_name, &transitions)
160
+ action = FlowChart::Action.new(action_name, self, &transitions)
161
+ @actions[action_name.to_sym] = action
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,59 @@
1
+ # Example of a Non-ActiveRecord Class using FlowChart in Ruby
2
+
3
+ class SampleOne
4
+ include FlowChart
5
+ attr_accessor :process_status
6
+
7
+ flowchart do
8
+ init_flowstate :init
9
+
10
+ flowstate :init do
11
+ preprocess Proc.new { |o| p "Initializing File" }
12
+ postprocess :notify_user
13
+ end
14
+
15
+ flowstate :uploaded do
16
+ preprocess Proc.new { |o| p "Validating File" }
17
+ postprocess Proc.new { |o| p "File has been uploaded in system" }
18
+ end
19
+
20
+ flowstate :open do
21
+ postprocess :notify_user
22
+ postprocess Proc.new { |o| p "File has been closed" }
23
+ end
24
+
25
+ flowstate :closed do
26
+ preprocess Proc.new { |o| p "File closed" }
27
+ postprocess :notify_user
28
+ end
29
+
30
+ action :upload do
31
+ transitions :from => :init, :to => :uploaded, :condition => :file_parsable?
32
+ end
33
+
34
+ action :process do
35
+ transitions :from => :uploaded, :to => [:open, :closed], :branch => :choose_branch
36
+ end
37
+
38
+ action :close do
39
+ transitions :from => [:init,:uploaded,:open], :to => :closed, :condition => :file_close?
40
+ end
41
+
42
+ end
43
+
44
+ def choose_branch
45
+ 6 > 5 ? :open : :closed
46
+ end
47
+
48
+ def notify_user
49
+ p "Notifying User!"
50
+ end
51
+
52
+ def file_parsable?
53
+ 3 > 2 ? true : false
54
+ end
55
+
56
+ def file_close?
57
+ 1 > 0 ? true : false
58
+ end
59
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flowchart
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Shuddhashil Ray
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2013-09-27 00:00:00 +05:30
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: Flowchart is a State Machine(state-action-transition) workflow for Non-ActiveRecord and ActiveRecord Model in Ruby or Rails
23
+ email: rayshuddhashil@gmail.com
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - README.rdoc
32
+ - LICENSE.txt
33
+ - flowchart.gemspec
34
+ - lib/flow_chart.rb
35
+ - lib/flowchart/flow_processor.rb
36
+ - sample/sample_one.rb
37
+ has_rdoc: true
38
+ homepage: http://github.com/raycoding/flowchart
39
+ licenses:
40
+ - MIT
41
+ post_install_message:
42
+ rdoc_options: []
43
+
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ none: false
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ hash: 3
52
+ segments:
53
+ - 0
54
+ version: "0"
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ hash: 3
61
+ segments:
62
+ - 0
63
+ version: "0"
64
+ requirements: []
65
+
66
+ rubyforge_project:
67
+ rubygems_version: 1.5.3
68
+ signing_key:
69
+ specification_version: 3
70
+ summary: State Machine and State Flow for Ruby
71
+ test_files: []
72
+