maintain 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 +1 -0
- data/README.markdown +168 -0
- data/Rakefile +38 -0
- data/VERSION +1 -0
- data/lib/maintain/bitmask_value.rb +43 -0
- data/lib/maintain/integer_value.rb +7 -0
- data/lib/maintain/maintainer.rb +171 -0
- data/lib/maintain/value.rb +105 -0
- data/lib/maintain.rb +119 -0
- data/spec/active_record_spec.rb +67 -0
- data/spec/aggregates_spec.rb +35 -0
- data/spec/bitwise_spec.rb +83 -0
- data/spec/comparing_state_spec.rb +194 -0
- data/spec/defining_states_spec.rb +79 -0
- data/spec/hooks_spec.rb +44 -0
- data/spec/integer_spec.rb +22 -0
- data/spec/maintain_spec.rb +45 -0
- data/spec/object_spec.rb +7 -0
- data/spec/proxy_spec.rb +124 -0
- data/spec/setting_state_spec.rb +56 -0
- metadata +84 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
coverage/
|
data/README.markdown
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
Maintain
|
2
|
+
===
|
3
|
+
|
4
|
+
**Maintain** is a simple state machine mixin for Ruby objects. It supports comparisons, bitmasks,
|
5
|
+
and hooks that really work. It can be used for multiple attributes and will always do its best to
|
6
|
+
stay out of your way and let your code drive the machine, and not vice versa.
|
7
|
+
|
8
|
+
Installation
|
9
|
+
-
|
10
|
+
|
11
|
+
**Maintain** is provided as a Gem. It's pretty basic, really:
|
12
|
+
|
13
|
+
1. Install it with `gem install maintain`
|
14
|
+
2. Require it with `require "maintain"`
|
15
|
+
|
16
|
+
Basic Usage
|
17
|
+
-
|
18
|
+
|
19
|
+
**Maintain** is pretty straightforward to use. First, you have to tell a Ruby object to maintain
|
20
|
+
state on an attribute:
|
21
|
+
|
22
|
+
class Foo
|
23
|
+
extend Maintain
|
24
|
+
maintains :state do
|
25
|
+
state :new, :default => true
|
26
|
+
state :old
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
That's it for basic state maintenance! Check it out:
|
31
|
+
|
32
|
+
foo = Foo.new
|
33
|
+
foo.state #=> :new
|
34
|
+
foo.new? #=> true
|
35
|
+
foo.state = :old
|
36
|
+
foo.old? #=> true
|
37
|
+
|
38
|
+
But wait! What if you've already defined "new?" on the Foo class? Not to worry, Maintain won't step on your toes. Just use:
|
39
|
+
|
40
|
+
foo.state.new?
|
41
|
+
|
42
|
+
Comparisons
|
43
|
+
-
|
44
|
+
|
45
|
+
**Maintain** provides quick and easy comparisons between states. You can specify integer values of states to compare on,
|
46
|
+
or you can just let it infer what it wants. From our example above:
|
47
|
+
|
48
|
+
foo.state = :new
|
49
|
+
foo.state > :old #=> false
|
50
|
+
foo.state <= :old #=> true
|
51
|
+
|
52
|
+
You could also do:
|
53
|
+
|
54
|
+
class Foo
|
55
|
+
extend Maintain
|
56
|
+
maintains :state do
|
57
|
+
state :new, 12, :default => true
|
58
|
+
state :old, 5
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
Foo.new.state > old #=> true
|
63
|
+
|
64
|
+
Bitmasking
|
65
|
+
-
|
66
|
+
|
67
|
+
Sometimes you need to store a simple combination of values. Sure, you could add individual columns for each value to your
|
68
|
+
relational database - or you could implement a single bitmask column:
|
69
|
+
|
70
|
+
class Foo
|
71
|
+
extend Maintain
|
72
|
+
maintains :state, :bitmask => true do
|
73
|
+
# NOTE: Maintain will try to infer a bitmask value if you do not provid an integer here,
|
74
|
+
# but if you don't -- and you re-order your state calls later -- all stored bitmasks will
|
75
|
+
# be invalidated. You have been warned.
|
76
|
+
state :new, 1
|
77
|
+
state :old, 2
|
78
|
+
state :borrowed, 3
|
79
|
+
state :blue, 4
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
foo = Foo.new
|
84
|
+
foo.state #=> nil
|
85
|
+
foo.state = [:new, :borrowed]
|
86
|
+
foo.state #=> [:new, :borrowed]
|
87
|
+
foo.new? #=> true
|
88
|
+
foo.borrowed? #=> true
|
89
|
+
foo.blue? #=> false
|
90
|
+
foo.blue!
|
91
|
+
foo.blue? #=> true
|
92
|
+
|
93
|
+
# foo.state will boil happily down to an integer when you store it.
|
94
|
+
|
95
|
+
Aggregates
|
96
|
+
-
|
97
|
+
|
98
|
+
What about when a group of states is needed? Yeah, you could write `foo.bar? || foo.baz?`. You could even make that a method!
|
99
|
+
But why not just add the following?
|
100
|
+
|
101
|
+
class Foo
|
102
|
+
extend Maintain
|
103
|
+
maintains :state do
|
104
|
+
state :new
|
105
|
+
state :old
|
106
|
+
state :borrowed
|
107
|
+
state :blue
|
108
|
+
|
109
|
+
aggregate :starts_with_b, [:borrowed, :blue]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
foo = Foo.new
|
114
|
+
foo.status = :borrowed
|
115
|
+
foo.starts_with_b? #=> true
|
116
|
+
|
117
|
+
Named Scopes
|
118
|
+
-
|
119
|
+
|
120
|
+
**Maintain** knows all about ActiveRecord. Adding states and aggregates will automatically create named scopes on ActiveRecord::Base
|
121
|
+
subclasses for those states! Check it:
|
122
|
+
|
123
|
+
class Foo < ActiveRecord::Base
|
124
|
+
extend Maintain
|
125
|
+
maintains :state do
|
126
|
+
state :active
|
127
|
+
state :inactive
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
Foo.active #=> []
|
132
|
+
Foo.inactive #=> []
|
133
|
+
|
134
|
+
Hooks
|
135
|
+
-
|
136
|
+
|
137
|
+
**Maintain** can hook into state entry and exit, and provides a number of mechanisms for doing so:
|
138
|
+
|
139
|
+
class Foo < ActiveRecord::Base
|
140
|
+
maintains :state do
|
141
|
+
state :active, :enter => :activated
|
142
|
+
state :inactive, :exit => lambda { self.bar.baz! }
|
143
|
+
end
|
144
|
+
|
145
|
+
def activated
|
146
|
+
puts "I'm alive!"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
Of course, maybe that's not your style. Why not try this?
|
151
|
+
|
152
|
+
class Foo
|
153
|
+
extend Maintain
|
154
|
+
maintains :state do
|
155
|
+
state :active
|
156
|
+
state :inactive
|
157
|
+
|
158
|
+
enter :active, :activated
|
159
|
+
exit :inactive do
|
160
|
+
bar.baz!
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def activated
|
165
|
+
puts "I'm alive!"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
task :default => :spec
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'spec/rake/spectask'
|
7
|
+
|
8
|
+
desc "Run all examples"
|
9
|
+
Spec::Rake::SpecTask.new('spec') do |t|
|
10
|
+
t.spec_files = FileList['spec/**/*.rb']
|
11
|
+
end
|
12
|
+
|
13
|
+
desc "Run all examples with RCov"
|
14
|
+
Spec::Rake::SpecTask.new('spec:rcov') do |t|
|
15
|
+
t.spec_files = FileList['spec/**/*.rb']
|
16
|
+
t.rcov = true
|
17
|
+
t.rcov_opts = ['--exclude', 'spec,gem']
|
18
|
+
end
|
19
|
+
rescue LoadError
|
20
|
+
puts "Could not load Rspec. To run tests, use `gem install rspec`"
|
21
|
+
end
|
22
|
+
|
23
|
+
begin
|
24
|
+
require 'jeweler'
|
25
|
+
Jeweler::Tasks.new do |gemspec|
|
26
|
+
gemspec.name = "maintain"
|
27
|
+
gemspec.summary = "A Ruby state machine that lets your code do the driving"
|
28
|
+
gemspec.description = %{
|
29
|
+
Maintain is a simple state machine mixin for Ruby objects. It supports comparisons, bitmasks,
|
30
|
+
and hooks that really work. It can be used for multiple attributes and will always do its best to
|
31
|
+
stay out of your way and let your code drive the machine, and not vice versa.
|
32
|
+
}
|
33
|
+
gemspec.email = "flip@x451.com"
|
34
|
+
gemspec.homepage = "http://github.com/flipsasser/maintain"
|
35
|
+
gemspec.authors = ["Flip Sasser"]
|
36
|
+
end
|
37
|
+
rescue LoadError
|
38
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Maintain
|
2
|
+
class BitmaskValue < Value
|
3
|
+
def set_value(value)
|
4
|
+
@value = bitmask_for(value)
|
5
|
+
end
|
6
|
+
|
7
|
+
protected
|
8
|
+
def bitmask_for(states)
|
9
|
+
Array(states).map{|value| value_for(value) }.sort.inject(0) {|total, mask| total | mask }
|
10
|
+
end
|
11
|
+
|
12
|
+
def compare_value
|
13
|
+
@value ||= 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def compare_value_for(state)
|
17
|
+
bitmask_for(state)
|
18
|
+
end
|
19
|
+
|
20
|
+
def method_missing(method, *args)
|
21
|
+
if (method.to_s =~ /^(.+)(\?|!)$/) && @state.states.has_key?($1.to_sym)
|
22
|
+
compare = value_for($1)
|
23
|
+
if $2 == '?'
|
24
|
+
self.class.class_eval <<-EOC
|
25
|
+
def #{method}
|
26
|
+
@value & #{compare.inspect} != 0
|
27
|
+
end
|
28
|
+
EOC
|
29
|
+
@value & compare != 0
|
30
|
+
else
|
31
|
+
self.class.class_eval <<-EOC
|
32
|
+
def #{method}
|
33
|
+
@value = (@value || 0) | #{compare.inspect}
|
34
|
+
end
|
35
|
+
EOC
|
36
|
+
@value = (@value || 0) | compare
|
37
|
+
end
|
38
|
+
else
|
39
|
+
super
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
module Maintain
|
2
|
+
class Maintainer
|
3
|
+
def aggregate(name, options)
|
4
|
+
if options.is_a?(Hash) && options.has_key?(:as)
|
5
|
+
options = options[:as]
|
6
|
+
end
|
7
|
+
aggregates[name] = options
|
8
|
+
# Now we're going to add proxies to test for state being in this aggregate. Don't create
|
9
|
+
# this method unless it doesn't exist.
|
10
|
+
boolean_method = "#{name}?"
|
11
|
+
if method_free?(boolean_method)
|
12
|
+
# Define it if'n it don't already exit! These are just proxies - so Foo.maintains(:state) { state :awesome }
|
13
|
+
# will now have Foo.new.awesome?. But that's really just a proxy for Foo.new.state.awesome?
|
14
|
+
# So they're just shortcuts for brevity's sake.
|
15
|
+
maintainee.class_eval <<-EOC
|
16
|
+
def #{boolean_method}
|
17
|
+
#{@attribute}.#{boolean_method}
|
18
|
+
end
|
19
|
+
EOC
|
20
|
+
end
|
21
|
+
# Now define the state
|
22
|
+
if @active_record && method_free?(name, true)
|
23
|
+
maintainee.named_scope name, :conditions => {@attribute => options}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def aggregates
|
28
|
+
@aggregates ||= {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def bitmask(value)
|
32
|
+
@bitmask = !!value
|
33
|
+
end
|
34
|
+
|
35
|
+
def bitmask?
|
36
|
+
@bitmask
|
37
|
+
end
|
38
|
+
|
39
|
+
def default(state)
|
40
|
+
@default = state
|
41
|
+
end
|
42
|
+
|
43
|
+
def default?
|
44
|
+
!!@default
|
45
|
+
end
|
46
|
+
|
47
|
+
def hook(event, state, instance)
|
48
|
+
if state && hooks[state.to_sym] && hooks[state.to_sym][event.to_sym]
|
49
|
+
hooks[state.to_sym][event.to_sym].each do |method|
|
50
|
+
if method.is_a?(Proc)
|
51
|
+
instance.instance_eval(&method)
|
52
|
+
else
|
53
|
+
instance.send(method)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def initialize(maintainee, attribute, active_record = false, options = {})
|
60
|
+
@maintainee = maintainee.name
|
61
|
+
@attribute = attribute.to_sym
|
62
|
+
@active_record = !!active_record
|
63
|
+
options.each do |key, value|
|
64
|
+
self.send(key, value)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def integer(value)
|
69
|
+
@integer = !!value
|
70
|
+
end
|
71
|
+
|
72
|
+
def on(event, state, method = nil, &block)
|
73
|
+
if block_given?
|
74
|
+
method = block
|
75
|
+
end
|
76
|
+
hooks[state.to_sym] ||= {}
|
77
|
+
hooks[state.to_sym][event.to_sym] ||= []
|
78
|
+
hooks[state.to_sym][event.to_sym].push(method) unless hooks[state.to_sym][event.to_sym].include?(method)
|
79
|
+
end
|
80
|
+
|
81
|
+
def state_name_for(value)
|
82
|
+
if value = states.find {|key, options| options[:compare_value] == value}
|
83
|
+
value[0]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def state(name, value = nil, options = {})
|
88
|
+
if value.is_a?(Hash)
|
89
|
+
options = value
|
90
|
+
value = nil
|
91
|
+
end
|
92
|
+
if options.has_key?(:default)
|
93
|
+
default(name)
|
94
|
+
end
|
95
|
+
@increment ||= 0
|
96
|
+
if @bitmask
|
97
|
+
unless value.is_a?(Integer)
|
98
|
+
value = @increment
|
99
|
+
end
|
100
|
+
value = 2 ** value.to_i
|
101
|
+
elsif value.is_a?(Integer)
|
102
|
+
integer(true)
|
103
|
+
end
|
104
|
+
value ||= name
|
105
|
+
states[name] = {:compare_value => !@bitmask && value.is_a?(Integer) ? value : @increment, :value => value}
|
106
|
+
@increment += 1
|
107
|
+
if @active_record && !maintainee.respond_to?(name)
|
108
|
+
conditions = {}
|
109
|
+
maintainee.named_scope name, :conditions => {@attribute => value.is_a?(Symbol) ? value.to_s : value}
|
110
|
+
end
|
111
|
+
|
112
|
+
# Now we're going to add proxies to test for state. These methods only get added if a
|
113
|
+
# method of their name doesn't already exist.
|
114
|
+
boolean_method = "#{name}?"
|
115
|
+
if method_free?(boolean_method)
|
116
|
+
# Define it if'n it don't already exit! These are just proxies - so Foo.maintains(:state) { state :awesome }
|
117
|
+
# will now have Foo.new.awesome?. But that's really just a proxy for Foo.new.state.awesome?
|
118
|
+
# So they're just shortcuts for brevity's sake.
|
119
|
+
maintainee.class_eval <<-EOC
|
120
|
+
def #{boolean_method}
|
121
|
+
#{@attribute}.#{boolean_method}
|
122
|
+
end
|
123
|
+
EOC
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def states
|
128
|
+
@states ||= {}
|
129
|
+
end
|
130
|
+
|
131
|
+
def value(initial = nil)
|
132
|
+
if @bitmask
|
133
|
+
BitmaskValue.new(self, initial || @default || 0)
|
134
|
+
elsif @integer
|
135
|
+
IntegerValue.new(self, initial || @default)
|
136
|
+
else
|
137
|
+
Value.new(self, initial || @default)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
protected
|
142
|
+
def hooks
|
143
|
+
@hooks ||= {}
|
144
|
+
end
|
145
|
+
|
146
|
+
def maintainee
|
147
|
+
Object.const_get(@maintainee)
|
148
|
+
end
|
149
|
+
|
150
|
+
def method_free?(method_name, class_method = false)
|
151
|
+
# Ugly hack so we don't fetch it 100 times for no reason
|
152
|
+
maintainee_class = maintainee
|
153
|
+
if class_method
|
154
|
+
respond_to = maintainee_class.respond_to?(method_name)
|
155
|
+
methods = maintainee_class.public_methods + maintainee_class.private_methods + maintainee_class.protected_methods
|
156
|
+
else
|
157
|
+
respond_to = false
|
158
|
+
methods = maintainee_class.public_instance_methods + maintainee_class.private_instance_methods + maintainee_class.protected_instance_methods
|
159
|
+
end
|
160
|
+
!respond_to && !methods.include?(method_name)
|
161
|
+
end
|
162
|
+
|
163
|
+
def method_missing(method, *args)
|
164
|
+
if states.has_key?(method)
|
165
|
+
states[method][:value]
|
166
|
+
else
|
167
|
+
super
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Maintain
|
2
|
+
class Value
|
3
|
+
def >(value)
|
4
|
+
compare_value > compare_value_for(value)
|
5
|
+
end
|
6
|
+
|
7
|
+
def >=(value)
|
8
|
+
compare_value >= compare_value_for(value)
|
9
|
+
end
|
10
|
+
|
11
|
+
def <(value)
|
12
|
+
compare_value < compare_value_for(value)
|
13
|
+
end
|
14
|
+
|
15
|
+
def <=(value)
|
16
|
+
compare_value <= compare_value_for(value)
|
17
|
+
end
|
18
|
+
|
19
|
+
def ==(value)
|
20
|
+
compare_value == compare_value_for(value)
|
21
|
+
end
|
22
|
+
|
23
|
+
def class
|
24
|
+
value.class
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(state, value = nil)
|
28
|
+
@state = state
|
29
|
+
@value = value
|
30
|
+
end
|
31
|
+
|
32
|
+
def inspect
|
33
|
+
value.inspect
|
34
|
+
end
|
35
|
+
|
36
|
+
def nil?
|
37
|
+
value.nil?
|
38
|
+
end
|
39
|
+
|
40
|
+
def set_value(value)
|
41
|
+
@compare_value = nil
|
42
|
+
@value = state_name_for(value)
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_s
|
46
|
+
value.to_s
|
47
|
+
end
|
48
|
+
|
49
|
+
def value
|
50
|
+
@value
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
def compare_value
|
55
|
+
@compare_value ||= compare_value_for(@value)
|
56
|
+
end
|
57
|
+
|
58
|
+
def compare_value_for(state)
|
59
|
+
state_value_for(state, :compare_value)
|
60
|
+
end
|
61
|
+
|
62
|
+
def method_missing(method, *args)
|
63
|
+
if (method.to_s =~ /^(.+)\?$/)
|
64
|
+
check = $1.to_sym
|
65
|
+
if @state.states.has_key?(check)
|
66
|
+
self.class.class_eval <<-EOC
|
67
|
+
def #{method}
|
68
|
+
self == #{check.inspect}
|
69
|
+
end
|
70
|
+
EOC
|
71
|
+
# Calling `method` on ourselves fails. Something to do w/subclasses. Meh.
|
72
|
+
return self == $1.to_sym
|
73
|
+
elsif aggregates = @state.aggregates[check]
|
74
|
+
self.class.class_eval <<-EOC
|
75
|
+
def #{method}
|
76
|
+
@state.aggregates[#{check.inspect}].include?(@value)
|
77
|
+
end
|
78
|
+
EOC
|
79
|
+
return aggregates.include?(@value)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
super
|
83
|
+
end
|
84
|
+
|
85
|
+
def state_name_for(value)
|
86
|
+
if (value.is_a?(String) || value.is_a?(Symbol))
|
87
|
+
@state.states.has_key?(value.to_sym) ? value.to_sym : nil
|
88
|
+
else
|
89
|
+
@state.state_name_for(value)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def state_value_for(state, value)
|
94
|
+
if (state.is_a?(String) || state.is_a?(Symbol)) && state_hash = @state.states[state.to_sym]
|
95
|
+
state_hash[value]
|
96
|
+
else
|
97
|
+
state
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def value_for(state)
|
102
|
+
state_value_for(state, :value)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/maintain.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
module Maintain
|
2
|
+
# We're not really interested in loading anything into memory if we don't need to,
|
3
|
+
# so Maintainer, Value, and the Value subclasses are ignored until they're needed.
|
4
|
+
autoload(:Maintainer, 'lib/maintain/maintainer')
|
5
|
+
autoload(:Value, 'lib/maintain/value')
|
6
|
+
autoload(:BitmaskValue, 'lib/maintain/bitmask_value')
|
7
|
+
autoload(:IntegerValue, 'lib/maintain/integer_value')
|
8
|
+
|
9
|
+
# The core class method of Maintain. Basic usage is:
|
10
|
+
#
|
11
|
+
# maintain :state do
|
12
|
+
# state :new, :default => true
|
13
|
+
# state :expired, :enter => :expire_children
|
14
|
+
# state :reopened, :exit => lambda { children.each(&:reopen) }
|
15
|
+
# aggregate :accessible, :as => [:new, :reopened]
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# It also supports more complex configuration options, like bitmask columns
|
19
|
+
# and integer values (for performance and portability)
|
20
|
+
#
|
21
|
+
# maintain :permissions, :bitmask => true do
|
22
|
+
# state :edit, 1
|
23
|
+
# state :delete, 2
|
24
|
+
# state :manage, 3
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# This method is aliased as `maintains` with the intention of allowing developers
|
28
|
+
# to code imperatively ("maintain, damn you!") or descriptively ("it maintains, man")
|
29
|
+
def maintain(attribute, options = {}, &block)
|
30
|
+
# Detect if this is ActiveRecord::Base or a subclass of it
|
31
|
+
# TODO: Make this not suck
|
32
|
+
if defined?(ActiveRecord::Base)
|
33
|
+
active_record = self == ActiveRecord::Base
|
34
|
+
superclass = self
|
35
|
+
while !active_record && superclass.superclass
|
36
|
+
active_record = superclass == ActiveRecord::Base
|
37
|
+
superclass = superclass.superclass
|
38
|
+
end
|
39
|
+
else
|
40
|
+
active_record = false
|
41
|
+
end
|
42
|
+
|
43
|
+
# Create an instance of the maintainer class. It handles all of the state
|
44
|
+
# configuration, hooking, aggregation, named_scoping, etc.
|
45
|
+
maintainer = Maintainer.new(self, attribute, active_record, options)
|
46
|
+
if block_given?
|
47
|
+
maintainer.instance_eval(&block)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Define our getters and setters - these are the only methods Maintain will stomp
|
51
|
+
# on if you've already defined them. This is because they're how Maintain works.
|
52
|
+
class_eval <<-EOC
|
53
|
+
def #{attribute}=(value)
|
54
|
+
# If we can find the maintainer on this attribute, we'll use it to set values.
|
55
|
+
if maintainer = self.class.maintainers[#{attribute.to_sym.inspect}]
|
56
|
+
# First, we instantiate a value on this maintainer if we haven't already
|
57
|
+
@#{attribute} ||= maintainer.value
|
58
|
+
|
59
|
+
# Then run the exit hook if we're changing the value
|
60
|
+
maintainer.hook(:exit, @#{attribute}.value, self)
|
61
|
+
|
62
|
+
# Then set the value itself. Maintainer::State will return the value you set,
|
63
|
+
# so if we're setting to nil we get rid of the attribute entirely - it's not
|
64
|
+
# needed and we want the getter to return nil in that case.
|
65
|
+
unless @#{attribute}.set_value(value)
|
66
|
+
@#{attribute} = nil
|
67
|
+
end#{%{
|
68
|
+
|
69
|
+
# If this is ActiveRecord::Base or a subclass of it, we'll make sure calling the
|
70
|
+
# setter writes a DB-friendly value.
|
71
|
+
if respond_to?(:write_attribute)
|
72
|
+
write_attribute(:#{attribute}, @#{attribute} ? @#{attribute}.value.to_s : nil)
|
73
|
+
end
|
74
|
+
} if active_record}
|
75
|
+
|
76
|
+
# Last but not least, run the enter hooks for the new value - cause that's how we
|
77
|
+
# do.
|
78
|
+
maintainer.hook(:enter, @#{attribute}.value, self) if @#{attribute}
|
79
|
+
else
|
80
|
+
# If we can't find a maintainer for this attribute, make our best effort to do what
|
81
|
+
# attr_accessor does - set the instance variable.
|
82
|
+
@#{attribute} = value#{%{
|
83
|
+
|
84
|
+
# ... and on ActiveRecord::Base, we'll also write the attribute like a normal setter.
|
85
|
+
if respond_to?(:write_attribute)
|
86
|
+
write_attribute(:#{attribute}, @#{attribute})
|
87
|
+
end
|
88
|
+
} if active_record}
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def #{attribute}
|
93
|
+
# Start by returning an already-instantiated Maintainer::State if it exists
|
94
|
+
return @#{attribute} if @#{attribute}
|
95
|
+
|
96
|
+
# If'n it doesn't already exist AND this maintained attribute has a default value (and
|
97
|
+
# bitmasks must have at least a 0 value), we'll instantiate a Maintainer::State and return
|
98
|
+
# it.
|
99
|
+
if self.class.maintainers[#{attribute.to_sym.inspect}].default? || self.class.maintainers[#{attribute.to_sym.inspect}].bitmask?
|
100
|
+
@#{attribute} = self.class.maintainers[#{attribute.to_sym.inspect}].value#{"(read_attribute(:#{attribute}))" if active_record}
|
101
|
+
end
|
102
|
+
end
|
103
|
+
EOC
|
104
|
+
|
105
|
+
# Last! Not least! Save our maintainer directly on this class. We'll use it in our setters (as in above)
|
106
|
+
# and we'll also modify it instead of replacing it outright, so subclasses or mixins can extend functionality
|
107
|
+
# without replacing it.
|
108
|
+
maintainers[attribute.to_sym] = maintainer
|
109
|
+
end
|
110
|
+
alias :maintains :maintain
|
111
|
+
|
112
|
+
def maintainers #:nodoc:
|
113
|
+
@maintainers ||= {}
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
if defined?(ActiveRecord::Base)
|
118
|
+
ActiveRecord::Base.extend Maintain
|
119
|
+
end
|