maintain 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,7 @@
1
+ module Maintain
2
+ class IntegerValue < Value
3
+ def value
4
+ value_for(@value)
5
+ end
6
+ end
7
+ 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