state_flow 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -3,38 +3,76 @@
3
3
 
4
4
  状態遷移のためのDSLを提供するためのActiveRecordを拡張するプラグインです。
5
5
 
6
+ 基本的にUMLのステートチャート図に忠実に状態遷移を記述できるようにすることが最終目標です。
7
+ 現在のところ、以下のような機能があります。
8
+ * イベント/ガード/アクションの設定
9
+ * イベントとして定義可能なのは、以下の通りです。
10
+ * モデルに対する任意の名前のアクション(メソッドとして定義されるので、メソッド名に被っていると上書きされます)。
11
+ * アクションの戻り値
12
+ * 例外
13
+ * ガードはその条件に該当するかどうかを判断するメソッド名を指定します。
14
+ * アクションはモデルのメソッドを指定しますが、状態を変更するコードは不要です。
15
+ * 状態の遷移は自動で行われます。
16
+ * 状態のネスト
17
+ * 親の状態で定義されたイベントが実行された場合に正しく状態を遷移します。
18
+ * 状態を遷移する際のトランザクションの制御
19
+ * 各状態間の遷移毎にトランザクションを発行します。
20
+ * 例外発生時などにはロールバックを行い、(指定されていれば)例外に対応する状態に遷移してモデルを保存します。
21
+
22
+ === サンプル
6
23
  以下のような記述が可能です。
7
24
 
8
- class Page < ActiveRecord::Base
9
- validates_presence_of :name
25
+ class Order < ActiveRecord::Base
26
+
27
+ class StockShortageError < StandardError
28
+ end
10
29
 
11
30
  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, '公開失敗'
31
+ entry '00', :waiting_settling , '決済前'
32
+ entry '01', :online_settling , '決済中'
33
+ entry '02', :receiving , '入金待ち'
34
+ entry '03', :deliver_preparing, '配送準備中'
18
35
  end
19
36
 
20
37
  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
38
+ origin(:waiting_settling)
39
+
40
+ group(:valid) do
41
+ from(:waiting_settling) do
42
+ guard(:pay_cash_on_delivery?).action(:reserve_point).action(:reserve_stock){
43
+ event(:reserve_stock_ok).to(:deliver_preparing)
44
+ event_else.action(:delete_point).to(:stock_error)
45
+ }
46
+ guard_else.action(:reserve_point).action(:reserve_stock, :temporary => true){
47
+ event(:reserve_stock_ok){
48
+ guard(:bank_deposit?).action(:send_mail_thanks).to(:receiving)
49
+ guard(:credit_card?).to(:online_settling)
50
+ guard(:foreign_payment?).action(:settle).to(:online_settling)
51
+ }
52
+ event_else{
53
+ guard(:foreign_payment?).action(:delete_point).action(:send_mail_stock_shortage)
54
+ }.to(:stock_error)
55
+ }
56
+ recover(StockShortageError).to(:stock_error)
57
+ end
58
+
59
+ from(:online_settling) do
60
+ guard(:credit_card?).action(:settle){
61
+ event(:ok).action(:reserve_stock).action(:send_mail_thanks).to(:deliver_preparing)
62
+ event_else.action(:release_stock).action(:delete_point).to(:settlement_error)
63
+ }
64
+ guard(:foreign_payment?){
65
+ event(:settlement_ok).to(:deliver_preparing)
66
+ event(:settlement_ng).action(:release_stock).action(:delete_point).action(:send_mail_invalid_purchage).to(:settlement_error)
67
+ }
68
+ recover(Exception).action(:release_stock).action(:delete_point).to(:settlement_error)
69
+ end
28
70
  end
29
-
30
- state :published
31
- end
32
-
33
- def start_publish
34
- # 公開時の処理
35
71
  end
36
72
  end
37
73
 
74
+ 詳しくはspec/order_spec.rbをご覧ください。
75
+
38
76
  == セットアップ
39
77
  state_flowプラグインはselectable_attrに依存しています。
40
78
 
@@ -78,8 +116,8 @@ config/initializers/state_flow.rb
78
116
 
79
117
  == Example
80
118
  以下のテスト用のモデルや、テストをご覧ください。
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
119
+ http://github.com/akm/state_flow/blob/master/spec/resources/models/order.rb
120
+ http://github.com/akm/state_flow/blob/master/spec/order_spec.rb
83
121
 
84
122
 
85
123
  Copyright (c) 2009 [Takeshi AKIMA], released under the MIT license
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.0
@@ -2,66 +2,32 @@
2
2
  require 'state_flow'
3
3
  module StateFlow
4
4
 
