flowchart 0.0.1

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/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
+