multiflow 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 27023220462c60ad9bf0cbdee297069da3c6db61
4
+ data.tar.gz: cfd4c4c3c6b624e4377d375ea56aad5369579035
5
+ SHA512:
6
+ metadata.gz: 4c7835b010f45c295513917d015e2d5c59789d3e790945a40217894f337139854cd10f7c1ee3a4ece2dae05201a933c41730f5e1c1bd5c1ae6c933baf1ef60c7
7
+ data.tar.gz: 62c8b45de48b83647a7c402479b052889fa98dfbbd285dbdd3d9733bd2f496affabc42c45ad3fe1d90a3f17076748e916d57dde6b8776d8fdb3a3d979bc8ebb1
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ pkg
2
+ doc
3
+ *.gem
4
+ .redcar
5
+ **.swp
6
+ .bundle/
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,38 @@
1
+ = Version 0.5.0.beta
2
+ * Added a Railtie
3
+ * Changed up the detection for persistence in Rails
4
+
5
+ = Version 0.4.2
6
+ * Some code neatening by pedromenezes - Thanks!
7
+ * Added simple benchmark which is probably not accurate :)
8
+
9
+ = Version 0.4.1
10
+ * Removed the depreciation warnings - FINALLY :)
11
+
12
+ = Version 0.4.0
13
+ * Changed the order of the hooks, now runs in the following order:
14
+ - Exit on previous state
15
+ - Enter on new state
16
+ - Save to persistence
17
+
18
+ = Version 0.3.0
19
+ * Added a non bang method for events (Check README)
20
+ * Added tests to persistence layers (About time!)
21
+ * Changed the way the persistence layers save to model
22
+ * Protected state column is now supported!
23
+
24
+ = Version 0.2.3
25
+ * Silence depreciation warnings (Fixing irritating issue)
26
+
27
+ = Version 0.2.2
28
+ * Previous state can be accessed with `_previous_state` instance method after transitioning states
29
+
30
+ = Version 0.2.1
31
+ * Reverted the writes back to update_attributes
32
+
33
+ = Version 0.2
34
+ * Added a transition to any event, please look at the tests to understand how it works - Thanks to nu7hatch for the patch!
35
+ * Changed the persistence layers to use write_attribute, instead of update_attribute - Thanks to achirkunov
36
+
37
+ = Version 0.1.2
38
+ * Fixed Mongoid support - Thanks bmartin for pointing that out
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,46 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ multiflow (1.0.0)
5
+ activesupport
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ specs:
10
+ activemodel (3.2.8)
11
+ activesupport (= 3.2.8)
12
+ builder (~> 3.0.0)
13
+ activerecord (3.2.8)
14
+ activemodel (= 3.2.8)
15
+ activesupport (= 3.2.8)
16
+ arel (~> 3.0.2)
17
+ tzinfo (~> 0.3.29)
18
+ activesupport (3.2.8)
19
+ i18n (~> 0.6)
20
+ multi_json (~> 1.0)
21
+ arel (3.0.2)
22
+ builder (3.0.0)
23
+ diff-lcs (1.1.3)
24
+ i18n (0.6.0)
25
+ multi_json (1.3.6)
26
+ rspec (2.11.0)
27
+ rspec-core (~> 2.11.0)
28
+ rspec-expectations (~> 2.11.0)
29
+ rspec-mocks (~> 2.11.0)
30
+ rspec-core (2.11.1)
31
+ rspec-expectations (2.11.2)
32
+ diff-lcs (~> 1.1.3)
33
+ rspec-mocks (2.11.2)
34
+ sqlite3 (1.3.6)
35
+ sqlite3-ruby (1.3.3)
36
+ sqlite3 (>= 1.3.3)
37
+ tzinfo (0.3.33)
38
+
39
+ PLATFORMS
40
+ ruby
41
+
42
+ DEPENDENCIES
43
+ activerecord
44
+ multiflow!
45
+ rspec (>= 2.0.0)
46
+ sqlite3-ruby
data/LICENCE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Ryan Oberholzer
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,131 @@
1
+ = Stateflow
2
+
3
+ == PLEASE NOTE!!!
4
+
5
+ *Version 0.5.x > will only support Rails 3 / ActiveModel persistences. If you are using Rails 2 Please make sure you use the 0.4.x releases. There is a branch dedicated to that.*
6
+
7
+ This is the basics of the gem. Please check out the examples directory or tests for usage until this README gets fleshed out. Feel free to fork and modify as you please.
8
+
9
+ == INSTALL
10
+
11
+ gem install stateflow
12
+
13
+ == Usage
14
+
15
+ As you can see below, Stateflow's API is very similar to AASM, but allows for a more dynamic state transition flow. Stateflow supports persistence/storage with Mongoid, MongoMapper, and ActiveRecord. Request any others or push them to me.
16
+
17
+ Stateflow defaults to ActiveRecord but you can set the persistence layer with:
18
+
19
+ Stateflow.persistence = :mongo_mapper
20
+ OR
21
+ Stateflow.persistence = :active_record
22
+ OR
23
+ Stateflow.persistence = :mongoid
24
+
25
+ Stateflow allows dynamic :to transitions with :decide. The result :decide returns needs to be one of the states listed in the :to array, otherwise it wont allow the transition. Please view the advanced example below for usage.
26
+
27
+ You can set the default column with the state_column function in the stateflow block. The default state column is "state".
28
+
29
+ state_column :state
30
+
31
+ == Rails 3
32
+ Stateflow now automatically tries to detect your persistence from your applications default ORM config. If the ORM you are using does not have a persistence layer it will default to ActiveRecord.
33
+
34
+ == Basic Example
35
+
36
+ require 'rubygems'
37
+ require 'stateflow'
38
+
39
+ # No persistence
40
+ Stateflow.persistence = :none
41
+
42
+ class Stoplight
43
+ include Stateflow
44
+
45
+ stateflow do
46
+ initial :green
47
+
48
+ state :green, :yellow, :red
49
+
50
+ event :change_color do
51
+ transitions :from => :green, :to => :yellow
52
+ transitions :from => :yellow, :to => :red
53
+ transitions :from => :red, :to => :green
54
+ end
55
+ end
56
+ end
57
+
58
+ == Advanced Example
59
+
60
+ require 'rubygems'
61
+ require 'stateflow'
62
+
63
+ # No persistence
64
+ Stateflow.persistence = :none
65
+
66
+ class Test
67
+ include Stateflow
68
+
69
+ stateflow do
70
+
71
+ initial :love
72
+
73
+ state :love do
74
+ enter lambda { |t| p "Entering love" }
75
+ exit :exit_love
76
+ end
77
+
78
+ state :hate do
79
+ enter lambda { |t| p "Entering hate" }
80
+ exit lambda { |t| p "Exiting hate" }
81
+ end
82
+
83
+ state :mixed do
84
+ enter lambda { |t| p "Entering mixed" }
85
+ exit lambda { |t| p "Exiting mixed" }
86
+ end
87
+
88
+ event :b do
89
+ transitions :from => :love, :to => :hate, :if => :no_ice_cream
90
+ transitions :from => :hate, :to => :love
91
+ end
92
+
93
+ event :a do
94
+ transitions :from => :love, :to => [:hate, :mixed], :decide => :likes_ice_cream?
95
+ transitions :from => [:hate, :mixed], :to => :love
96
+ end
97
+ end
98
+
99
+ def likes_ice_cream?
100
+ rand(10) > 5 ? :mixed : :hate
101
+ end
102
+
103
+ def exit_love
104
+ p "Exiting love"
105
+ end
106
+
107
+ def no_ice_cream
108
+ rand(4) > 2 ? true : false
109
+ end
110
+ end
111
+
112
+ == Bang event vs non-bang event
113
+
114
+ Bang events will save the model after call, where the non bang event will just update the state and call the transitions. (ie. model.change! vs model.change)
115
+
116
+ == Extras
117
+
118
+ * When transitioning states, the previous state from which you have transitioned to can be accessed via `_previous_state`. See tests for more information.
119
+
120
+ == Note on Patches/Pull Requests
121
+
122
+ * Fork the project.
123
+ * Make your feature addition or bug fix.
124
+ * Add tests for it. This is important so I don't break it in a future version unintentionally.
125
+ * Commit, do not mess with Rakefile, version, or history.
126
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
127
+ * Send me a pull request. Bonus points for topic branches.
128
+
129
+ == Copyright
130
+
131
+ Copyright (c) 2010 Ryan Oberholzer. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,88 @@
1
+ require 'benchmark'
2
+ require 'active_record'
3
+ require 'stateflow'
4
+ require 'aasm'
5
+
6
+ # change this if sqlite is unavailable
7
+ dbconfig = {
8
+ :adapter => 'sqlite3',
9
+ :database => ':memory:'
10
+ }
11
+
12
+ ActiveRecord::Base.establish_connection(dbconfig)
13
+ ActiveRecord::Migration.verbose = false
14
+
15
+
16
+ class TestMigration < ActiveRecord::Migration
17
+ STATE_MACHINES = ['stateflow', 'aasm']
18
+
19
+ def self.up
20
+ STATE_MACHINES.each do |name|
21
+ create_table "#{name}_tests", :force => true do |t|
22
+ t.column :state, :string
23
+ end
24
+ end
25
+ end
26
+
27
+ def self.down
28
+ STATE_MACHINES.each do |name|
29
+ drop_table "#{name}_tests"
30
+ end
31
+ end
32
+ end
33
+
34
+ Stateflow.persistence = :active_record
35
+
36
+ class StateflowTest < ActiveRecord::Base
37
+ include Stateflow
38
+
39
+ stateflow do
40
+ initial :green
41
+
42
+ state :green, :yellow, :red
43
+
44
+ event :change_color do
45
+ transitions :from => :green, :to => :yellow
46
+ transitions :from => :yellow, :to => :red
47
+ transitions :from => :red, :to => :green
48
+ end
49
+ end
50
+ end
51
+
52
+ class AasmTest < ActiveRecord::Base
53
+ include AASM
54
+
55
+ aasm_column :state # defaults to aasm_state
56
+
57
+ aasm_initial_state :green
58
+
59
+ aasm_state :green
60
+ aasm_state :yellow
61
+ aasm_state :red
62
+
63
+ aasm_event :change_color do
64
+ transitions :from => :green, :to => :yellow
65
+ transitions :from => :yellow, :to => :red
66
+ transitions :from => :red, :to => :green
67
+ end
68
+ end
69
+
70
+ n = 1000
71
+ TestMigration.up
72
+
73
+ Benchmark.bm(7) do |x|
74
+ x.report('stateflow') do
75
+ n.times do
76
+ stateflow = StateflowTest.new
77
+ 3.times { stateflow.change_color! }
78
+ end
79
+ end
80
+ x.report('aasm') do
81
+ n.times do
82
+ aasm = AasmTest.new
83
+ 3.times { aasm.change_color! }
84
+ end
85
+ end
86
+ end
87
+
88
+ TestMigration.down
data/examples/robot.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'rubygems'
2
+ require 'stateflow'
3
+
4
+ class Robot
5
+ include Stateflow
6
+
7
+ stateflow do
8
+ initial :green
9
+
10
+ state :green, :yellow, :red
11
+
12
+ event :change_color do
13
+ transitions :from => :green, :to => :yellow
14
+ transitions :from => :yellow, :to => :red
15
+ transitions :from => :red, :to => :green
16
+ end
17
+ end
18
+ end
data/examples/test.rb ADDED
@@ -0,0 +1,48 @@
1
+ require 'rubygems'
2
+ require 'stateflow'
3
+
4
+ class Test
5
+ include Stateflow
6
+
7
+ stateflow do
8
+
9
+ initial :love
10
+
11
+ state :love do
12
+ enter lambda { |t| p "Entering love" }
13
+ exit :exit_love
14
+ end
15
+
16
+ state :hate do
17
+ enter lambda { |t| p "Entering hate" }
18
+ exit lambda { |t| p "Exiting hate" }
19
+ end
20
+
21
+ state :mixed do
22
+ enter lambda { |t| p "Entering mixed" }
23
+ exit lambda { |t| p "Exiting mixed" }
24
+ end
25
+
26
+ event :b do
27
+ transitions :from => :love, :to => :hate, :if => :no_ice_cream
28
+ transitions :from => :hate, :to => :love
29
+ end
30
+
31
+ event :a do
32
+ transitions :from => :love, :to => [:hate, :mixed], :decide => :likes_ice_cream?
33
+ transitions :from => [:hate, :mixed], :to => :love
34
+ end
35
+ end
36
+
37
+ def likes_ice_cream?
38
+ rand(10) > 5 ? :mixeds : :hate
39
+ end
40
+
41
+ def exit_love
42
+ p "Exiting love"
43
+ end
44
+
45
+ def no_ice_cream
46
+ rand(4) > 2 ? true : false
47
+ end
48
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'stateflow'
@@ -0,0 +1,38 @@
1
+ module Multiflow
2
+ class Event
3
+ attr_accessor :name, :transitions, :machine
4
+
5
+ def initialize(name, machine=nil, &transitions)
6
+ @name = name
7
+ @machine = machine
8
+ @transitions = Array.new
9
+
10
+ instance_eval(&transitions)
11
+ end
12
+
13
+ def fire(machine, current_state, klass, options)
14
+ transition = @transitions.select{ |t| t.from.include? current_state.name }.first
15
+ raise NoTransitionFound.new("No transition found for event #{@name}") if transition.nil?
16
+
17
+ return nil unless transition.can_transition?(klass)
18
+
19
+ new_state = machine.states[transition.find_to_state(klass)]
20
+ raise NoStateFound.new("Invalid state #{transition.to.to_s} for transition.") if new_state.nil?
21
+
22
+ current_state.execute_action(:exit, klass)
23
+ new_state.execute_action(:enter, klass)
24
+
25
+ new_state
26
+ end
27
+
28
+ private
29
+ def transitions(args = {})
30
+ transition = Multiflow::Transition.new(args)
31
+ @transitions << transition
32
+ end
33
+
34
+ def any
35
+ @machine.states.keys
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,5 @@
1
+ module Multiflow
2
+ class NoTransitionFound < Exception; end
3
+ class NoStateFound < Exception; end
4
+ class NoEventFound < Exception; end
5
+ end
@@ -0,0 +1,40 @@
1
+ module Multiflow
2
+ class Machine
3
+ attr_accessor :states, :initial_state, :events
4
+
5
+ def initialize(&machine)
6
+ @states, @events, @create_scopes = Hash.new, Hash.new, true
7
+ instance_eval(&machine)
8
+ end
9
+
10
+ def state_column(name = :state)
11
+ @state_column ||= name
12
+ end
13
+
14
+ def create_scopes(bool = false)
15
+ @create_scopes = bool
16
+ end
17
+
18
+ def create_scopes?
19
+ @create_scopes
20
+ end
21
+
22
+ private
23
+ def initial(name)
24
+ @initial_state_name = name
25
+ end
26
+
27
+ def state(*names, &options)
28
+ names.each do |name|
29
+ state = Multiflow::State.new(name, &options)
30
+ @initial_state = state if @states.empty? || @initial_state_name == name
31
+ @states[name.to_sym] = state
32
+ end
33
+ end
34
+
35
+ def event(name, &transitions)
36
+ event = Multiflow::Event.new(name, self, &transitions)
37
+ @events[name.to_sym] = event
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,35 @@
1
+ module Multiflow
2
+ module Persistence
3
+ module ActiveRecord
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ before_validation(:ensure_initial_state, :on => :create)
8
+ end
9
+
10
+ module ClassMethods
11
+ def add_scope(machine, state)
12
+ scope state.name, -> { where("#{machine.state_column}".to_sym => state.name.to_s) }
13
+ end
14
+ end
15
+
16
+ def load_from_persistence(machine)
17
+ send machine.state_column.to_sym
18
+ end
19
+
20
+ def save_to_persistence(machine, new_state, options = {})
21
+ send("#{machine.state_column}=".to_sym, new_state)
22
+ save! if options[:save]
23
+ end
24
+
25
+ def ensure_initial_state
26
+ machines.each do |machine|
27
+ if send(machine.state_column.to_s).blank?
28
+ current_state = send("current_#{machine.state_column}")
29
+ send("#{machine.state_column.to_s}=", current_state.name.to_s)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,21 @@
1
+ module Multiflow
2
+ module Persistence
3
+ module None
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ def add_scope(machine, state)
8
+ # do nothing
9
+ end
10
+ end
11
+
12
+ def load_from_persistence(machine)
13
+ instance_variable_get :"@state_#{machine.state_column}"
14
+ end
15
+
16
+ def save_to_persistence(machine, new_state, options)
17
+ instance_variable_set :"@state_#{machine.state_column}", new_state
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,29 @@
1
+ module Multiflow
2
+ module Persistence
3
+ def self.active
4
+ persistences = Array.new
5
+
6
+ Dir[File.dirname(__FILE__) + '/persistence/*.rb'].each do |file|
7
+ persistences << File.basename(file, File.extname(file)).underscore.to_sym
8
+ end
9
+
10
+ persistences
11
+ end
12
+
13
+ def self.load!(base)
14
+ begin
15
+ base.send :include, "Multiflow::Persistence::#{Multiflow.persistence.to_s.camelize}".constantize
16
+ rescue NameError
17
+ puts "[Multiflow] The ORM you are using does not have a Persistence layer. Defaulting to ActiveRecord."
18
+ puts "[Multiflow] You can overwrite the persistence with Multiflow.persistence = :new_persistence_layer"
19
+
20
+ Multiflow.persistence = :active_record
21
+ base.send :include, "Multiflow::Persistence::ActiveRecord".constantize
22
+ end
23
+ end
24
+
25
+ Multiflow::Persistence.active.each do |p|
26
+ autoload p.to_s.camelize.to_sym, "multiflow/persistence/#{p.to_s}"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,12 @@
1
+ module Multiflow
2
+ class Railtie < Rails::Railtie
3
+ def default_orm
4
+ generators = config.respond_to?(:app_generators) ? :app_generators : :generators
5
+ config.send(generators).options[:rails][:orm]
6
+ end
7
+
8
+ initializer "multiflow.set_persistence" do
9
+ Multiflow.persistence = default_orm if Multiflow.persistence.blank?
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,31 @@
1
+ module Multiflow
2
+ class State
3
+ attr_accessor :name, :options
4
+
5
+ def initialize(name, &options)
6
+ @name = name
7
+ @options = Hash.new
8
+
9
+ instance_eval(&options) if block_given?
10
+ end
11
+
12
+ def enter(method = nil, &block)
13
+ @options[:enter] = method.nil? ? block : method
14
+ end
15
+
16
+ def exit(method = nil, &block)
17
+ @options[:exit] = method.nil? ? block : method
18
+ end
19
+
20
+ def execute_action(action, base)
21
+ action = @options[action.to_sym]
22
+
23
+ case action
24
+ when Symbol, String
25
+ base.send(action)
26
+ when Proc
27
+ action.call(base)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,39 @@
1
+ module Multiflow
2
+ class IncorrectTransition < Exception; end
3
+
4
+ class Transition
5
+ attr_reader :from, :to, :if, :decide
6
+
7
+ def initialize(args)
8
+ @from = [args[:from]].flatten
9
+ @to = args[:to]
10
+ @if = args[:if]
11
+ @decide = args[:decide]
12
+ end
13
+
14
+ def can_transition?(base)
15
+ return true unless @if
16
+ execute_action(@if, base)
17
+ end
18
+
19
+ def find_to_state(base)
20
+ raise IncorrectTransition.new("Array of destinations and no decision") if @to.is_a?(Array) && @decide.nil?
21
+ return @to unless @to.is_a?(Array)
22
+
23
+ to = execute_action(@decide, base)
24
+
25
+ raise NoStateFound.new("Decision did not return a state that was set in the 'to' argument") unless @to.include?(to)
26
+ to
27
+ end
28
+
29
+ private
30
+ def execute_action(action, base)
31
+ case action
32
+ when Symbol, String
33
+ base.send(action)
34
+ when Proc
35
+ action.call(base)
36
+ end
37
+ end
38
+ end
39
+ end