5
- class Action
6
- attr_reader :flow
7
- attr_accessor :success_key
8
- attr_accessor :failure_key
9
- attr_accessor :lock, :if, :unless
5
+ class Action < Element
6
+ include ActionClient
7
+ include EventClient
8
+ include GuardClient
10
9
 
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)
10
+ attr_reader :method_name, :method_args
11
+ def initialize(origin, method_name, *method_args, &block)
12
+ @method_name, @method_args = method_name, method_args
13
+ super(origin, &block)
37
14
  end
38
15
 
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
16
+ def process(context)
17
+ context.trace(self)
18
+ context.mark_proceeding
19
+ exception_handling(context) do
20
+ result = context.record_send(method_name, *method_args)
21
+ event = event_for_action_result(result)
22
+ if event
23
+ event.process(context)
24
+ elsif action
25
+ action.process(context)
26
+ end
27
+ update_to_destination(context)
28
+ end
63
29
  end
64
30
 
65
31
  end
66
-
32
+
67
33
  end
@@ -0,0 +1,16 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'state_flow'
3
+ module StateFlow
4
+
5
+ module ActionClient
6
+ def action(method_name = nil, *method_args, &block)
7
+ if method_name
8
+ result = Action.new(self, method_name, *method_args, &block)
9
+ @action = result
10
+ result
11
+ else
12
+ @action
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,21 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'state_flow'
3
+ module StateFlow
4
+
5
+ class ActionEvent < Event
6
+ ELSE = Object.new
7
+
8
+ attr_reader :matcher
9
+ def initialize(origin, matcher, &block)
10
+ @matcher = matcher
11
+ super(origin, &block)
12
+ end
13
+
14
+ def match?(action_result)
15
+ return true if matcher == ActionEvent::ELSE
16
+ matcher === action_result
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -3,28 +3,32 @@ require 'state_flow'
3
3
  module StateFlow
4
4
 
5
5
  class Base
6
- include Builder
7
6
 
8
- module ClassMethods
7
+ module ClientClassMethods
8
+ def state_flow_for(selectable_attr)
9
+ return nil unless @state_flows
10
+ @state_flows.detect{|flow| flow.attr_name == selectable_attr}
11
+ end
9
12
 
10
- def process_state(selectable_attr, *keys, &block)
13
+ def state_flow(selectable_attr, options = nil, &block)
11
14
  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
