state_flow 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +85 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/init.rb +5 -0
- data/install.rb +1 -0
- data/lib/state_flow/action.rb +67 -0
- data/lib/state_flow/base.rb +108 -0
- data/lib/state_flow/builder.rb +193 -0
- data/lib/state_flow/entry.rb +44 -0
- data/lib/state_flow/event.rb +21 -0
- data/lib/state_flow/log.rb +30 -0
- data/lib/state_flow/named_action.rb +19 -0
- data/lib/state_flow.rb +20 -0
- data/spec/.gitignore +1 -0
- data/spec/base_spec.rb +457 -0
- data/spec/concurrency_spec.rb +275 -0
- data/spec/database.yml +25 -0
- data/spec/page_spec.rb +554 -0
- data/spec/resources/models/page.rb +70 -0
- data/spec/schema.rb +24 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +91 -0
- data/tasks/state_flow_tasks.rake +4 -0
- data/uninstall.rb +1 -0
- metadata +85 -0
data/.gitignore
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 [name of plugin creator]
|
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,85 @@
|
|
1
|
+
= StateFlow
|
2
|
+
== StateFlowとは?
|
3
|
+
|
4
|
+
状態遷移のためのDSLを提供するためのActiveRecordを拡張するプラグインです。
|
5
|
+
|
6
|
+
以下のような記述が可能です。
|
7
|
+
|
8
|
+
class Page < ActiveRecord::Base
|
9
|
+
validates_presence_of :name
|
10
|
+
|
11
|
+
selectable_attr :status_cd do
|
12
|
+
entry '01', :editable , '編集可'
|
13
|
+
entry '04', :waiting_publish, '公開待ち'
|
14
|
+
entry '05', :publishing , '公開処理中'
|
15
|
+
entry '06', :publishing_done, '公開処理完了'
|
16
|
+
entry '07', :published , '公開済'
|
17
|
+
entry '08', :publish_failure, '公開失敗'
|
18
|
+
end
|
19
|
+
|
20
|
+
state_flow(:status_cd) do
|
21
|
+
state :created => {event(:publish) => :waiting_publish, :lock => true}
|
22
|
+
|
23
|
+
with_options(:failure => :publish_failure) do |publishing|
|
24
|
+
publishing.state :waiting_publish => :publishing, :lock => true
|
25
|
+
publishing.state :publishing => {action(:start_publish) => :publishing_done}
|
26
|
+
publishing.state :publishing_done => :published, :if => :accessable?
|
27
|
+
publishing.state :publish_failure
|
28
|
+
end
|
29
|
+
|
30
|
+
state :published
|
31
|
+
end
|
32
|
+
|
33
|
+
def start_publish
|
34
|
+
# 公開時の処理
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
== セットアップ
|
39
|
+
state_flowプラグインはselectable_attrに依存しています。
|
40
|
+
|
41
|
+
- selectable_attr
|
42
|
+
-- http://github.com/akm/selectable_attr
|
43
|
+
- selectable_attr_rails
|
44
|
+
-- http://github.com/akm/selectable_attr_rails
|
45
|
+
|
46
|
+
== Railsで使う場合
|
47
|
+
|
48
|
+
=== プラグインとしてインストール
|
49
|
+
ruby script/plugin install git://github.com/akm/selectable_attr.git
|
50
|
+
ruby script/plugin install git://github.com/akm/selectable_attr_rails.git
|
51
|
+
ruby script/plugin install git://github.com/akm/state_flow.git
|
52
|
+
でオッケーです。
|
53
|
+
|
54
|
+
== gemの場合
|
55
|
+
まずgemcutterの設定をしていなかったら、
|
56
|
+
gem install gemcutter
|
57
|
+
gem tumble
|
58
|
+
を実行した後、
|
59
|
+
gem install selectable_attr selectable_attr_rails state_flow
|
60
|
+
を実行するとインストール完了。
|
61
|
+
|
62
|
+
で、config/initializersに以下の2つのファイルを作成すればオッケーです。
|
63
|
+
|
64
|
+
config/initializers/selectable_attr.rb
|
65
|
+
|
66
|
+
require 'selectable_attr'
|
67
|
+
require 'selectable_attr_i18n'
|
68
|
+
require 'selectable_attr_rails'
|
69
|
+
SelectableAttrRails.add_features_to_rails
|
70
|
+
|
71
|
+
config/initializers/state_flow.rb
|
72
|
+
|
73
|
+
require 'state_flow'
|
74
|
+
ActiveRecord::Base.module_eval do
|
75
|
+
include StateFlow
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
== Example
|
80
|
+
以下のテスト用のモデルや、テストをご覧ください。
|
81
|
+
http://github.com/akm/state_flow/blob/master/spec/resources/models/page.rb
|
82
|
+
http://github.com/akm/state_flow/blob/master/spec/page_spec.rb
|
83
|
+
|
84
|
+
|
85
|
+
Copyright (c) 2009 [Takeshi AKIMA], released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'rspec', '>= 1.1.4'
|
3
|
+
require 'rake'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'spec/rake/spectask'
|
6
|
+
require 'spec/rake/verify_rcov'
|
7
|
+
|
8
|
+
desc 'Default: run unit tests.'
|
9
|
+
task :default => :spec
|
10
|
+
|
11
|
+
task :pre_commit => [:spec, 'coverage:verify']
|
12
|
+
|
13
|
+
desc 'Run all specs under spec/**/*_spec.rb'
|
14
|
+
Spec::Rake::SpecTask.new(:spec => 'coverage:clean') do |t|
|
15
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
16
|
+
t.spec_opts = ['--options', 'spec/spec.opts']
|
17
|
+
t.rcov_dir = 'coverage'
|
18
|
+
t.rcov = true
|
19
|
+
# t.rcov_opts = ["--include-file", "lib\/*\.rb", "--exclude", "spec\/"]
|
20
|
+
t.rcov_opts = ["--exclude", "spec\/"]
|
21
|
+
end
|
22
|
+
|
23
|
+
desc 'Generate documentation for the state_flow plugin.'
|
24
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
25
|
+
rdoc.rdoc_dir = 'rdoc'
|
26
|
+
rdoc.title = 'State_flow'
|
27
|
+
rdoc.options << '--line-numbers' << '--inline-source' << '-c UTF-8'
|
28
|
+
rdoc.rdoc_files.include('README*')
|
29
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
30
|
+
end
|
31
|
+
|
32
|
+
namespace :coverage do
|
33
|
+
desc "Delete aggregate coverage data."
|
34
|
+
task(:clean) { rm_f "coverage" }
|
35
|
+
|
36
|
+
desc "verify coverage threshold via RCov"
|
37
|
+
RCov::VerifyTask.new(:verify => :spec) do |t|
|
38
|
+
t.threshold = 100.0 # Make sure you have rcov 0.7 or higher!
|
39
|
+
t.index_html = 'coverage/index.html'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
begin
|
44
|
+
require 'jeweler'
|
45
|
+
Jeweler::Tasks.new do |s|
|
46
|
+
s.name = "state_flow"
|
47
|
+
s.summary = "state_flow provides a DSL for State Transition"
|
48
|
+
s.description = "state_flow provides a DSL for State Transition"
|
49
|
+
s.email = "akima@gmail.com"
|
50
|
+
s.homepage = "http://github.com/akm/state_flow/"
|
51
|
+
s.authors = ["Takeshi Akima"]
|
52
|
+
end
|
53
|
+
rescue LoadError
|
54
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
55
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/init.rb
ADDED
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'state_flow'
|
3
|
+
module StateFlow
|
4
|
+
|
5
|
+
class Action
|
6
|
+
attr_reader :flow
|
7
|
+
attr_accessor :success_key
|
8
|
+
attr_accessor :failure_key
|
9
|
+
attr_accessor :lock, :if, :unless
|
10
|
+
|
11
|
+
def initialize(flow)
|
12
|
+
@flow = flow
|
13
|
+
@record_key_on_thread = "#{self.class.name}_#{self.object_id}_record"
|
14
|
+
end
|
15
|
+
|
16
|
+
def record
|
17
|
+
Thread.current[@record_key_on_thread]
|
18
|
+
end
|
19
|
+
|
20
|
+
def record=(value)
|
21
|
+
Thread.current[@record_key_on_thread] = value
|
22
|
+
end
|
23
|
+
|
24
|
+
def process(record)
|
25
|
+
return if self.if && !call_or_send(self.if, record)
|
26
|
+
return if self.unless && call_or_send(self.unless, record)
|
27
|
+
self.record = record
|
28
|
+
begin
|
29
|
+
block_given? ? yield(self) : proceed
|
30
|
+
ensure
|
31
|
+
self.record = nil
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def proceed
|
36
|
+
flow.process_with_log(self.record, success_key, failure_key)
|
37
|
+
end
|
38
|
+
|
39
|
+
def call_or_send(filter, record)
|
40
|
+
filter.respond_to?(:call) ? filter.call(record) :
|
41
|
+
filter.is_a?(Array) ? record.send(*filter) : record.send(filter)
|
42
|
+
end
|
43
|
+
|
44
|
+
def inspect
|
45
|
+
result = "<#{self.class.name}"
|
46
|
+
result << " @name=#{@name.inspect}" if @name
|
47
|
+
result << " @success_key=#{@success_key.inspect}" if @success_key
|
48
|
+
result << " @failure_key=#{@failure_key.inspect}" if @failure_key
|
49
|
+
result << " @lock=#{@lock.inspect}" if @lock
|
50
|
+
result << " @if=#{@if.inspect}" if @if
|
51
|
+
result << " @unless=#{@unless.inspect}" if @unless
|
52
|
+
result << '>'
|
53
|
+
end
|
54
|
+
|
55
|
+
module Executable
|
56
|
+
attr_accessor :action
|
57
|
+
|
58
|
+
def options ; @options ||= {} ; end
|
59
|
+
def options=(value); @options = value; end
|
60
|
+
|
61
|
+
def success_key; action.success_key if action; end
|
62
|
+
def failure_key; action.failure_key if action; end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'state_flow'
|
3
|
+
module StateFlow
|
4
|
+
|
5
|
+
class Base
|
6
|
+
include Builder
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
def process_state(selectable_attr, *keys, &block)
|
11
|
+
options = {
|
12
|
+
:transactional => false, # :each, # :all
|
13
|
+
}.update(keys.extract_options!)
|
14
|
+
options[:transactional] = :each if options[:transactional] == true
|
15
|
+
state_flow = state_flow_for(selectable_attr)
|
16
|
+
raise ArgumentError, "state_flow not found: #{selectable_attr.inspect}" unless state_flow
|
17
|
+
transaction_if_need(options[:transactional] == :all) do
|
18
|
+
keys.each do |key|
|
19
|
+
entry = state_flow[key]
|
20
|
+
raise ArgumentError, "entry not found: #{key.inspect}" unless entry
|
21
|
+
transaction_if_need(options[:transactional] == :each) do
|
22
|
+
entry.process(&block)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def transaction_if_need(with_transaction, &block)
|
29
|
+
if with_transaction
|
30
|
+
self.transaction(&block)
|
31
|
+
else
|
32
|
+
yield
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_reader :klass, :attr_name, :attr_key_name, :status_keys
|
38
|
+
attr_reader :entries
|
39
|
+
def initialize(klass, attr_name, attr_key_name)
|
40
|
+
@klass, @attr_name, @attr_key_name = klass, attr_name, attr_key_name
|
41
|
+
@status_keys = klass.send(@attr_key_name.to_s.pluralize).map{|s| s.to_sym}
|
42
|
+
@entries = []
|
43
|
+
end
|
44
|
+
|
45
|
+
def state_cd_by_key(key)
|
46
|
+
@state_cd_by_key_method_name ||= "#{klass.enum_base_name(attr_name)}_id_by_key"
|
47
|
+
klass.send(@state_cd_by_key_method_name, key)
|
48
|
+
end
|
49
|
+
|
50
|
+
def entry_for(key)
|
51
|
+
unless @entry_hash
|
52
|
+
@entry_hash = entries.inject({}) do |dest, entry|
|
53
|
+
dest[entry.key] = entry
|
54
|
+
dest
|
55
|
+
end
|
56
|
+
end
|
57
|
+
@entry_hash[key]
|
58
|
+
end
|
59
|
+
alias_method :[], :entry_for
|
60
|
+
|
61
|
+
def process_with_log(record, success_key, failure_key)
|
62
|
+
origin_state = record.send(attr_name)
|
63
|
+
origin_state_key = record.send(attr_key_name)
|
64
|
+
begin
|
65
|
+
yield(record) if block_given?
|
66
|
+
# success_keyが指定されない場合、その変更はアクションとして指定されたメソッドに依存します。
|
67
|
+
if success_key
|
68
|
+
record.send("#{attr_key_name}=", success_key)
|
69
|
+
record.save!
|
70
|
+
end
|
71
|
+
rescue Exception => error
|
72
|
+
log_attrs = {
|
73
|
+
:target => record,
|
74
|
+
:origin_state => origin_state,
|
75
|
+
:origin_state_key => origin_state_key ? origin_state_key.to_s : nil,
|
76
|
+
:dest_state => self.state_cd_by_key(success_key),
|
77
|
+
:dest_state_key => success_key ? success_key.to_s : nil
|
78
|
+
}
|
79
|
+
StateFlow::Log.error(error, log_attrs)
|
80
|
+
if failure_key
|
81
|
+
retry_count = 0
|
82
|
+
begin
|
83
|
+
record.send("#{attr_key_name}=", failure_key)
|
84
|
+
record.save!
|
85
|
+
rescue Exception => fatal_error
|
86
|
+
if retry_count == 0
|
87
|
+
retry_count += 1
|
88
|
+
record.attributes = record.class.find(record.id).attributes
|
89
|
+
retry
|
90
|
+
end
|
91
|
+
StateFlow::Log.fatal(fatal_error, log_attrs)
|
92
|
+
raise fatal_error
|
93
|
+
end
|
94
|
+
end
|
95
|
+
raise error
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def inspect
|
100
|
+
result = "<#{self.class.name} @attr_name=#{@attr_name.inspect} @attr_key_name=#{@attr_key_name.inspect}"
|
101
|
+
result << " @klass=\"#{@klass.name}\""
|
102
|
+
result << " @entries=#{@entries.inspect}"
|
103
|
+
result << '>'
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'state_flow'
|
3
|
+
module StateFlow
|
4
|
+
|
5
|
+
module Builder
|
6
|
+
module ClientClassMethods
|
7
|
+
def state_flow_for(selectable_attr)
|
8
|
+
return nil unless @state_flows
|
9
|
+
@state_flows.detect{|flow| flow.attr_name == selectable_attr}
|
10
|
+
end
|
11
|
+
|
12
|
+
def state_flow(selectable_attr, options = nil, &block)
|
13
|
+
options = {
|
14
|
+
:attr_key_name => "#{self.enum_base_name(selectable_attr)}_key".to_sym
|
15
|
+
}.update(options || {})
|
16
|
+
flow = Base.new(self, selectable_attr, options[:attr_key_name])
|
17
|
+
flow.instance_eval(&block)
|
18
|
+
flow.setup_events
|
19
|
+
@state_flows ||= []
|
20
|
+
@state_flows << flow
|
21
|
+
flow
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def new_entry(key)
|
26
|
+
@entry_hash = nil
|
27
|
+
result = Entry.new(self, key)
|
28
|
+
entries << result
|
29
|
+
result
|
30
|
+
end
|
31
|
+
|
32
|
+
def state(*args)
|
33
|
+
raise_invalid_state_argument if args.length > 2
|
34
|
+
if args.length == 2
|
35
|
+
# 引数が2個ならできるだけ一つのHashにまとめます。
|
36
|
+
case args.first
|
37
|
+
when Hash then
|
38
|
+
return state(args.first.update(args.last))
|
39
|
+
when Symbol, String
|
40
|
+
return state({args.first => nil}.update(args.last))
|
41
|
+
else
|
42
|
+
raise_invalid_state_argument
|
43
|
+
end
|
44
|
+
end
|
45
|
+
# 引数が一つだけになってます
|
46
|
+
arg = args.first
|
47
|
+
case arg
|
48
|
+
when Symbol, String
|
49
|
+
return state({args.first => nil})
|
50
|
+
when Hash then
|
51
|
+
# through
|
52
|
+
else
|
53
|
+
raise_invalid_state_argument
|
54
|
+
end
|
55
|
+
# 引数がHash一つだけの場合
|
56
|
+
base_options = extract_common_options(arg)
|
57
|
+
arg.each do |key, value|
|
58
|
+
entry = new_entry(key)
|
59
|
+
build_entry(entry, value, base_options)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def action(name)
|
64
|
+
NamedAction.new(self, name)
|
65
|
+
end
|
66
|
+
|
67
|
+
def event(name)
|
68
|
+
Event.new(self, name)
|
69
|
+
end
|
70
|
+
|
71
|
+
def setup_events
|
72
|
+
event_defs = {}
|
73
|
+
entries.each do |entry|
|
74
|
+
origin_key = entry.key
|
75
|
+
entry.events.each do |event|
|
76
|
+
event_trans = event_defs[event.name] ||= {}
|
77
|
+
event_trans[origin_key] = [event.success_key, event.failure_key]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
event_defs.each do |event_name, trans|
|
81
|
+
method_def = <<-"EOS"
|
82
|
+
def #{event_name}
|
83
|
+
@state_flow ||= self.class.state_flow_for(:#{attr_name})
|
84
|
+
@#{event_name}_transitions ||= #{trans.inspect}
|
85
|
+
@state_flow.process_with_log(self,
|
86
|
+
*@#{event_name}_transitions[#{attr_key_name}])
|
87
|
+
end
|
88
|
+
EOS
|
89
|
+
if klass.instance_methods.include?(event_name)
|
90
|
+
klass.logger.warn("state_flow plugin was going to define #{event_name} but didn't do it because #{event_name} does exist.")
|
91
|
+
else
|
92
|
+
klass.module_eval(method_def, __FILE__, __LINE__)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def build_entry(entry, options_or_success_key, base_options)
|
100
|
+
if options_or_success_key.nil?
|
101
|
+
return null_action_entry(entry, base_options)
|
102
|
+
end
|
103
|
+
case options_or_success_key
|
104
|
+
when String, Symbol then
|
105
|
+
return null_action_entry(entry, base_options, options_or_success_key.to_sym)
|
106
|
+
when Action then
|
107
|
+
entry.action = setup_action(options_or_success_key, base_options)
|
108
|
+
return entry
|
109
|
+
when Hash then
|
110
|
+
options = options_or_success_key
|
111
|
+
prior_options = extract_common_options(options)
|
112
|
+
options_keys = options.keys
|
113
|
+
if options_keys.all?{|key| key.is_a?(Action)}
|
114
|
+
build_action(entry, options, base_options.merge(prior_options))
|
115
|
+
elsif options_keys.all?{|key| key.is_a?(Event)}
|
116
|
+
raise_invalid_state_argument unless entry.is_a?(Entry)
|
117
|
+
build_events(entry, options, base_options.merge(prior_options))
|
118
|
+
else
|
119
|
+
raise_invalid_state_argument
|
120
|
+
end
|
121
|
+
when Array then
|
122
|
+
options_or_success_key.each do |options|
|
123
|
+
each_base_options = base_options.merge(extract_common_options(options))
|
124
|
+
build_entry(entry, options, each_base_options)
|
125
|
+
end
|
126
|
+
else
|
127
|
+
raise_invalid_state_argument
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def null_action_entry(entry, options, success_key = nil)
|
132
|
+
action = Action.new(self)
|
133
|
+
options = options.dup
|
134
|
+
entry.action = setup_action(action, options, success_key)
|
135
|
+
entry.options = options
|
136
|
+
entry
|
137
|
+
end
|
138
|
+
|
139
|
+
def build_action(entry, action_hash, options)
|
140
|
+
raise_invalid_state_argument unless action_hash.length == 1
|
141
|
+
options = options.dup
|
142
|
+
entry.action = setup_action(action_hash.keys.first, options, action_hash.values.first)
|
143
|
+
entry.options = options
|
144
|
+
end
|
145
|
+
|
146
|
+
def setup_action(action, options, success_key = nil)
|
147
|
+
action.success_key = success_key.to_sym if success_key
|
148
|
+
action.failure_key = options.delete(:failure)
|
149
|
+
action.lock = options.delete(:lock)
|
150
|
+
action.if = options.delete(:if)
|
151
|
+
action.unless = options.delete(:unless)
|
152
|
+
action
|
153
|
+
end
|
154
|
+
|
155
|
+
|
156
|
+
def build_events(entry, event_hash, options)
|
157
|
+
event_hash.each do |event, value|
|
158
|
+
build_entry(event, value, options)
|
159
|
+
entry.events << event
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
COMMON_OPTION_NAMES = [:lock, :if, :unless, :failure]
|
164
|
+
|
165
|
+
def extract_common_options(hash)
|
166
|
+
COMMON_OPTION_NAMES.inject({}) do |dest, name|
|
167
|
+
value = hash.delete(name)
|
168
|
+
dest[name] = value if value
|
169
|
+
dest
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def raise_invalid_state_argument
|
174
|
+
raise ArgumentError, state_argument_pattern
|
175
|
+
end
|
176
|
+
|
177
|
+
def state_argument_pattern
|
178
|
+
descriptions = <<-"EOS"
|
179
|
+
state arguments pattern:
|
180
|
+
* state :<state_name>
|
181
|
+
* state :<state_name> => :<new_state_name>
|
182
|
+
* state :<state_name> => action(:<method_name>)
|
183
|
+
* state :<state_name> => { action(:<method_name>) => :<new_state_name>}
|
184
|
+
* state :<state_name> => { event(:<event_name1>) => :<new_state_name>, event(:<event_name2>) => :<new_state_name>}
|
185
|
+
* state :<state_name> => { event(:<event_name1>) => { action(:<method_name1>) => :<new_state_name>}, event(:<event_name2>) => {action(:<method_name1>) => :<new_state_name>} }
|
186
|
+
And you can append :lock, :if, :unless option in Hash
|
187
|
+
EOS
|
188
|
+
descriptions.split(/$/).map{|s| s.sub(/^\s{8}/, '')}.join("\n")
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'state_flow'
|
3
|
+
module StateFlow
|
4
|
+
|
5
|
+
class Entry
|
6
|
+
include Action::Executable
|
7
|
+
attr_reader :flow, :key
|
8
|
+
|
9
|
+
def initialize(flow, key)
|
10
|
+
@flow = flow
|
11
|
+
@key = key.to_s.to_sym
|
12
|
+
end
|
13
|
+
|
14
|
+
def events
|
15
|
+
@events ||= [];
|
16
|
+
end
|
17
|
+
|
18
|
+
def event_for(name)
|
19
|
+
events.detect{|event| event.name == name}
|
20
|
+
end
|
21
|
+
|
22
|
+
def process(&block)
|
23
|
+
value = flow.state_cd_by_key(key)
|
24
|
+
find_options = {
|
25
|
+
:order => "id asc",
|
26
|
+
:conditions => ["#{flow.attr_name} = ?", value]
|
27
|
+
}
|
28
|
+
find_options[:lock] = action.lock if action.lock
|
29
|
+
if record = flow.klass.find(:first, find_options)
|
30
|
+
action.process(record, &block) if action
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def inspect
|
35
|
+
result = "<#{self.class.name} @key=#{@key.inspect}"
|
36
|
+
result << " @action=#{@action.inspect}" if @action
|
37
|
+
if @events && !@events.empty?
|
38
|
+
result << " @events=#{@events.sort_by{|event|event.name.to_s}.inspect}"
|
39
|
+
end
|
40
|
+
result << ">"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'state_flow'
|
3
|
+
module StateFlow
|
4
|
+
|
5
|
+
class Event
|
6
|
+
include Action::Executable
|
7
|
+
attr_reader :flow, :name
|
8
|
+
|
9
|
+
def initialize(flow, name)
|
10
|
+
@flow = flow
|
11
|
+
@name = name.to_s.to_sym
|
12
|
+
end
|
13
|
+
|
14
|
+
def inspect
|
15
|
+
result = "<#{self.class.name} @name=#{@name.inspect}"
|
16
|
+
result << " @action=#{@action.inspect}" if @action
|
17
|
+
result << ">"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module StateFlow
|
2
|
+
class Log < ActiveRecord::Base
|
3
|
+
set_table_name 'state_flow_logs'
|
4
|
+
|
5
|
+
belongs_to :target, :polymorphic => true
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def fatal(message, options = nil); write(message, :fatal, options); end
|
9
|
+
def error(message, options = nil); write(message, :error, options); end
|
10
|
+
def warn (message, options = nil); write(message, :warn , options); end
|
11
|
+
def info (message, options = nil); write(message, :info , options); end
|
12
|
+
def debug(message, options = nil); write(message, :debug, options); end
|
13
|
+
|
14
|
+
private
|
15
|
+
def write(message, level, options = nil)
|
16
|
+
log = self.new(options || {})
|
17
|
+
log.level = level.to_s
|
18
|
+
log.descriptions = message.is_a?(Exception) ? format_exception(message) : message
|
19
|
+
unless log.save
|
20
|
+
logger.error(log.inspect)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def format_exception(exception)
|
25
|
+
'%s\n %s' % [exception.to_s, exception.backtrace.join("\n ")]
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'state_flow'
|
3
|
+
module StateFlow
|
4
|
+
|
5
|
+
class NamedAction < Action
|
6
|
+
attr_reader :name
|
7
|
+
def initialize(flow, name)
|
8
|
+
super(flow)
|
9
|
+
@name = name.to_s.to_sym
|
10
|
+
end
|
11
|
+
|
12
|
+
def proceed
|
13
|
+
flow.process_with_log(self.record, success_key, failure_key) do
|
14
|
+
self.record.send(name)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
data/lib/state_flow.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
module StateFlow
|
2
|
+
autoload :Base, 'state_flow/base'
|
3
|
+
autoload :Builder, 'state_flow/builder'
|
4
|
+
autoload :Action, 'state_flow/action'
|
5
|
+
autoload :NamedAction, 'state_flow/named_action'
|
6
|
+
autoload :Event, 'state_flow/event'
|
7
|
+
autoload :Entry, 'state_flow/entry'
|
8
|
+
autoload :Log, 'state_flow/log'
|
9
|
+
|
10
|
+
# autoload :ActiveRecord, 'state_flow/active_record'
|
11
|
+
|
12
|
+
def self.included(mod)
|
13
|
+
mod.module_eval do
|
14
|
+
extend ::StateFlow::Builder::ClientClassMethods
|
15
|
+
extend ::StateFlow::Base::ClassMethods
|
16
|
+
# include ::StateFlow::ActiveRecord if mod.ancestors.map{|m| m.name}.include?('ActiveRecord::Base')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
data/spec/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
/*.sqlite3
|