state-fu 0.12.3 → 0.13.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/README.textile +1 -1
- data/lib/binding.rb +10 -5
- data/lib/executioner.rb +23 -90
- data/lib/lathe.rb +1 -1
- data/lib/machine.rb +19 -16
- data/lib/method_factory.rb +100 -142
- data/lib/persistence.rb +2 -2
- data/lib/persistence/active_record.rb +3 -3
- data/lib/persistence/attribute.rb +4 -4
- data/lib/persistence/base.rb +4 -20
- data/lib/state-fu.rb +4 -2
- data/lib/support/core_ext.rb +22 -44
- data/lib/support/{logger.rb → logging.rb} +5 -5
- data/lib/transition.rb +11 -13
- data/spec/features/active_record_auto_save_spec.rb +22 -0
- data/spec/features/not_requirements_spec.rb +37 -75
- data/spec/features/shared_log_spec.rb +1 -1
- data/spec/integration/active_record_persistence_spec.rb +21 -13
- data/spec/integration/example_01_document_spec.rb +5 -4
- data/spec/integration/requirement_reflection_spec.rb +5 -4
- data/spec/integration/transition_spec.rb +2 -2
- data/spec/spec_helper.rb +2 -2
- data/spec/state_fu_spec.rb +45 -52
- data/spec/units/binding_spec.rb +1 -1
- metadata +5 -6
- data/lib/support/methodical.rb +0 -17
- data/spec/features/method_missing_only_once_spec.rb +0 -28
data/lib/persistence.rb
CHANGED
@@ -20,7 +20,7 @@ module StateFu
|
|
20
20
|
# end
|
21
21
|
#
|
22
22
|
# def write_attribute( string_value )
|
23
|
-
#
|
23
|
+
# Logging.debug "magnetising ( #{field_name} => #{string_value} on #{object.inspect}"
|
24
24
|
# object.send "magnetised_#{field_name}=", string_value
|
25
25
|
# end
|
26
26
|
# end
|
@@ -56,7 +56,7 @@ module StateFu
|
|
56
56
|
persister_class = class_for klass, field_name
|
57
57
|
prepare_field( klass, field_name, persister_class)
|
58
58
|
returning persister_class.new( binding, field_name ) do |persister|
|
59
|
-
|
59
|
+
Logging.debug( "#{persister_class}: method #{binding.method_name} as field #{persister.field_name}" )
|
60
60
|
end
|
61
61
|
end
|
62
62
|
|
@@ -4,7 +4,7 @@ module StateFu
|
|
4
4
|
|
5
5
|
def self.prepare_field( klass, field_name )
|
6
6
|
_field_name = field_name
|
7
|
-
|
7
|
+
Logging.debug("Preparing ActiveRecord field #{klass}.#{field_name}")
|
8
8
|
|
9
9
|
# this adds a before_save hook to ensure that the field is initialized
|
10
10
|
# (and the initial state set) before create.
|
@@ -20,12 +20,12 @@ module StateFu
|
|
20
20
|
# Attribute version, so just do the simplest thing we can.
|
21
21
|
|
22
22
|
def read_attribute
|
23
|
-
|
23
|
+
Logging.debug "Read attribute #{field_name}, got #{object.send(:read_attribute,field_name)} for #{object.inspect}"
|
24
24
|
object.send( :read_attribute, field_name )
|
25
25
|
end
|
26
26
|
|
27
27
|
def write_attribute( string_value )
|
28
|
-
|
28
|
+
Logging.debug "Write attribute #{field_name} to #{string_value} for #{object.inspect}"
|
29
29
|
object.send( :write_attribute, field_name, string_value )
|
30
30
|
end
|
31
31
|
|
@@ -5,7 +5,7 @@ module StateFu
|
|
5
5
|
def self.prepare_field( klass, field_name )
|
6
6
|
# ensure getter exists
|
7
7
|
unless klass.instance_methods.map(&:to_sym).include?( field_name.to_sym )
|
8
|
-
|
8
|
+
Logging.debug "Adding attr_reader :#{field_name} for #{klass}"
|
9
9
|
_field_name = field_name
|
10
10
|
klass.class_eval do
|
11
11
|
private
|
@@ -15,7 +15,7 @@ module StateFu
|
|
15
15
|
|
16
16
|
# ensure setter exists
|
17
17
|
unless klass.instance_methods.map(&:to_sym).include?( :"#{field_name}=" )
|
18
|
-
|
18
|
+
Logging.debug "Adding attr_writer :#{field_name}= for #{klass}"
|
19
19
|
_field_name = field_name
|
20
20
|
klass.class_eval do
|
21
21
|
private
|
@@ -32,13 +32,13 @@ module StateFu
|
|
32
32
|
|
33
33
|
def read_attribute
|
34
34
|
string = object.send( field_name )
|
35
|
-
|
35
|
+
Logging.debug "Read attribute #{field_name}, got #{string.inspect} for #{object.inspect}"
|
36
36
|
string
|
37
37
|
end
|
38
38
|
|
39
39
|
def write_attribute( string_value )
|
40
40
|
writer_method = "#{field_name}="
|
41
|
-
|
41
|
+
Logging.debug "Writing attribute #{field_name} -> #{string_value.inspect} for #{object.inspect}"
|
42
42
|
object.send( writer_method, string_value )
|
43
43
|
end
|
44
44
|
|
data/lib/persistence/base.rb
CHANGED
@@ -8,25 +8,9 @@ module StateFu
|
|
8
8
|
|
9
9
|
attr_reader :binding, :field_name, :current_state
|
10
10
|
|
11
|
-
def self.prepare_class( klass )
|
12
|
-
unless klass.instance_methods.include?( :method_missing_before_state_fu )
|
13
|
-
alias_method :method_missing_before_state_fu, :method_missing
|
14
|
-
klass.class_eval do
|
15
|
-
def method_missing( method_name, *args, &block )
|
16
|
-
state_fu!
|
17
|
-
begin
|
18
|
-
send( method_name, *args, &block )
|
19
|
-
rescue NoMethodError => e
|
20
|
-
method_missing_before_state_fu( method_name, *args, &block )
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
11
|
# define this method in subclasses to do any preparation
|
28
12
|
def self.prepare_field( klass, field_name )
|
29
|
-
|
13
|
+
Logging.warn("Abstract method in #{self}.prepare_field called. Override me!")
|
30
14
|
end
|
31
15
|
|
32
16
|
def initialize( binding, field_name )
|
@@ -36,11 +20,11 @@ module StateFu
|
|
36
20
|
@current_state = find_current_state()
|
37
21
|
|
38
22
|
if current_state.nil?
|
39
|
-
|
40
|
-
|
23
|
+
Logging.warn("undefined state for binding #{binding} on #{object} with field_name #{field_name.inspect}")
|
24
|
+
Logging.warn("Machine for #{object} has no states: #{machine}") if machine.states.empty?
|
41
25
|
else
|
42
26
|
persist!
|
43
|
-
|
27
|
+
Logging.debug("#{object} resumes #{binding.method_name} at #{current_state.name}")
|
44
28
|
end
|
45
29
|
end
|
46
30
|
|
data/lib/state-fu.rb
CHANGED
@@ -15,12 +15,14 @@
|
|
15
15
|
# extend the core features.
|
16
16
|
#
|
17
17
|
# It is also delightfully elegant and easy to use for simple things.
|
18
|
+
%w( support support/active_support_lite ).each do |path|
|
19
|
+
$LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), path)))
|
20
|
+
end
|
18
21
|
|
19
22
|
[ 'support/core_ext',
|
20
|
-
'support/
|
23
|
+
'support/logging',
|
21
24
|
'support/applicable',
|
22
25
|
'support/arrays',
|
23
|
-
'support/methodical',
|
24
26
|
'support/has_options',
|
25
27
|
'support/vizier',
|
26
28
|
'support/plotter',
|
data/lib/support/core_ext.rb
CHANGED
@@ -38,53 +38,31 @@ class Array
|
|
38
38
|
end
|
39
39
|
|
40
40
|
class Object
|
41
|
-
|
42
|
-
|
43
|
-
self.class.class_eval do
|
44
|
-
define_method method_name, &block
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def __define_singleton_method( method_name, &block )
|
49
|
-
(class << self; self; end).class_eval do
|
50
|
-
define_method method_name, &block
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
|
55
|
-
def with_methods_on(other)
|
56
|
-
(class << self; self; end).class_eval do
|
57
|
-
# we need some accounting to ensure that everything behaves itself when
|
58
|
-
# .with_methods_on is called more than once.
|
59
|
-
@_with_methods_on ||= []
|
60
|
-
if !@_with_methods_on.include?("method_missing_before_#{other.__id__}")
|
61
|
-
alias_method "method_missing_before_#{other.__id__}", :method_missing
|
62
|
-
end
|
63
|
-
@_with_methods_on << "method_missing_before_#{other.__id__}"
|
64
|
-
|
65
|
-
define_method :method_missing do |method_name, *args|
|
66
|
-
if _other.respond_to?(method_name, true)
|
67
|
-
_other.__send__( method_name, *args )
|
68
|
-
else
|
69
|
-
send "method_missing_before_#{other.__id__}", method_name, *args
|
70
|
-
end
|
71
|
-
end
|
41
|
+
unless defined? instance_exec # 1.9
|
42
|
+
module InstanceExecMethods #:nodoc:
|
72
43
|
end
|
44
|
+
include InstanceExecMethods
|
73
45
|
|
74
|
-
|
46
|
+
# Evaluate the block with the given arguments within the context of
|
47
|
+
# this object, so self is set to the method receiver.
|
48
|
+
#
|
49
|
+
# From Mauricio's http://eigenclass.org/hiki/bounded+space+instance_exec
|
50
|
+
def instance_exec(*args, &block)
|
51
|
+
begin
|
52
|
+
old_critical, Thread.critical = Thread.critical, true
|
53
|
+
n = 0
|
54
|
+
n += 1 while respond_to?(method_name = "__instance_exec#{n}")
|
55
|
+
InstanceExecMethods.module_eval { define_method(method_name, &block) }
|
56
|
+
ensure
|
57
|
+
Thread.critical = old_critical
|
58
|
+
end
|
75
59
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
end
|
81
|
-
if !@_with_methods_on.include?("method_missing_before_#{other.__id__}")
|
82
|
-
alias_method :method_missing, "method_missing_before_#{other.__id__}"
|
83
|
-
undef_method "method_missing_before_#{other.__id__}"
|
84
|
-
end
|
60
|
+
begin
|
61
|
+
send(method_name, *args)
|
62
|
+
ensure
|
63
|
+
InstanceExecMethods.module_eval { remove_method(method_name) } rescue nil
|
64
|
+
end
|
85
65
|
end
|
86
|
-
|
87
|
-
result
|
88
|
-
end # with_methods_on
|
66
|
+
end
|
89
67
|
end
|
90
68
|
|
@@ -7,7 +7,7 @@ module StateFu
|
|
7
7
|
# Use Rails' log if running as a rails plugin; allow independent control of
|
8
8
|
# StateFu log level.
|
9
9
|
|
10
|
-
class
|
10
|
+
class Logging
|
11
11
|
cattr_accessor :prefix # prefix for log messages
|
12
12
|
cattr_accessor :suppress # set true to send messages to /dev/null
|
13
13
|
cattr_accessor :shared
|
@@ -101,10 +101,10 @@ module StateFu
|
|
101
101
|
case logger
|
102
102
|
when String
|
103
103
|
file = File.open(logger, File::WRONLY | File::APPEND)
|
104
|
-
@@logger =
|
105
|
-
when
|
104
|
+
@@logger = activesupport_logger_available? ? ActiveSupport::BufferedLogger.new(file) : Logger.new(file)
|
105
|
+
when Logger
|
106
106
|
@@logger = logger
|
107
|
-
when
|
107
|
+
when activesupport_logger_available? && ActiveSupport::BufferedLogger
|
108
108
|
@@logger = logger
|
109
109
|
else
|
110
110
|
default_logger
|
@@ -134,7 +134,7 @@ module StateFu
|
|
134
134
|
ActiveSupport::BufferedLogger.new(target)
|
135
135
|
end
|
136
136
|
else
|
137
|
-
|
137
|
+
Logger.new(target)
|
138
138
|
end
|
139
139
|
end
|
140
140
|
|
data/lib/transition.rb
CHANGED
@@ -165,7 +165,7 @@ module StateFu
|
|
165
165
|
# halt a transition with a message
|
166
166
|
# can be used to back out of a transition inside eg a state entry hook
|
167
167
|
def halt! message
|
168
|
-
raise TransitionHalted.new( self, message )
|
168
|
+
raise StateFu::TransitionHalted.new( self, message )
|
169
169
|
end
|
170
170
|
|
171
171
|
#
|
@@ -184,10 +184,10 @@ module StateFu
|
|
184
184
|
StateFu::Hooks::ALL_HOOKS.map do |owner, slot|
|
185
185
|
[ [owner, slot], send(owner).hooks[slot] ]
|
186
186
|
end.each do |address, hooks|
|
187
|
-
|
187
|
+
Logging.info("running #{address.inspect} hooks for #{object.class} #{object}")
|
188
188
|
owner,slot = *address
|
189
189
|
hooks.each do |hook|
|
190
|
-
|
190
|
+
Logging.info("running hook #{hooks} for #{object.class} #{object}")
|
191
191
|
@current_hook_slot = address
|
192
192
|
@current_hook = hook
|
193
193
|
run_hook hook
|
@@ -195,14 +195,14 @@ module StateFu
|
|
195
195
|
if slot == :entry
|
196
196
|
@accepted = true
|
197
197
|
@binding.persister.current_state = @target
|
198
|
-
|
198
|
+
Logging.info("State is now :#{@target.name} for #{object.class} #{object}")
|
199
199
|
end
|
200
200
|
end
|
201
201
|
# transition complete
|
202
202
|
@current_hook_slot = nil
|
203
203
|
@current_hook = nil
|
204
204
|
rescue TransitionHalted => e
|
205
|
-
|
205
|
+
Logging.info("Transition halted for #{object.class} #{object}: #{e.inspect}")
|
206
206
|
@errors << e
|
207
207
|
end
|
208
208
|
self
|
@@ -292,17 +292,15 @@ module StateFu
|
|
292
292
|
s
|
293
293
|
end
|
294
294
|
|
295
|
+
def evaluate(method_name_or_proc)
|
296
|
+
executioner.evaluate(method_name_or_proc)
|
297
|
+
end
|
298
|
+
alias_method :call, :evaluate
|
299
|
+
|
295
300
|
private
|
296
301
|
|
297
302
|
def executioner
|
298
|
-
@executioner ||= Executioner.new( self )
|
299
|
-
machine.inject_helpers_into( ex )
|
300
|
-
machine.inject_methods_into( ex )
|
301
|
-
end
|
302
|
-
end
|
303
|
-
|
304
|
-
def evaluate(method_name_or_proc)
|
305
|
-
executioner.evaluate(method_name_or_proc)
|
303
|
+
@executioner ||= Executioner.new( self )
|
306
304
|
end
|
307
305
|
|
308
306
|
def evaluate_requirement_message( name, revalidate=false)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.expand_path("#{File.dirname(__FILE__)}/../helper")
|
2
|
+
|
3
|
+
describe "machine(:autosave => true)" do
|
4
|
+
before(:all) do
|
5
|
+
reset!
|
6
|
+
prepare_active_record() do
|
7
|
+
def self.up
|
8
|
+
create_table :example_records do |t|
|
9
|
+
t.string :name, :null => false
|
10
|
+
t.string :state_fu_field, :null => false
|
11
|
+
t.string :description
|
12
|
+
t.string :status
|
13
|
+
t.timestamps
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should automatically save the record when a transition is complete" do
|
20
|
+
pending
|
21
|
+
end
|
22
|
+
end
|
@@ -1,57 +1,32 @@
|
|
1
1
|
require File.expand_path("#{File.dirname(__FILE__)}/../helper")
|
2
2
|
|
3
3
|
module RequirementFeatureHelper
|
4
|
-
def account_expired?
|
5
|
-
!! account_expired
|
6
|
-
end
|
7
4
|
|
8
5
|
def valid_password?
|
9
6
|
!! valid_password
|
10
7
|
end
|
11
8
|
end
|
12
9
|
|
13
|
-
# it_should_behave_like "!" do
|
14
|
-
shared_examples_for "not requirements" do
|
15
|
-
describe "requirements with names beginning with no[t]_" do
|
16
|
-
|
17
|
-
it "should return the opposite of the requirement name without not_" do
|
18
|
-
# @obj.current_state.should == :guest
|
19
|
-
@obj.stfu.teleport! :anonymous
|
20
|
-
@obj.valid_password = false
|
21
|
-
@binding.can_has_valid_password?.should == false
|
22
|
-
@binding.can_has_not_valid_password?.should == true
|
23
|
-
@binding.can_has_no_valid_password?.should == true
|
24
|
-
@obj.valid_password = true
|
25
|
-
@binding.can_has_valid_password?.should == true
|
26
|
-
@binding.can_has_not_valid_password?.should == false
|
27
|
-
@binding.can_has_no_valid_password?.should == false
|
28
|
-
end
|
29
|
-
|
30
|
-
it "should call the method directly if one exists" do
|
31
|
-
@obj.valid_password = true
|
32
|
-
(class << @obj; self; end).class_eval do
|
33
|
-
define_method( :no_valid_password? ) { true }
|
34
|
-
end
|
35
|
-
@binding.can_has_valid_password?.should == true
|
36
|
-
@binding.can_has_not_valid_password?.should == false
|
37
|
-
@binding.can_has_no_valid_password?.should == true
|
38
|
-
end
|
39
|
-
|
40
|
-
end
|
41
|
-
|
42
|
-
end
|
43
|
-
|
44
10
|
describe "requirements" do
|
11
|
+
|
45
12
|
before(:all) do
|
46
13
|
reset!
|
47
14
|
make_pristine_class('Klass')
|
15
|
+
|
48
16
|
Klass.class_eval do
|
49
17
|
attr_accessor :valid_password
|
50
18
|
attr_accessor :account_expired
|
19
|
+
|
20
|
+
def account_expired?
|
21
|
+
!! account_expired
|
22
|
+
end
|
51
23
|
end
|
24
|
+
|
52
25
|
@machine = StateFu::Machine.new do
|
53
26
|
initial_state :guest
|
54
27
|
|
28
|
+
define(:valid_password?) { !! valid_password }
|
29
|
+
|
55
30
|
event :has_valid_password, :from => :anonymous, :to => :logged_in do
|
56
31
|
requires :valid_password?
|
57
32
|
end
|
@@ -62,9 +37,12 @@ describe "requirements" do
|
|
62
37
|
|
63
38
|
event :has_no_valid_password, :from => :anonymous, :to => :suspect do
|
64
39
|
requires :no_valid_password?
|
65
|
-
end
|
66
|
-
|
40
|
+
end
|
67
41
|
end
|
42
|
+
|
43
|
+
@machine.bind!(Klass, :default)
|
44
|
+
@obj = Klass.new
|
45
|
+
@binding = @obj.state_fu
|
68
46
|
end
|
69
47
|
|
70
48
|
before :each do
|
@@ -72,47 +50,31 @@ describe "requirements" do
|
|
72
50
|
@obj.account_expired = false
|
73
51
|
end
|
74
52
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
it "should have methods on the binding" do
|
91
|
-
# this is a little misleading because theyre not evaluated on the binding ..
|
92
|
-
@binding.respond_to?(:valid_password?).should == true
|
93
|
-
@binding.respond_to?(:account_expired?).should == true
|
94
|
-
@binding.respond_to?(:not_valid_password?).should == false
|
95
|
-
@binding.respond_to?(:not_account_expired?).should == false
|
96
|
-
end
|
53
|
+
it "should have methods ...." do
|
54
|
+
@obj.should respond_to(:account_expired?)
|
55
|
+
@machine.named_procs.keys.should include(:valid_password?)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should return the opposite of the requirement name without not_" do
|
59
|
+
@obj.stfu.teleport! :anonymous
|
60
|
+
@obj.valid_password = false
|
61
|
+
@binding.can_has_valid_password?.should == false
|
62
|
+
@binding.can_has_not_valid_password?.should == true
|
63
|
+
@binding.can_has_no_valid_password?.should == true
|
64
|
+
@obj.valid_password = true
|
65
|
+
@binding.can_has_valid_password?.should == true
|
66
|
+
@binding.can_has_not_valid_password?.should == false
|
67
|
+
@binding.can_has_no_valid_password?.should == false
|
97
68
|
end
|
98
69
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
@binding = @obj.state_fu
|
104
|
-
Klass.class_eval do
|
105
|
-
include RequirementFeatureHelper
|
106
|
-
end
|
70
|
+
it "should call the method directly if one exists" do
|
71
|
+
@obj.valid_password = true
|
72
|
+
(class << @obj; self; end).class_eval do
|
73
|
+
define_method( :no_valid_password? ) { true }
|
107
74
|
end
|
75
|
+
@binding.can_has_valid_password?.should == true
|
76
|
+
@binding.can_has_not_valid_password?.should == false
|
77
|
+
@binding.can_has_no_valid_password?.should == true
|
78
|
+
end
|
108
79
|
|
109
|
-
it_should_behave_like "not requirements"
|
110
|
-
|
111
|
-
it "should have methods on the object" do
|
112
|
-
@obj.respond_to?(:valid_password?).should == true
|
113
|
-
@obj.respond_to?(:not_valid_password?).should == false
|
114
|
-
@obj.respond_to?(:account_expired?).should == true
|
115
|
-
@obj.respond_to?(:not_account_expired?).should == false
|
116
|
-
end
|
117
|
-
end
|
118
80
|
end
|