15
+ :attr_key_name => "#{self.enum_base_name(selectable_attr)}_key".to_sym
16
+ }.update(options || {})
17
+ flow = Base.new(self, selectable_attr, options[:attr_key_name])
18
+ flow.instance_eval(&block)
19
+ @state_flows ||= []
20
+ @state_flows << flow
21
+ module_eval(<<-EOS, __FILE__, __LINE__)
22
+ def process_#{selectable_attr}(context_or_options = nil)
23
+ flow = self.class.state_flow_for(:#{selectable_attr})
24
+ context = flow.prepare_context(self, context_or_options)
25
+ context.process(flow)
24
26
  end
25
- end
27
+ EOS
28
+ NamedEvent.build_event_methods(flow)
29
+ flow
26
30
  end
27
-
31
+
28
32
  def transaction_if_need(with_transaction, &block)
29
33
  if with_transaction
30
34
  self.transaction(&block)
@@ -33,30 +37,70 @@ module StateFlow
33
37
  end
34
38
  end
35
39
  end
36
-
40
+
37
41
  attr_reader :klass, :attr_name, :attr_key_name, :status_keys
38
- attr_reader :entries
39
42
  def initialize(klass, attr_name, attr_key_name)
40
43
  @klass, @attr_name, @attr_key_name = klass, attr_name, attr_key_name
41
44
  @status_keys = klass.send(@attr_key_name.to_s.pluralize).map{|s| s.to_sym}
42
- @entries = []
43
45
  end
44
46
 
45
47
  def state_cd_by_key(key)
46
48
  @state_cd_by_key_method_name ||= "#{klass.enum_base_name(attr_name)}_id_by_key"
47
49
  klass.send(@state_cd_by_key_method_name, key)
48
50
  end
49
-
50
- def entry_for(key)
51
- unless @entry_hash
52
- @entry_hash = entries.inject({}) do |dest, entry|
53
- dest[entry.key] = entry
51
+
52
+ def state(name, &block)
53
+ result = State.new(self, name, &block)
54
+ states << result
55
+ result
56
+ end
57
+ alias_method :group, :state
58
+ alias_method :state_group, :state
59
+
60
+ def states
61
+ @states ||= []
62
+ end
63
+
64
+ def all_states
65
+ unless @all_states
66
+ @all_states = states.map{|state| state.descendants}.flatten.inject({}) do |dest, state|
67
+ dest[state.name] = state
54
68
  dest
55
69
  end
56
70
  end
57
- @entry_hash[key]
71
+ @all_states
72
+ end
73
+
74
+ def concrete_states
75
+ unless @concrete_states
76
+ @concrete_states = {}
77
+ all_states.each do |name, state|
78
+ @concrete_states[state.name] = state if state.concrete?
79
+ end
80
+ end
81
+ @concrete_states
82
+ end
83
+
84
+ def origin(value = nil)
85
+ if value
86
+ @origin_name = value
87
+ else
88
+ @origin ||= all_states[@origin_name]
89
+ end
90
+ end
91
+
92
+ def prepare_context(record, context_or_options = nil)
93
+ context_or_options.is_a?(StateFlow::Context) ?
94
+ context_or_options :
95
+ StateFlow::Context.new(self, record, context_or_options)
96
+ end
97
+
98
+ def process(context)
99
+ current_key = context.current_attr_key
100
+ state = concrete_states[current_key]
101
+ state.process(context)
102
+ context
58
103
  end
59
- alias_method :[], :entry_for
60
104
 
61
105
  def process_with_log(record, success_key, failure_key)
62
106
  origin_state = record.send(attr_name)
@@ -99,7 +143,6 @@ module StateFlow
99
143
  def inspect
100
144
  result = "<#{self.class.name} @attr_name=#{@attr_name.inspect} @attr_key_name=#{@attr_key_name.inspect}"
101
145
  result << " @klass=\"#{@klass.name}\""
102
- result << " @entries=#{@entries.inspect}"
103
146
  result << '>'
104
147
  end
105
148
 
@@ -0,0 +1,82 @@
1
+ module StateFlow
2
+
3
+ class Context
4
+
5
+ attr_reader :flow, :record, :options
6
+
7
+ def initialize(flow, record, options = nil)
8
+ @flow, @record = flow, record
9
+ @options = {
10
+ :save => :save!,
11
+ :keep_process => true
12
+ }.update(options || {})
13
+ end
14
+
15
+ def process(flow_or_named_event = flow)
16
+ flow.klass.transaction do
17
+ flow_or_named_event.process(self)
18
+ save_record_if_need
19
+ end
20
+ if options[:keep_process]
21
+ last_current_key = current_attr_key
22
+ while true
23
+ @mark_proceeding = false
24
+ flow.process(self)
25
+ save_record_if_need if @mark_proceeding
26
+ break unless @mark_proceeding
27
+ break if last_current_key == current_attr_key
28
+ last_current_key = current_attr_key
29
+ end
30
+ end
31
+ self
32
+ end
33
+
34
+ def mark_proceeding
35
+ @mark_proceeding = true
36
+ end
37
+
38
+ def trace(object)
39
+ stack_trace << object
40
+ end
41
+
42
+ def stack_trace
43
+ @stack_trace ||= []
44
+ end
45
+
46
+
47
+ def save_record_if_need
48
+ return unless options[:save]
49
+ record.send(options[:save])
50
+ end
51
+
52
+ def record_send(*args, &block)
53
+ record.send(*args, &block)
54
+ end
55
+
56
+ def record_reload_if_possible
57
+ record.reload unless record.new_record?
58
+ end
59
+
60
+ def transaction_rollback
61
+ record.class.connection.rollback_db_transaction
62
+ end
63
+
64
+ def exceptions
65
+ @exceptions ||= []
66
+ end
67
+
68
+ def recovered_exceptions
69
+ @recovered_exceptions ||= []
70
+ end
71
+
72
+ def recovered?(exception)
73
+ recovered_exceptions.include?(exception)
74
+ end
75
+
76
+ def current_attr_key
77
+ record_send(flow.attr_key_name)
78
+ end
79
+
80
+ end
81
+
82
+ end
@@ -0,0 +1,60 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'state_flow'
3
+ module StateFlow
4
+
5
+ class Element
6
+ include ElementVisitable
7
+
8
+ attr_reader :origin
9
+ attr_reader :destination
10
+ def initialize(origin, &block)
11
+ @origin = origin
12
+ instance_eval(&block) if block
13
+ end
14
+
15
+ def to(destination)
16
+ @destination = destination
17
+ end
18
+
19
+ def flow
20
+ @flow || origin.flow
21
+ end
22
+
23
+ def state
24
+ origin.is_a?(State) ? origin : origin.state
25
+ end
26
+
27
+ def update_to_destination(context)
28
+ return unless destination
29
+ context.mark_proceeding
30
+ context.record_send("#{flow.attr_key_name}=", destination)
31
+ end
32
+
33
+ class << self
34
+ def uninspected_var(*vars)
35
+ @@uninspected_var_hash ||= {}
36
+ @@uninspected_var_hash[self] ||= []
37
+ @@uninspected_var_hash[self].concat(vars.map{|v| v.to_s.sub(/^([^@])/){"@#{$1}"}})
38
+ end
39
+
40
+ def uninspected_vars
41
+ @uninspected_vars ||= @@uninspected_var_hash.nil? ? %w(@flow @origin) :
42
+ self.ancestors.map{|klass| @@uninspected_var_hash[klass] || []}.flatten
43
+ end
44
+ end
45
+ self.uninspected_var :flow, :origin, :events, :guards, :action
46
+
47
+ def inspect
48
+ vars = (instance_variables - self.class.uninspected_vars).map do |name|
49
+ "#{name}=#{instance_variable_get(name).inspect}"
50
+ end
51
+ # vars = []
52
+ vars.unshift("%s:%#x" % [self.class.name, self.object_id])
53
+ "#<#{vars.join(' ')}>"
54
+ end
55
+
56
+
57
+
58
+ end
59
+
60
+ end
@@ -0,0 +1,19 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'state_flow'
3
+ module StateFlow
4
+
5
+ module ElementVisitable
6
+ # Visitorパターン
7
+ def visit(&block)
8
+ results = block.call(self)
9
+ (results || [:events, :guards, :action]).each do |elements_name|
10
+ next if [:events, :guards, :action].include?(elements_name) && !respond_to?(elements_name)
11
+ elements = send(elements_name)
12
+ elements = [elements] unless elements.is_a?(Array)
13
+ elements.each do |element|
14
+ element.visit(&block) if element
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -2,19 +2,19 @@
2
2
  require 'state_flow'
3
3
  module StateFlow
4
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 << ">"
5
+ class Event < Element
6
+ include GuardClient
7
+ include ActionClient
8
+ include ExceptionHandlerClient
9
+
10
+ def process(context)
11
+ context.trace(self)
12
+ if guard = guard_for(context)
13
+ guard.process(context)
14
+ else
15
+ action.process(context) if action
16
+ end
17
+ update_to_destination(context)
18
18
  end
19
19
  end
20
20
 
@@ -0,0 +1,88 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'state_flow'
3
+ module StateFlow
4
+
5
+ module EventClient
6
+ include ExceptionHandlerClient
7
+
8
+ def events
9
+ @events ||= []
10
+ end
11
+
12
+ def event(*name_or_matcher_or_exceptions, &block)
13
+ options = name_or_matcher_or_exceptions.extract_options!
14
+ if name_or_matcher_or_exceptions.all?{|v| v.is_a?(Class) && v <= Exception}
15
+ handle_exception(*name_or_matcher_or_exceptions.push(options), &block)
16
+ else
17
+ if name_or_matcher_or_exceptions.length > 1
18
+ raise ArgumentError, "event(event_name) or event(action_result) or event(Exception1, Exception2...)"
19
+ end
20
+ name_or_matcher = name_or_matcher_or_exceptions.first
21
+ if origin.is_a?(State)
22
+ named_event(name_or_matcher, &block)
23
+ else
24
+ action_event(name_or_matcher, &block)
25
+ end
26
+ end
27
+ end
28
+
29
+ def named_event(name, &block)
30
+ result = NamedEvent.new(self, name, &block)
31
+ events << result
32
+ result
33
+ end
34
+
35
+ def action_event(matcher, &block)
36
+ result = ActionEvent.new(self, matcher, &block)
37
+ events << result
38
+ result
39
+ end
40
+
41
+ def event_else(&block)
42
+ if origin.is_a?(State)
43
+ raise ArgumentError, "event_else can't be after/in a state but an action"
44
+ end
45
+ result = ActionEvent.new(self, ActionEvent::ELSE, &block)
46
+ events << result
47
+ result
48
+ end
49
+
50
+ def handle_exception(*args, &block)
51
+ result = ExceptionHandler.new(self, *args, &block)
52
+ events << result
53
+ result
54
+ end
55
+
56
+ def recover(*args, &block)
57
+ options = {
58
+ :recovering => true,
59
+ :rolling_back => true,
60
+ :logging => :error
61
+ }.update(args.extract_options!)
62
+ handle_exception(*(args << options), &block)
63
+ end
64
+
65
+ def event_for_action_result(result)
66
+ events.detect{|ev| ev.match?(result)}
67
+ end
68
+
69
+ def exception_handling(context)
70
+ begin
71
+ yield
72
+ rescue Exception => exception
73
+ context.exceptions << exception
74
+ context.trace(exception)
75
+ handlers = events.select{|ev| ev.is_a?(ExceptionHandler)}
76
+ handlers.each do |handler|
77
+ next unless handler.match?(exception)
78
+ handler.process(context)
79
+ break
80
+ end
81
+ raise exception unless context.recovered?(exception)
82
+ end
83
+ end
84
+
85
+ end
86
+
87
+
88
+ end