maintain 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|