state-fu 0.11.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 +293 -0
- data/Rakefile +114 -0
- data/lib/binding.rb +292 -0
- data/lib/event.rb +192 -0
- data/lib/executioner.rb +120 -0
- data/lib/hooks.rb +39 -0
- data/lib/interface.rb +132 -0
- data/lib/lathe.rb +538 -0
- data/lib/machine.rb +184 -0
- data/lib/method_factory.rb +243 -0
- data/lib/persistence.rb +116 -0
- data/lib/persistence/active_record.rb +34 -0
- data/lib/persistence/attribute.rb +47 -0
- data/lib/persistence/base.rb +100 -0
- data/lib/persistence/relaxdb.rb +23 -0
- data/lib/persistence/session.rb +7 -0
- data/lib/sprocket.rb +58 -0
- data/lib/state-fu.rb +56 -0
- data/lib/state.rb +48 -0
- data/lib/support/active_support_lite/array.rb +9 -0
- data/lib/support/active_support_lite/array/access.rb +60 -0
- data/lib/support/active_support_lite/array/conversions.rb +202 -0
- data/lib/support/active_support_lite/array/extract_options.rb +21 -0
- data/lib/support/active_support_lite/array/grouping.rb +109 -0
- data/lib/support/active_support_lite/array/random_access.rb +13 -0
- data/lib/support/active_support_lite/array/wrapper.rb +25 -0
- data/lib/support/active_support_lite/blank.rb +67 -0
- data/lib/support/active_support_lite/cattr_reader.rb +57 -0
- data/lib/support/active_support_lite/keys.rb +57 -0
- data/lib/support/active_support_lite/misc.rb +59 -0
- data/lib/support/active_support_lite/module.rb +1 -0
- data/lib/support/active_support_lite/module/delegation.rb +130 -0
- data/lib/support/active_support_lite/object.rb +9 -0
- data/lib/support/active_support_lite/string.rb +38 -0
- data/lib/support/active_support_lite/symbol.rb +16 -0
- data/lib/support/applicable.rb +41 -0
- data/lib/support/arrays.rb +197 -0
- data/lib/support/core_ext.rb +90 -0
- data/lib/support/exceptions.rb +106 -0
- data/lib/support/has_options.rb +16 -0
- data/lib/support/logger.rb +165 -0
- data/lib/support/methodical.rb +17 -0
- data/lib/support/no_stdout.rb +55 -0
- data/lib/support/plotter.rb +62 -0
- data/lib/support/vizier.rb +300 -0
- data/lib/tasks/spec_last.rake +55 -0
- data/lib/tasks/state_fu.rake +57 -0
- data/lib/transition.rb +338 -0
- data/lib/transition_query.rb +224 -0
- data/spec/custom_formatter.rb +49 -0
- data/spec/features/binding_and_transition_helper_mixin_spec.rb +111 -0
- data/spec/features/method_missing_only_once_spec.rb +28 -0
- data/spec/features/not_requirements_spec.rb +118 -0
- data/spec/features/plotter_spec.rb +97 -0
- data/spec/features/shared_log_spec.rb +7 -0
- data/spec/features/singleton_machine_spec.rb +39 -0
- data/spec/features/state_and_array_options_accessor_spec.rb +47 -0
- data/spec/features/transition_boolean_comparison_spec.rb +101 -0
- data/spec/helper.rb +13 -0
- data/spec/integration/active_record_persistence_spec.rb +202 -0
- data/spec/integration/binding_extension_spec.rb +41 -0
- data/spec/integration/class_accessor_spec.rb +117 -0
- data/spec/integration/event_definition_spec.rb +74 -0
- data/spec/integration/example_01_document_spec.rb +133 -0
- data/spec/integration/example_02_string_spec.rb +88 -0
- data/spec/integration/instance_accessor_spec.rb +97 -0
- data/spec/integration/lathe_extension_spec.rb +67 -0
- data/spec/integration/machine_duplication_spec.rb +101 -0
- data/spec/integration/relaxdb_persistence_spec.rb +97 -0
- data/spec/integration/requirement_reflection_spec.rb +270 -0
- data/spec/integration/state_definition_spec.rb +163 -0
- data/spec/integration/transition_spec.rb +1033 -0
- data/spec/spec.opts +9 -0
- data/spec/spec_helper.rb +132 -0
- data/spec/state_fu_spec.rb +948 -0
- data/spec/units/binding_spec.rb +192 -0
- data/spec/units/event_spec.rb +214 -0
- data/spec/units/exceptions_spec.rb +82 -0
- data/spec/units/lathe_spec.rb +570 -0
- data/spec/units/machine_spec.rb +229 -0
- data/spec/units/method_factory_spec.rb +366 -0
- data/spec/units/sprocket_spec.rb +69 -0
- data/spec/units/state_spec.rb +59 -0
- metadata +171 -0
data/lib/machine.rb
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
module StateFu
|
2
|
+
class Machine
|
3
|
+
|
4
|
+
def self.BINDINGS
|
5
|
+
@@_bindings ||= {}
|
6
|
+
end
|
7
|
+
|
8
|
+
include Applicable
|
9
|
+
include HasOptions
|
10
|
+
|
11
|
+
attr_reader :hooks
|
12
|
+
|
13
|
+
#
|
14
|
+
# Class methods
|
15
|
+
#
|
16
|
+
|
17
|
+
def self.for_class(klass, name, options={}, &block)
|
18
|
+
options.symbolize_keys!
|
19
|
+
name = name.to_sym
|
20
|
+
|
21
|
+
unless machine = klass.state_fu_machines[ name ]
|
22
|
+
machine = new(options)
|
23
|
+
machine.bind! klass, name, options[:field_name]
|
24
|
+
end
|
25
|
+
if block_given?
|
26
|
+
machine.apply! &block
|
27
|
+
end
|
28
|
+
machine
|
29
|
+
end
|
30
|
+
|
31
|
+
# make it so that a class which has included StateFu has a binding to
|
32
|
+
# this machine
|
33
|
+
def self.bind!( machine, owner, name, field_name)
|
34
|
+
name = name.to_sym
|
35
|
+
# define an accessor method with the given name
|
36
|
+
if owner.class == Class
|
37
|
+
owner.state_fu_machines[name] = machine
|
38
|
+
owner.state_fu_field_names[name] = field_name
|
39
|
+
# method_missing to catch NoMethodError for event methods, etc
|
40
|
+
StateFu::MethodFactory.define_once_only_method_missing( owner )
|
41
|
+
unless owner.respond_to?(name)
|
42
|
+
owner.class_eval do
|
43
|
+
define_method name do
|
44
|
+
state_fu( name )
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
# prepare the persistence field
|
49
|
+
StateFu::Persistence.prepare_field owner, field_name
|
50
|
+
else
|
51
|
+
_binding = StateFu::Binding.new machine, owner, name, :field_name => field_name, :singleton => true
|
52
|
+
MethodFactory.define_singleton_method(owner, name) { _binding }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
## Instance Methods
|
58
|
+
##
|
59
|
+
|
60
|
+
attr_reader :states, :events, :options, :helpers, :named_procs, :requirement_messages, :tools
|
61
|
+
|
62
|
+
def initialize( options={}, &block )
|
63
|
+
@states = [].extend( StateArray )
|
64
|
+
@events = [].extend( EventArray )
|
65
|
+
@helpers = [].extend( HelperArray )
|
66
|
+
@tools = [].extend( ToolArray )
|
67
|
+
@named_procs = {}
|
68
|
+
@requirement_messages = {}
|
69
|
+
@options = options
|
70
|
+
@hooks = Hooks.for( self )
|
71
|
+
apply!( &block ) if block_given?
|
72
|
+
end
|
73
|
+
|
74
|
+
# merge the commands in &block with the existing machine; returns
|
75
|
+
# a lathe for the machine.
|
76
|
+
def apply!( &block )
|
77
|
+
StateFu::Lathe.new( self, &block )
|
78
|
+
end
|
79
|
+
alias_method :lathe, :apply!
|
80
|
+
|
81
|
+
def helper_modules
|
82
|
+
helpers.modules
|
83
|
+
end
|
84
|
+
|
85
|
+
def inject_helpers_into( obj )
|
86
|
+
helpers.inject_into( obj )
|
87
|
+
end
|
88
|
+
|
89
|
+
def inject_tools_into( obj )
|
90
|
+
tools.inject_into( obj )
|
91
|
+
end
|
92
|
+
|
93
|
+
def inject_methods_into( obj )
|
94
|
+
#puts 'inject_methods_into'
|
95
|
+
end
|
96
|
+
|
97
|
+
# the modules listed here will be mixed into Binding and
|
98
|
+
# Transition objects for this machine. use this to define methods,
|
99
|
+
# references or data useful to you during transitions, event
|
100
|
+
# hooks, or in general use of StateFu.
|
101
|
+
#
|
102
|
+
# They can be supplied as a string/symbol (as per rails controller
|
103
|
+
# helpers), or a Module.
|
104
|
+
#
|
105
|
+
# To do this globally, just duck-punch StateFu::Machine /
|
106
|
+
# StateFu::Binding.
|
107
|
+
def helper *modules_to_add
|
108
|
+
modules_to_add.each { |mod| helpers << mod }
|
109
|
+
end
|
110
|
+
|
111
|
+
# same as helper, but for extending Lathes rather than the Bindings / Transitions.
|
112
|
+
# use this to extend the Lathe DSL to suit your problem domain.
|
113
|
+
def tool *modules_to_add
|
114
|
+
modules_to_add.each { |mod| tools << mod }
|
115
|
+
end
|
116
|
+
|
117
|
+
# make it so a class which has included StateFu has a binding to
|
118
|
+
# this machine
|
119
|
+
def bind!( owner, name= DEFAULT, field_name = nil )
|
120
|
+
field_name ||= Persistence.default_field_name( name )
|
121
|
+
self.class.bind!(self, owner, name, field_name)
|
122
|
+
end
|
123
|
+
|
124
|
+
def empty?
|
125
|
+
states.empty?
|
126
|
+
end
|
127
|
+
|
128
|
+
def initial_state=( s )
|
129
|
+
case s
|
130
|
+
when Symbol, String, StateFu::State
|
131
|
+
unless init_state = states[ s.to_sym ]
|
132
|
+
init_state = StateFu::State.new( self, s.to_sym )
|
133
|
+
states << init_state
|
134
|
+
end
|
135
|
+
@initial_state = init_state
|
136
|
+
else
|
137
|
+
raise( ArgumentError, s.inspect )
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def initial_state()
|
142
|
+
@initial_state ||= states.first
|
143
|
+
end
|
144
|
+
|
145
|
+
def state_names
|
146
|
+
states.map(&:name)
|
147
|
+
end
|
148
|
+
|
149
|
+
def event_names
|
150
|
+
events.map(&:name)
|
151
|
+
end
|
152
|
+
|
153
|
+
# given a messy bunch of symbols, find or create a list of
|
154
|
+
# matching States.
|
155
|
+
def find_or_create_states_by_name( *args )
|
156
|
+
args = args.compact.flatten
|
157
|
+
raise ArgumentError.new( args.inspect ) unless args.all? { |a| [Symbol, StateFu::State].include? a.class }
|
158
|
+
args.map do |s|
|
159
|
+
unless state = states[s.to_sym]
|
160
|
+
# TODO clean this line up
|
161
|
+
state = s.is_a?( StateFu::State ) ? s : StateFu::State.new( self, s )
|
162
|
+
self.states << state
|
163
|
+
end
|
164
|
+
state
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Marshal, the poor man's X-Ray photocopier.
|
169
|
+
# TODO: a version which will not break its teeth on procs
|
170
|
+
def deep_clone
|
171
|
+
Marshal::load(Marshal.dump(self))
|
172
|
+
end
|
173
|
+
alias_method :deep_copy, :deep_clone
|
174
|
+
|
175
|
+
def inspect
|
176
|
+
"#<#{self.class} ##{__id__} states=#{state_names.inspect} events=#{event_names.inspect} options=#{options.inspect}>"
|
177
|
+
end
|
178
|
+
|
179
|
+
def graphviz
|
180
|
+
@graphviz ||= Plotter.new(self).output
|
181
|
+
end
|
182
|
+
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,243 @@
|
|
1
|
+
module StateFu
|
2
|
+
# This class is responsible for defining methods at runtime.
|
3
|
+
#
|
4
|
+
# TODO: all events, simple or complex, should get the same method signature
|
5
|
+
# simple events will be called as: event_name! nil, *args
|
6
|
+
# complex events will be called as: event_name! :state, *args
|
7
|
+
|
8
|
+
class MethodFactory
|
9
|
+
attr_accessor :method_definitions
|
10
|
+
attr_reader :binding
|
11
|
+
|
12
|
+
# An instance of MethodFactory is created to define methods on a specific StateFu::Binding, and
|
13
|
+
# on the object it is bound to.
|
14
|
+
|
15
|
+
def initialize( _binding )
|
16
|
+
@binding = _binding
|
17
|
+
simple_events, complex_events = @binding.machine.events.partition(&:simple?)
|
18
|
+
@method_definitions = {}
|
19
|
+
|
20
|
+
# simple event methods
|
21
|
+
# all arguments are passed into the transition / transition query
|
22
|
+
|
23
|
+
simple_events.each do |event|
|
24
|
+
method_definitions["#{event.name}"] = lambda do |*args|
|
25
|
+
_binding.find_transition(event, event.target, *args)
|
26
|
+
end
|
27
|
+
|
28
|
+
method_definitions["can_#{event.name}?"] = lambda do |*args|
|
29
|
+
_binding.can_transition?(event, event.target, *args)
|
30
|
+
end
|
31
|
+
|
32
|
+
method_definitions["#{event.name}!"] = lambda do |*args|
|
33
|
+
_binding.fire_transition!(event, event.target, *args)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# complex event methods
|
38
|
+
# the first argument is the target state
|
39
|
+
# any remaining arguments are passed into the transition / transition query
|
40
|
+
|
41
|
+
# object.event_name [:target], *arguments
|
42
|
+
#
|
43
|
+
# returns a new transition. Will raise an IllegalTransition if
|
44
|
+
# it is not given arguments which result in a valid combination
|
45
|
+
# of event and target state being deducted.
|
46
|
+
#
|
47
|
+
# object.event_name [nil] suffices if the event has only one valid
|
48
|
+
# target (ie only one transition which would not raise a
|
49
|
+
# RequirementError if fired)
|
50
|
+
|
51
|
+
# object.event_name! [:target], *arguments
|
52
|
+
#
|
53
|
+
# as per the method above, except that it also fires the event
|
54
|
+
|
55
|
+
# object.can_event_name? [:target], *arguments
|
56
|
+
#
|
57
|
+
# tests that calling event_name or event_name! would not raise an error
|
58
|
+
# ie, the transition is legal and is valid with the arguments supplied
|
59
|
+
|
60
|
+
complex_events.each do |event|
|
61
|
+
method_definitions["#{event.name}"] = lambda do |target, *args|
|
62
|
+
_binding.find_transition(event, target, *args)
|
63
|
+
end
|
64
|
+
|
65
|
+
method_definitions["can_#{event.name}?"] = lambda do |target, *args|
|
66
|
+
begin
|
67
|
+
t = _binding.find_transition(event, target, *args)
|
68
|
+
t.valid?
|
69
|
+
rescue IllegalTransition
|
70
|
+
false
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
method_definitions["#{event.name}!"] = lambda do |target, *args|
|
75
|
+
_binding.fire_transition!(event, target, *args)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# methods dedicated to a combination of event and target
|
80
|
+
# all arguments are passed into the transition / transition query
|
81
|
+
|
82
|
+
(simple_events + complex_events).each do |event|
|
83
|
+
event.targets.each do |target|
|
84
|
+
method_definitions["#{event.name}_to_#{target.name}"] = lambda do |*args|
|
85
|
+
_binding.find_transition(event, target, *args)
|
86
|
+
end
|
87
|
+
|
88
|
+
method_definitions["can_#{event.name}_to_#{target.name}?"] = lambda do |*args|
|
89
|
+
_binding.can_transition?(event, target, *args)
|
90
|
+
end
|
91
|
+
|
92
|
+
method_definitions["#{event.name}_to_#{target.name}!"] = lambda do |*args|
|
93
|
+
_binding.fire_transition!(event, target, *args)
|
94
|
+
end
|
95
|
+
end unless event.targets.nil?
|
96
|
+
end
|
97
|
+
|
98
|
+
@binding.machine.states.each do |state|
|
99
|
+
method_definitions["#{state.name}?"] = lambda do
|
100
|
+
_binding.current_state == state
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
#
|
108
|
+
# Class Methods
|
109
|
+
#
|
110
|
+
|
111
|
+
# This should be called once per class using StateFu. It aliases and redefines
|
112
|
+
# method_missing for the class.
|
113
|
+
#
|
114
|
+
# Note this happens when a machine is first bound to the class,
|
115
|
+
# not when StateFu is included.
|
116
|
+
|
117
|
+
def self.prepare_class( klass )
|
118
|
+
raise caller.inspect
|
119
|
+
self.define_once_only_method_missing( klass )
|
120
|
+
end # prepare_class
|
121
|
+
|
122
|
+
# When triggered, method_missing will first call state_fu!,
|
123
|
+
# instantating all bindings & installing their attendant
|
124
|
+
# MethodFactories, then check if the object now responds to the
|
125
|
+
# missing method name; otherwise it will call the original
|
126
|
+
# method_missing.
|
127
|
+
#
|
128
|
+
# method_missing will then revert to its original implementation.
|
129
|
+
#
|
130
|
+
# The purpose of all this is to allow dynamically created methods
|
131
|
+
# to be called, without worrying about whether they have been
|
132
|
+
# defined yet, and without incurring the expense of loading all
|
133
|
+
# the object's StateFu::Bindings before they're likely to be needed.
|
134
|
+
#
|
135
|
+
# Note that if you redefine method_missing on your StateFul
|
136
|
+
# classes, it's best to either do it before you include StateFu,
|
137
|
+
# or thoroughly understand what's happening in
|
138
|
+
# MethodFactory#define_once_only_method_missing.
|
139
|
+
|
140
|
+
def self.define_once_only_method_missing( klass )
|
141
|
+
raise ArgumentError.new(klass.to_s) unless klass.is_a?(Class)
|
142
|
+
|
143
|
+
klass.class_eval do
|
144
|
+
return false if @_state_fu_prepared
|
145
|
+
@_state_fu_prepared = true
|
146
|
+
|
147
|
+
alias_method(:method_missing_before_state_fu, :method_missing) # if defined?(:method_missing, true)
|
148
|
+
|
149
|
+
def method_missing(method_name, *args, &block)
|
150
|
+
# invoke state_fu! to ensure event, etc methods are defined
|
151
|
+
begin
|
152
|
+
state_fu! unless defined? initialize_state_fu!
|
153
|
+
rescue NoMethodError => e
|
154
|
+
raise e
|
155
|
+
end
|
156
|
+
|
157
|
+
# reset method_missing for this instance
|
158
|
+
class << self; self; end.class_eval do
|
159
|
+
alias_method :method_missing, :method_missing_before_state_fu
|
160
|
+
end
|
161
|
+
|
162
|
+
# call the newly defined method, or the original method_missing
|
163
|
+
if respond_to?(method_name, true)
|
164
|
+
# it was defined by calling state_fu!, which instantiated bindings
|
165
|
+
# for its state machines, which defined singleton methods for its
|
166
|
+
# states & events when it was constructed.
|
167
|
+
__send__( method_name, *args, &block )
|
168
|
+
else
|
169
|
+
# call the original method_missing (method_missing_before_state_fu)
|
170
|
+
method_missing( method_name, *args, &block )
|
171
|
+
end
|
172
|
+
end # method_missing
|
173
|
+
end # class_eval
|
174
|
+
end # define_once_only_method_missing
|
175
|
+
|
176
|
+
# Define the same helper methods on the StateFu::Binding and its
|
177
|
+
# object. Any existing methods will not be tampered with, but a
|
178
|
+
# warning will be issued in the logs if any methods cannot be defined.
|
179
|
+
def install!
|
180
|
+
define_event_methods_on( @binding )
|
181
|
+
define_event_methods_on( @binding.object )
|
182
|
+
end
|
183
|
+
|
184
|
+
#
|
185
|
+
# For each event, on the given object, define three methods.
|
186
|
+
# - The first method is the same as the event name.
|
187
|
+
# Returns a new, unfired transition object.
|
188
|
+
# - The second method has a "?" suffix.
|
189
|
+
# Returns true if the event can be fired.
|
190
|
+
# - The third method has a "!" suffix.
|
191
|
+
# Creates a new Transition, fires and returns it once complete.
|
192
|
+
#
|
193
|
+
# The arguments expected depend on whether the event is "simple" - ie,
|
194
|
+
# has only one possible target state.
|
195
|
+
#
|
196
|
+
# All simple event methods pass their entire argument list
|
197
|
+
# directly to transition. These arguments can be accessed inside
|
198
|
+
# event hooks, requirements, etc by calling Transition#args.
|
199
|
+
#
|
200
|
+
# All complex event methods require their first argument to be a
|
201
|
+
# Symbol containing a valid target State's name, or the State
|
202
|
+
# itself. The remaining arguments are passed into the transition,
|
203
|
+
# as with simple event methods.
|
204
|
+
#
|
205
|
+
def define_event_methods_on( obj )
|
206
|
+
method_definitions.each do |method_name, method_body|
|
207
|
+
define_singleton_method( obj, method_name, &method_body)
|
208
|
+
end
|
209
|
+
end # define_event_methods_on
|
210
|
+
|
211
|
+
def define_singleton_method( object, method_name, &block )
|
212
|
+
MethodFactory.define_singleton_method object, method_name, &block
|
213
|
+
end
|
214
|
+
|
215
|
+
# define a a method on the metaclass of the given object. The
|
216
|
+
# resulting "singleton method" will be unique to that instance,
|
217
|
+
# not shared by other instances of its class.
|
218
|
+
#
|
219
|
+
# This allows us to embed a reference to the instance's unique
|
220
|
+
# binding in the new method.
|
221
|
+
#
|
222
|
+
# existing methods will never be overwritten.
|
223
|
+
|
224
|
+
def self.define_singleton_method( object, method_name, options={}, &block )
|
225
|
+
if object.respond_to?(method_name, true)
|
226
|
+
msg = !options[:force]
|
227
|
+
Logger.info "Existing method #{method(method_name) rescue [method_name].inspect} "\
|
228
|
+
"for #{object.class} #{object} "\
|
229
|
+
"#{options[:force] ? 'WILL' : 'won\'t'} "\
|
230
|
+
"be overwritten."
|
231
|
+
else
|
232
|
+
metaclass = class << object; self; end
|
233
|
+
metaclass.class_eval do
|
234
|
+
define_method( method_name, &block )
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
alias_method :define_singleton_method, :define_singleton_method
|
239
|
+
|
240
|
+
end # class MethodFactory
|
241
|
+
end # module StateFu
|
242
|
+
|
243
|
+
|
data/lib/persistence.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
module StateFu
|
2
|
+
|
3
|
+
# the persistence module has a few simple tests which help decide which
|
4
|
+
# persistence mechanism to use
|
5
|
+
|
6
|
+
# TODO add event hooks (on_change etc) ...
|
7
|
+
# after benchmarking
|
8
|
+
|
9
|
+
# To create your own custom persistence mechanism,
|
10
|
+
# subclass StateFu::Persistence::Base
|
11
|
+
# and define prepare_field, read_attribute and write_attribute:
|
12
|
+
|
13
|
+
|
14
|
+
# class StateFu::Persistence::MagneticCarpet < StateFu::Persistence::Base
|
15
|
+
# def prepare_field
|
16
|
+
#
|
17
|
+
#
|
18
|
+
# def read_attribute
|
19
|
+
# object.send "magnetised_#{field_name}"
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# def write_attribute( string_value )
|
23
|
+
# Logger.debug "magnetising ( #{field_name} => #{string_value} on #{object.inspect}"
|
24
|
+
# object.send "magnetised_#{field_name}=", string_value
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
|
28
|
+
module Persistence
|
29
|
+
DEFAULT_SUFFIX = '_field'
|
30
|
+
@@class_for = {}
|
31
|
+
@@fields_prepared = {}
|
32
|
+
|
33
|
+
#
|
34
|
+
# Class Methods
|
35
|
+
#
|
36
|
+
|
37
|
+
def self.default_field_name( machine_name )
|
38
|
+
machine_name == DEFAULT ? DEFAULT_FIELD : "#{machine_name.to_s.underscore.tr(' ','_')}#{DEFAULT_SUFFIX}"
|
39
|
+
end
|
40
|
+
|
41
|
+
# returns the appropriate persister class for the given class & field name.
|
42
|
+
def self.class_for( klass, field_name )
|
43
|
+
raise ArgumentError if [klass, field_name].any?(&:nil?)
|
44
|
+
@@class_for[klass] ||= {}
|
45
|
+
@@class_for[klass][field_name] ||=
|
46
|
+
if active_record_column?( klass, field_name )
|
47
|
+
self::ActiveRecord
|
48
|
+
elsif relaxdb_document_property?( klass, field_name )
|
49
|
+
self::RelaxDB
|
50
|
+
else
|
51
|
+
self::Attribute
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.for_class( klass, binding, field_name )
|
56
|
+
persister_class = class_for klass, field_name
|
57
|
+
prepare_field( klass, field_name, persister_class)
|
58
|
+
returning persister_class.new( binding, field_name ) do |persister|
|
59
|
+
Logger.debug( "#{persister_class}: method #{binding.method_name} as field #{persister.field_name}" )
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.for_instance( binding, field_name )
|
64
|
+
metaclass = class << binding.object; self; end
|
65
|
+
for_class( metaclass, binding, field_name )
|
66
|
+
end
|
67
|
+
|
68
|
+
# returns a new persister appropriate to the given binding and field_name
|
69
|
+
# also ensures the persister class method :prepare_field has been called
|
70
|
+
# once for the given class & field name so the field can be set up; eg an
|
71
|
+
# attr_accessor or a before_save hook defined
|
72
|
+
def self.for( binding )
|
73
|
+
field_name = binding.field_name.to_sym
|
74
|
+
if binding.singleton?
|
75
|
+
for_instance( binding, field_name )
|
76
|
+
else
|
77
|
+
for_class( binding.target, binding, field_name )
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# ensures that <persister_class>.prepare_field is called only once
|
82
|
+
def self.prepare_field(klass, field_name, persister_class=nil)
|
83
|
+
@@fields_prepared[klass] ||= []
|
84
|
+
unless @@fields_prepared[klass].include?(field_name)
|
85
|
+
persister_class ||= class_for(klass, field_name)
|
86
|
+
persister_class.prepare_field( klass, field_name )
|
87
|
+
@@fields_prepared[klass] << field_name
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
#
|
92
|
+
# Heuristics - simple test methods to determine which persister to use
|
93
|
+
#
|
94
|
+
|
95
|
+
# checks to see if the field_name for persistence is a
|
96
|
+
# RelaxDB attribute.
|
97
|
+
# Safe to use (skipped) if RelaxDB is not included.
|
98
|
+
def self.relaxdb_document_property?( klass, field_name )
|
99
|
+
Object.const_defined?('RelaxDB') &&
|
100
|
+
klass.ancestors.include?( ::RelaxDB::Document ) &&
|
101
|
+
klass.properties.map(&:to_s).include?( field_name.to_s )
|
102
|
+
end
|
103
|
+
|
104
|
+
# checks to see if the field_name for persistence is an
|
105
|
+
# ActiveRecord column.
|
106
|
+
# Safe to use (skipped) if ActiveRecord is not included.
|
107
|
+
def self.active_record_column?( klass, field_name )
|
108
|
+
Object.const_defined?("ActiveRecord") &&
|
109
|
+
::ActiveRecord.const_defined?("Base") &&
|
110
|
+
klass.ancestors.include?( ::ActiveRecord::Base ) &&
|
111
|
+
klass.table_exists? &&
|
112
|
+
klass.columns.map(&:name).include?( field_name.to_s )
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|