davidlee-state-fu 0.0.1
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/LICENSE +40 -0
- data/README.textile +174 -0
- data/Rakefile +87 -0
- data/lib/no_stdout.rb +32 -0
- data/lib/state-fu.rb +93 -0
- data/lib/state_fu/binding.rb +262 -0
- data/lib/state_fu/core_ext.rb +23 -0
- data/lib/state_fu/event.rb +98 -0
- data/lib/state_fu/exceptions.rb +42 -0
- data/lib/state_fu/fu_space.rb +50 -0
- data/lib/state_fu/helper.rb +189 -0
- data/lib/state_fu/hooks.rb +28 -0
- data/lib/state_fu/interface.rb +139 -0
- data/lib/state_fu/lathe.rb +247 -0
- data/lib/state_fu/logger.rb +10 -0
- data/lib/state_fu/machine.rb +159 -0
- data/lib/state_fu/method_factory.rb +95 -0
- data/lib/state_fu/persistence/active_record.rb +27 -0
- data/lib/state_fu/persistence/attribute.rb +46 -0
- data/lib/state_fu/persistence/base.rb +98 -0
- data/lib/state_fu/persistence/session.rb +7 -0
- data/lib/state_fu/persistence.rb +50 -0
- data/lib/state_fu/sprocket.rb +27 -0
- data/lib/state_fu/state.rb +45 -0
- data/lib/state_fu/transition.rb +213 -0
- data/spec/helper.rb +86 -0
- data/spec/integration/active_record_persistence_spec.rb +189 -0
- data/spec/integration/class_accessor_spec.rb +127 -0
- data/spec/integration/event_definition_spec.rb +74 -0
- data/spec/integration/ex_machine_for_accounts_spec.rb +79 -0
- data/spec/integration/example_01_document_spec.rb +127 -0
- data/spec/integration/example_02_string_spec.rb +87 -0
- data/spec/integration/instance_accessor_spec.rb +100 -0
- data/spec/integration/machine_duplication_spec.rb +95 -0
- data/spec/integration/requirement_reflection_spec.rb +201 -0
- data/spec/integration/sanity_spec.rb +31 -0
- data/spec/integration/state_definition_spec.rb +177 -0
- data/spec/integration/transition_spec.rb +1060 -0
- data/spec/spec.opts +7 -0
- data/spec/units/binding_spec.rb +145 -0
- data/spec/units/event_spec.rb +232 -0
- data/spec/units/exceptions_spec.rb +75 -0
- data/spec/units/fu_space_spec.rb +95 -0
- data/spec/units/lathe_spec.rb +567 -0
- data/spec/units/machine_spec.rb +237 -0
- data/spec/units/method_factory_spec.rb +359 -0
- data/spec/units/sprocket_spec.rb +71 -0
- data/spec/units/state_spec.rb +50 -0
- metadata +122 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module StateFu
|
|
2
|
+
module Persistence
|
|
3
|
+
DEFAULT_FIELD_NAME_SUFFIX = '_field'
|
|
4
|
+
|
|
5
|
+
def self.prepare_class( klass )
|
|
6
|
+
return if ( klass.instance_methods + klass.private_methods + klass.protected_methods ).map(&:to_sym).include?( :method_missing_before_state_fu )
|
|
7
|
+
alias_method :method_missing_before_state_fu, :method_missing
|
|
8
|
+
klass.class_eval do
|
|
9
|
+
def method_missing( method_name, *args, &block )
|
|
10
|
+
args.unshift method_name
|
|
11
|
+
if @state_fu_initialized
|
|
12
|
+
method_missing_before_state_fu( *args, &block )
|
|
13
|
+
else
|
|
14
|
+
state_fu!
|
|
15
|
+
if respond_to?(method_name)
|
|
16
|
+
send( *args, &block )
|
|
17
|
+
else
|
|
18
|
+
method_missing_before_state_fu( *args, &block )
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end # method_missing
|
|
22
|
+
end # class_eval
|
|
23
|
+
end # prepare_class
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def self.active_record_column?( klass, field_name )
|
|
27
|
+
Object.const_defined?("ActiveRecord") &&
|
|
28
|
+
::ActiveRecord.const_defined?("Base") &&
|
|
29
|
+
klass.ancestors.include?( ::ActiveRecord::Base ) &&
|
|
30
|
+
klass.columns.map(&:name).include?( field_name.to_s )
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.for( binding, field_name )
|
|
34
|
+
if active_record_column?( binding.object.class, field_name )
|
|
35
|
+
self::ActiveRecord.new( binding, field_name )
|
|
36
|
+
else
|
|
37
|
+
self::Attribute.new( binding, field_name )
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.prepare_field( klass, field_name )
|
|
42
|
+
if active_record_column?( klass, field_name )
|
|
43
|
+
self::ActiveRecord.prepare_field( klass, field_name )
|
|
44
|
+
else
|
|
45
|
+
self::Attribute.prepare_field( klass, field_name )
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module StateFu
|
|
2
|
+
class Sprocket # Abstract Superclass of State & Event
|
|
3
|
+
include StateFu::Helper # define apply!
|
|
4
|
+
|
|
5
|
+
attr_reader :machine, :name, :options, :hooks
|
|
6
|
+
|
|
7
|
+
def initialize(machine, name, options={})
|
|
8
|
+
@machine = machine
|
|
9
|
+
@name = name.to_sym
|
|
10
|
+
@options = options.symbolize_keys!
|
|
11
|
+
@hooks = StateFu::Hooks.for( self )
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# sneaky way to make some comparisons / duck typing a bit cleaner
|
|
15
|
+
alias_method :to_sym, :name
|
|
16
|
+
|
|
17
|
+
def add_hook slot, name, value
|
|
18
|
+
@hooks[slot.to_sym] << [name.to_sym, value]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def lathe(options={}, &block)
|
|
22
|
+
StateFu::Lathe.new( machine, self, options, &block )
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module StateFu
|
|
2
|
+
class State < StateFu::Sprocket
|
|
3
|
+
|
|
4
|
+
attr_reader :entry_requirements, :exit_requirements
|
|
5
|
+
|
|
6
|
+
def initialize(machine, name, options={})
|
|
7
|
+
@entry_requirements = [].extend ArrayWithSymbolAccessor
|
|
8
|
+
@exit_requirements = [].extend ArrayWithSymbolAccessor
|
|
9
|
+
super( machine, name, options )
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def events
|
|
13
|
+
machine.events.from(self)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
#
|
|
17
|
+
# Proxy methods to StateFu::Lathe
|
|
18
|
+
#
|
|
19
|
+
# TODO - build something meta to build these proxy events
|
|
20
|
+
def event( name, options={}, &block )
|
|
21
|
+
if block_given?
|
|
22
|
+
lathe.event( name, options, &block )
|
|
23
|
+
else
|
|
24
|
+
lathe.event( name, options )
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def enterable_by?( binding )
|
|
29
|
+
entry_requirements.reject do |r|
|
|
30
|
+
res = binding.evaluate_requirement( r )
|
|
31
|
+
end.empty?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def exitable_by?( binding )
|
|
35
|
+
exit_requirements.reject do |r|
|
|
36
|
+
binding.evaluate_requirement( r )
|
|
37
|
+
end.empty?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# allows @obj.state_fu.state === :new
|
|
41
|
+
def === other
|
|
42
|
+
self.to_sym === other.to_sym
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
module StateFu
|
|
2
|
+
|
|
3
|
+
# A 'context' class, created when an event is fired, or needs to be
|
|
4
|
+
# validated.
|
|
5
|
+
#
|
|
6
|
+
# This is what gets yielded to event hooks; it also gets attached
|
|
7
|
+
# to any TransitionHalted exceptions raised.
|
|
8
|
+
|
|
9
|
+
class Transition
|
|
10
|
+
include StateFu::Helper
|
|
11
|
+
include ContextualEval
|
|
12
|
+
attr_reader( :binding,
|
|
13
|
+
:machine,
|
|
14
|
+
:origin,
|
|
15
|
+
:target,
|
|
16
|
+
:event,
|
|
17
|
+
:args,
|
|
18
|
+
:errors,
|
|
19
|
+
:object,
|
|
20
|
+
:options,
|
|
21
|
+
:current_hook_slot,
|
|
22
|
+
:current_hook )
|
|
23
|
+
|
|
24
|
+
attr_accessor :test_only, :args, :options
|
|
25
|
+
|
|
26
|
+
def initialize( binding, event, target=nil, *args, &block )
|
|
27
|
+
# ensure event is a StateFu::Event
|
|
28
|
+
if event.is_a?( Symbol ) && e = binding.machine.events[ event ]
|
|
29
|
+
event = e
|
|
30
|
+
end
|
|
31
|
+
raise( ArgumentError, "Not an event: #{event}" ) unless event.is_a?( StateFu::Event )
|
|
32
|
+
|
|
33
|
+
# infer target if necessary
|
|
34
|
+
case target
|
|
35
|
+
when StateFu::State # good
|
|
36
|
+
when Symbol
|
|
37
|
+
target = binding.machine.states[ target ] ||
|
|
38
|
+
raise( ArgumentError, "target cannot be determined: #{target.inspect}" )
|
|
39
|
+
when NilClass
|
|
40
|
+
unless target = event.target
|
|
41
|
+
raise( ArgumentError, "target cannot be determined: #{target.inspect}" )
|
|
42
|
+
end
|
|
43
|
+
else
|
|
44
|
+
raise ArgumentError.new( target.inspect )
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# ensure target is valid for the event
|
|
48
|
+
unless event.targets.include?( target )
|
|
49
|
+
raise( StateFu::InvalidTransition.new( binding, event, binding.current_state, target,
|
|
50
|
+
"Illegal target #{target} for #{event}" ))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# ensure current_state is a valid origin for the event
|
|
54
|
+
unless event.origins.include?( binding.current_state )
|
|
55
|
+
raise( StateFu::InvalidTransition.new( binding, event, binding.current_state, target,
|
|
56
|
+
"Illegal event #{event.name} for current state #{binding.state_name}" ))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@options = args.extract_options!.symbolize_keys!
|
|
60
|
+
@binding = binding
|
|
61
|
+
@machine = binding.machine
|
|
62
|
+
@object = binding.object
|
|
63
|
+
@origin = binding.current_state
|
|
64
|
+
@target = target
|
|
65
|
+
@event = event
|
|
66
|
+
@args = args
|
|
67
|
+
@errors = []
|
|
68
|
+
@testing = @options.delete( :test_only )
|
|
69
|
+
|
|
70
|
+
machine.inject_helpers_into( self )
|
|
71
|
+
|
|
72
|
+
# do stuff with the transition in a block, if you like
|
|
73
|
+
apply!( &block ) if block_given?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def requirements
|
|
77
|
+
origin.exit_requirements + target.entry_requirements + event.requirements
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def unmet_requirements
|
|
81
|
+
requirements.reject do |requirement|
|
|
82
|
+
binding.evaluate_requirement( requirement )
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def unmet_requirement_messages
|
|
87
|
+
unmet_requirements.map do |r|
|
|
88
|
+
binding.evaluate_requirement_message(r, self )
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def check_requirements!
|
|
93
|
+
raise RequirementError.new( unmet_requirements.inspect ) unless requirements_met?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def requirements_met?
|
|
97
|
+
unmet_requirements.empty?
|
|
98
|
+
end
|
|
99
|
+
alias_method :valid?, :requirements_met?
|
|
100
|
+
|
|
101
|
+
def hooks_for( element, slot )
|
|
102
|
+
send(element).hooks[slot]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def hooks()
|
|
106
|
+
StateFu::Hooks::ALL_HOOKS.map do |owner, slot|
|
|
107
|
+
[ [owner, slot], send( owner ).hooks[ slot ] ]
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def current_state
|
|
112
|
+
if accepted?
|
|
113
|
+
:accepted
|
|
114
|
+
else
|
|
115
|
+
current_hook.state rescue :unfired
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def run_hook( hook )
|
|
120
|
+
evaluate_named_proc_or_method( hook )
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def halt!( message )
|
|
124
|
+
raise TransitionHalted.new( self, message )
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def fire!
|
|
128
|
+
return false if fired? # no infinite loops please
|
|
129
|
+
check_requirements!
|
|
130
|
+
@fired = true
|
|
131
|
+
begin
|
|
132
|
+
StateFu::Hooks::ALL_HOOKS.map do |owner, slot|
|
|
133
|
+
[ [owner, slot], send( owner ).hooks[ slot ] ]
|
|
134
|
+
end.each do |address, hooks|
|
|
135
|
+
owner,slot = *address
|
|
136
|
+
hooks.each do |hook|
|
|
137
|
+
@current_hook_slot = address
|
|
138
|
+
@current_hook = hook
|
|
139
|
+
run_hook( hook )
|
|
140
|
+
end
|
|
141
|
+
if slot == :entry
|
|
142
|
+
@accepted = true
|
|
143
|
+
@binding.persister.current_state = @target
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
# transition complete
|
|
147
|
+
@current_hook_slot = nil
|
|
148
|
+
@current_hook = nil
|
|
149
|
+
rescue TransitionHalted => e
|
|
150
|
+
@errors << e
|
|
151
|
+
end
|
|
152
|
+
return accepted?
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def halted?
|
|
156
|
+
!@errors.empty?
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def fired?
|
|
160
|
+
!!@fired
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def testing?
|
|
164
|
+
!!@test_only
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def live?
|
|
168
|
+
!testing?
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def accepted?
|
|
172
|
+
!!@accepted
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
#
|
|
176
|
+
# Try to give as many options (chances) as possible
|
|
177
|
+
#
|
|
178
|
+
|
|
179
|
+
alias_method :obj, :object
|
|
180
|
+
alias_method :instance, :object
|
|
181
|
+
alias_method :model, :object
|
|
182
|
+
alias_method :instance, :object
|
|
183
|
+
|
|
184
|
+
alias_method :destination, :target
|
|
185
|
+
alias_method :final_state, :target
|
|
186
|
+
alias_method :to, :target
|
|
187
|
+
|
|
188
|
+
alias_method :original_state, :origin
|
|
189
|
+
alias_method :initial_state, :origin
|
|
190
|
+
alias_method :from, :origin
|
|
191
|
+
|
|
192
|
+
alias_method :om, :binding
|
|
193
|
+
alias_method :stateful, :binding
|
|
194
|
+
alias_method :binding, :binding
|
|
195
|
+
alias_method :present, :binding
|
|
196
|
+
|
|
197
|
+
alias_method :workflow, :machine
|
|
198
|
+
|
|
199
|
+
alias_method :write? , :live?
|
|
200
|
+
alias_method :destructive?, :live?
|
|
201
|
+
alias_method :real?, :live?
|
|
202
|
+
alias_method :really?, :live?
|
|
203
|
+
alias_method :seriously?, :live?
|
|
204
|
+
|
|
205
|
+
alias_method :test?, :testing?
|
|
206
|
+
alias_method :test_only?, :testing?
|
|
207
|
+
alias_method :read_only?, :testing?
|
|
208
|
+
alias_method :only_pretend?, :testing?
|
|
209
|
+
alias_method :pretend?, :testing?
|
|
210
|
+
alias_method :dry_run?, :testing?
|
|
211
|
+
|
|
212
|
+
end
|
|
213
|
+
end
|
data/spec/helper.rb
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
thisdir = File.expand_path(File.dirname(__FILE__))
|
|
3
|
+
$: << thisdir << "#{thisdir}/../lib"
|
|
4
|
+
|
|
5
|
+
require 'rubygems'
|
|
6
|
+
|
|
7
|
+
{"rr" => "rr", "spec" => "rspec" }.each do |lib, gem_name|
|
|
8
|
+
begin
|
|
9
|
+
require lib
|
|
10
|
+
rescue LoadError => e
|
|
11
|
+
STDERR.puts "The '#{gem_name}' gem is required to run StateFu's specs. Please install it by running (as root):\ngem install #{gem_name}\n\n"
|
|
12
|
+
exit 1;
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
require 'state-fu'
|
|
17
|
+
require File.join( thisdir, '..' , 'lib', 'no_stdout' )
|
|
18
|
+
|
|
19
|
+
Spec::Runner.configure do |config|
|
|
20
|
+
config.mock_with :rr
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module MySpecHelper
|
|
24
|
+
include NoStdout
|
|
25
|
+
|
|
26
|
+
def prepare_active_record( options={}, &migration )
|
|
27
|
+
begin
|
|
28
|
+
require 'active_record'
|
|
29
|
+
rescue MissingSourceFile => e
|
|
30
|
+
pending "skipping specifications due to load error: #{e}"
|
|
31
|
+
return false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
options.symbolize_keys!
|
|
35
|
+
options.assert_valid_keys( :db_config, :migration_name, :hidden )
|
|
36
|
+
|
|
37
|
+
# connect ActiveRecord
|
|
38
|
+
db_config = options.delete(:db_config) || {
|
|
39
|
+
:adapter => 'sqlite3',
|
|
40
|
+
:database => ':memory:'
|
|
41
|
+
}
|
|
42
|
+
ActiveRecord::Base.establish_connection( db_config )
|
|
43
|
+
|
|
44
|
+
return unless block_given?
|
|
45
|
+
|
|
46
|
+
# prepare the migration
|
|
47
|
+
migration_class_name =
|
|
48
|
+
options.delete(:migration_name) || 'BeforeSpecMigration'
|
|
49
|
+
make_pristine_class( migration_class_name, ActiveRecord::Migration )
|
|
50
|
+
migration_class = migration_class_name.constantize
|
|
51
|
+
migration_class.class_eval( &migration )
|
|
52
|
+
|
|
53
|
+
# run the migration without spewing crap everywhere
|
|
54
|
+
if options.delete(:hidden) != false
|
|
55
|
+
no_stdout { migration_class.migrate( :up ) }
|
|
56
|
+
else
|
|
57
|
+
migration_class.migrate( :up )
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def make_pristine_class(class_name, superklass=Object, reset_first = false)
|
|
62
|
+
reset! if reset_first
|
|
63
|
+
@class_names ||= []
|
|
64
|
+
@class_names << class_name
|
|
65
|
+
klass = Class.new( superklass )
|
|
66
|
+
klass.send( :include, StateFu )
|
|
67
|
+
Object.send(:remove_const, class_name ) if Object.const_defined?( class_name )
|
|
68
|
+
Object.const_set(class_name, klass)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def reset!
|
|
72
|
+
@class_names ||= []
|
|
73
|
+
@class_names.each do |class_name|
|
|
74
|
+
Object.send(:remove_const, class_name ) if Object.const_defined?( class_name )
|
|
75
|
+
end
|
|
76
|
+
@class_names = []
|
|
77
|
+
StateFu::FuSpace.reset!
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def set_method_arity( object, method_name, needed_arity = 1 )
|
|
81
|
+
a = Object.new
|
|
82
|
+
stub( a ).arity() { needed_arity }
|
|
83
|
+
stub( object ).method(method_name) { a }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
end
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
require File.expand_path("#{File.dirname(__FILE__)}/../helper")
|
|
2
|
+
|
|
3
|
+
describe "an ActiveRecord model with StateFu included:" do
|
|
4
|
+
|
|
5
|
+
include MySpecHelper
|
|
6
|
+
|
|
7
|
+
before(:each) do
|
|
8
|
+
reset!
|
|
9
|
+
prepare_active_record() do
|
|
10
|
+
def self.up
|
|
11
|
+
create_table :example_records do |t|
|
|
12
|
+
t.string :name, :null => false
|
|
13
|
+
t.string :state_fu_field, :null => false
|
|
14
|
+
t.string :description
|
|
15
|
+
t.string :status
|
|
16
|
+
t.timestamps
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# class ExampleRecord < ActiveRecord::Base
|
|
22
|
+
make_pristine_class( 'ExampleRecord', ActiveRecord::Base )
|
|
23
|
+
ExampleRecord.class_eval do
|
|
24
|
+
validates_presence_of :name
|
|
25
|
+
end
|
|
26
|
+
# end class ExampleRecord
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "should be a subclass of ActiveRecord::Base" do
|
|
30
|
+
ExampleRecord.superclass.should == ActiveRecord::Base
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe "when the default machine is defined with no field_name specified" do
|
|
34
|
+
before do
|
|
35
|
+
ExampleRecord.class_eval do
|
|
36
|
+
machine do
|
|
37
|
+
state :initial do
|
|
38
|
+
event( :change, :to => :final ) { after :save! }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
@ex = ExampleRecord.new( :name => "exemplar" )
|
|
43
|
+
end # before
|
|
44
|
+
|
|
45
|
+
it "should have an active_record string column 'state_fu_field' " do
|
|
46
|
+
col = ExampleRecord.columns.detect {|c| c.name == "state_fu_field" }
|
|
47
|
+
col.type.should == :string
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe "calling :save! via an event's after hook" do
|
|
51
|
+
it "should save the record with the new state persisted via the DB" do
|
|
52
|
+
@ex.change!
|
|
53
|
+
@ex.state_fu.name.should == :final
|
|
54
|
+
@ex.state_fu_field.should == 'final'
|
|
55
|
+
@ex.reload
|
|
56
|
+
@ex.state_fu_field.should == 'final'
|
|
57
|
+
@ex.state_fu.name.should == :final
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
describe "StateFu::Persistence.active_record_column?" do
|
|
62
|
+
it "should return true for ExampleRecord, :state_fu_field" do
|
|
63
|
+
StateFu::Persistence.active_record_column?( ExampleRecord, :state_fu_field ).should == true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "should return true for ExampleRecord, :status" do
|
|
67
|
+
StateFu::Persistence.active_record_column?( ExampleRecord, :status ).should == true
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "should return false for ExampleRecord, :not_a_column" do
|
|
71
|
+
StateFu::Persistence.active_record_column?( ExampleRecord, :not_a_column ).should == false
|
|
72
|
+
end
|
|
73
|
+
it "should not clobber activerecord accessors" do
|
|
74
|
+
@ex.noodle! rescue nil
|
|
75
|
+
# lambda { @ex.description }.should_not raise_error()
|
|
76
|
+
@ex.description.should be_nil
|
|
77
|
+
@ex.description= 'foo'
|
|
78
|
+
@ex.description.should == 'foo'
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "should have an active_record string column 'state_fu_field' " do
|
|
82
|
+
col = ExampleRecord.columns.detect {|c| c.name == "state_fu_field" }
|
|
83
|
+
col.type.should == :string
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "should have an active_record persister with the default field_name 'state_fu_field' " do
|
|
88
|
+
@ex.state_fu
|
|
89
|
+
@ex.state_fu.should be_kind_of( StateFu::Binding )
|
|
90
|
+
@ex.state_fu.persister.should be_kind_of( StateFu::Persistence::ActiveRecord )
|
|
91
|
+
@ex.state_fu.persister.field_name.should == :state_fu_field
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# this ensures state_fu initializes the field before create to
|
|
96
|
+
# satisfy the not null constraint
|
|
97
|
+
describe "automagic state_fu! before_save filter and validations" do
|
|
98
|
+
|
|
99
|
+
it "should call state_fu! before a record is created" do
|
|
100
|
+
@ex.should be_new_record
|
|
101
|
+
mock.proxy( @ex ).state_fu!.at_least( 1 ) { }
|
|
102
|
+
@ex.save!
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "should call state_fu! before a record is updated" do
|
|
106
|
+
@ex.should be_new_record
|
|
107
|
+
mock.proxy( @ex ).state_fu!.at_least( 1 ) { }
|
|
108
|
+
@ex.save!
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "should fail to save if state_fu! does not instantiate the binding before create" do
|
|
112
|
+
pending "is this still relevant?"
|
|
113
|
+
mock( @ex ).state_fu!.at_least( 1 ) { }
|
|
114
|
+
lambda { @ex.save! }.should raise_error( ActiveRecord::StatementInvalid )
|
|
115
|
+
@ex.state_fu_field.should == nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "should create a record given only a name, with the field set to the initial state" do
|
|
119
|
+
ex = ExampleRecord.new( :name => "exemplar" )
|
|
120
|
+
ex.should be_valid
|
|
121
|
+
ex.state_fu_field.should == nil
|
|
122
|
+
ex.save!
|
|
123
|
+
ex.should_not be_new_record
|
|
124
|
+
ex.state_fu_field.should == 'initial'
|
|
125
|
+
ex.state_fu.state.name.should == :initial
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "should update the field after a transition is completed" do
|
|
129
|
+
ex = ExampleRecord.create!( :name => "exemplar" )
|
|
130
|
+
ex.state_fu.state.name.should == :initial
|
|
131
|
+
ex.state_fu_field.should == 'initial'
|
|
132
|
+
t = ex.state_fu.fire!( :change )
|
|
133
|
+
t.should be_accepted
|
|
134
|
+
ex.state_fu.state.name.should == :final
|
|
135
|
+
ex.state_fu_field.should == 'final'
|
|
136
|
+
ex.attributes['state_fu_field'].should == 'final'
|
|
137
|
+
ex.save!
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
describe "a saved record whose state is not the default" do
|
|
141
|
+
before do
|
|
142
|
+
@r = ExampleRecord.create!( :name => "exemplar" )
|
|
143
|
+
@r.change!
|
|
144
|
+
@r.state_fu_field.should == 'final'
|
|
145
|
+
@r.save!
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "should be reconstituted with the correct state" do
|
|
149
|
+
r = ExampleRecord.find( @r.id )
|
|
150
|
+
r.state_fu.should be_kind_of( StateFu::Binding )
|
|
151
|
+
r.state_fu.current_state.should be_kind_of( StateFu::State )
|
|
152
|
+
r.state_fu.current_state.should == ExampleRecord.machine.states[:final]
|
|
153
|
+
end
|
|
154
|
+
end # saved record after transition
|
|
155
|
+
|
|
156
|
+
describe "when a second machine named :status is defined with :field_name => 'status' " do
|
|
157
|
+
before do
|
|
158
|
+
ExampleRecord.machine(:status, :field_name => 'status') do
|
|
159
|
+
event( :go, :from => :initial, :to => :final )
|
|
160
|
+
end
|
|
161
|
+
@ex = ExampleRecord.new()
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it "should have a binding for .status" do
|
|
165
|
+
@ex.status.should be_kind_of( StateFu::Binding )
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it "should have an ActiveRecord persister with the field_name :status" do
|
|
169
|
+
@ex.status.persister.should be_kind_of( StateFu::Persistence::ActiveRecord )
|
|
170
|
+
@ex.status.persister.field_name.should == :status
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it "should have a value of nil for the status field before state_fu is called" do
|
|
174
|
+
@ex.read_attribute('status').should be_nil
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "should have the ActiveRecord setter method .status=" do
|
|
178
|
+
@ex.status= 'damp'
|
|
179
|
+
@ex.read_attribute(:status).should == 'damp'
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it "should raise StateFu::InvalidState if the status field is set to a bad value and .status is called" do
|
|
183
|
+
@ex.status= 'damp'
|
|
184
|
+
lambda { @ex.status }.should raise_error( StateFu::InvalidStateName )
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end # second machine
|
|
188
|
+
end # with before_create filter
|
|
189
|
+
end # default machine
|