state_flow 0.1.0
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/.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
|