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 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