hsume2-state_machine 1.0.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.
Files changed (110) hide show
  1. data/CHANGELOG.rdoc +413 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +717 -0
  4. data/Rakefile +77 -0
  5. data/examples/AutoShop_state.png +0 -0
  6. data/examples/Car_state.png +0 -0
  7. data/examples/TrafficLight_state.png +0 -0
  8. data/examples/Vehicle_state.png +0 -0
  9. data/examples/auto_shop.rb +11 -0
  10. data/examples/car.rb +19 -0
  11. data/examples/merb-rest/controller.rb +51 -0
  12. data/examples/merb-rest/model.rb +28 -0
  13. data/examples/merb-rest/view_edit.html.erb +24 -0
  14. data/examples/merb-rest/view_index.html.erb +23 -0
  15. data/examples/merb-rest/view_new.html.erb +13 -0
  16. data/examples/merb-rest/view_show.html.erb +17 -0
  17. data/examples/rails-rest/controller.rb +43 -0
  18. data/examples/rails-rest/migration.rb +11 -0
  19. data/examples/rails-rest/model.rb +23 -0
  20. data/examples/rails-rest/view_edit.html.erb +25 -0
  21. data/examples/rails-rest/view_index.html.erb +23 -0
  22. data/examples/rails-rest/view_new.html.erb +14 -0
  23. data/examples/rails-rest/view_show.html.erb +17 -0
  24. data/examples/traffic_light.rb +7 -0
  25. data/examples/vehicle.rb +31 -0
  26. data/init.rb +1 -0
  27. data/lib/state_machine.rb +448 -0
  28. data/lib/state_machine/alternate_machine.rb +79 -0
  29. data/lib/state_machine/assertions.rb +36 -0
  30. data/lib/state_machine/branch.rb +224 -0
  31. data/lib/state_machine/callback.rb +236 -0
  32. data/lib/state_machine/condition_proxy.rb +94 -0
  33. data/lib/state_machine/error.rb +13 -0
  34. data/lib/state_machine/eval_helpers.rb +86 -0
  35. data/lib/state_machine/event.rb +304 -0
  36. data/lib/state_machine/event_collection.rb +139 -0
  37. data/lib/state_machine/extensions.rb +149 -0
  38. data/lib/state_machine/initializers.rb +4 -0
  39. data/lib/state_machine/initializers/merb.rb +1 -0
  40. data/lib/state_machine/initializers/rails.rb +25 -0
  41. data/lib/state_machine/integrations.rb +110 -0
  42. data/lib/state_machine/integrations/active_model.rb +502 -0
  43. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  44. data/lib/state_machine/integrations/active_model/observer.rb +45 -0
  45. data/lib/state_machine/integrations/active_model/versions.rb +31 -0
  46. data/lib/state_machine/integrations/active_record.rb +424 -0
  47. data/lib/state_machine/integrations/active_record/locale.rb +20 -0
  48. data/lib/state_machine/integrations/active_record/versions.rb +143 -0
  49. data/lib/state_machine/integrations/base.rb +91 -0
  50. data/lib/state_machine/integrations/data_mapper.rb +392 -0
  51. data/lib/state_machine/integrations/data_mapper/observer.rb +210 -0
  52. data/lib/state_machine/integrations/data_mapper/versions.rb +62 -0
  53. data/lib/state_machine/integrations/mongo_mapper.rb +272 -0
  54. data/lib/state_machine/integrations/mongo_mapper/locale.rb +4 -0
  55. data/lib/state_machine/integrations/mongo_mapper/versions.rb +110 -0
  56. data/lib/state_machine/integrations/mongoid.rb +357 -0
  57. data/lib/state_machine/integrations/mongoid/locale.rb +4 -0
  58. data/lib/state_machine/integrations/mongoid/versions.rb +18 -0
  59. data/lib/state_machine/integrations/sequel.rb +428 -0
  60. data/lib/state_machine/integrations/sequel/versions.rb +36 -0
  61. data/lib/state_machine/machine.rb +1873 -0
  62. data/lib/state_machine/machine_collection.rb +87 -0
  63. data/lib/state_machine/matcher.rb +123 -0
  64. data/lib/state_machine/matcher_helpers.rb +54 -0
  65. data/lib/state_machine/node_collection.rb +157 -0
  66. data/lib/state_machine/path.rb +120 -0
  67. data/lib/state_machine/path_collection.rb +90 -0
  68. data/lib/state_machine/state.rb +271 -0
  69. data/lib/state_machine/state_collection.rb +112 -0
  70. data/lib/state_machine/transition.rb +458 -0
  71. data/lib/state_machine/transition_collection.rb +244 -0
  72. data/lib/tasks/state_machine.rake +1 -0
  73. data/lib/tasks/state_machine.rb +27 -0
  74. data/test/files/en.yml +17 -0
  75. data/test/files/switch.rb +11 -0
  76. data/test/functional/alternate_state_machine_test.rb +122 -0
  77. data/test/functional/state_machine_test.rb +993 -0
  78. data/test/test_helper.rb +4 -0
  79. data/test/unit/assertions_test.rb +40 -0
  80. data/test/unit/branch_test.rb +890 -0
  81. data/test/unit/callback_test.rb +701 -0
  82. data/test/unit/condition_proxy_test.rb +328 -0
  83. data/test/unit/error_test.rb +43 -0
  84. data/test/unit/eval_helpers_test.rb +222 -0
  85. data/test/unit/event_collection_test.rb +358 -0
  86. data/test/unit/event_test.rb +985 -0
  87. data/test/unit/integrations/active_model_test.rb +1097 -0
  88. data/test/unit/integrations/active_record_test.rb +2021 -0
  89. data/test/unit/integrations/base_test.rb +99 -0
  90. data/test/unit/integrations/data_mapper_test.rb +1909 -0
  91. data/test/unit/integrations/mongo_mapper_test.rb +1611 -0
  92. data/test/unit/integrations/mongoid_test.rb +1591 -0
  93. data/test/unit/integrations/sequel_test.rb +1523 -0
  94. data/test/unit/integrations_test.rb +61 -0
  95. data/test/unit/invalid_event_test.rb +20 -0
  96. data/test/unit/invalid_parallel_transition_test.rb +18 -0
  97. data/test/unit/invalid_transition_test.rb +77 -0
  98. data/test/unit/machine_collection_test.rb +599 -0
  99. data/test/unit/machine_test.rb +3043 -0
  100. data/test/unit/matcher_helpers_test.rb +37 -0
  101. data/test/unit/matcher_test.rb +155 -0
  102. data/test/unit/node_collection_test.rb +217 -0
  103. data/test/unit/path_collection_test.rb +266 -0
  104. data/test/unit/path_test.rb +485 -0
  105. data/test/unit/state_collection_test.rb +310 -0
  106. data/test/unit/state_machine_test.rb +31 -0
  107. data/test/unit/state_test.rb +924 -0
  108. data/test/unit/transition_collection_test.rb +2102 -0
  109. data/test/unit/transition_test.rb +1541 -0
  110. metadata +207 -0
@@ -0,0 +1,210 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ module DataMapper
4
+ # Adds support for creating before/after/around/failure transition
5
+ # callbacks within a DataMapper observer. These callbacks behave very
6
+ # similar to hooks during save/update/destroy/etc., but with the following
7
+ # modifications:
8
+ # * Each callback can define a set of transition requirements that must be
9
+ # met in order for the callback to get invoked.
10
+ # * An additional transition parameter is available that provides
11
+ # contextual information about the event (see StateMachine::Transition
12
+ # for more information)
13
+ #
14
+ # To define a single observer for multiple state machines:
15
+ #
16
+ # class StateMachineObserver
17
+ # include DataMapper::Observer
18
+ #
19
+ # observe Vehicle, Switch, Project
20
+ #
21
+ # after_transition do |transition|
22
+ # Audit.log(self, transition)
23
+ # end
24
+ # end
25
+ #
26
+ # == Requirements
27
+ #
28
+ # To use this feature of the DataMapper integration, the dm-observer library
29
+ # must be available. This can be installed either directly or indirectly
30
+ # through dm-more. When loading DataMapper, be sure to load the dm-observer
31
+ # library as well like so:
32
+ #
33
+ # require 'rubygems'
34
+ # require 'dm-core'
35
+ # require 'dm-observer'
36
+ #
37
+ # If dm-observer is not available, then this feature will be skipped.
38
+ module Observer
39
+ include MatcherHelpers
40
+
41
+ # Creates a callback that will be invoked *before* a transition is
42
+ # performed, so long as the given configuration options match the
43
+ # transition. Each part of the transition (event, to state, from state)
44
+ # must match in order for the callback to get invoked.
45
+ #
46
+ # See StateMachine::Machine#before_transition for more
47
+ # information about the various configuration options available.
48
+ #
49
+ # == Examples
50
+ #
51
+ # class Vehicle
52
+ # include DataMapper::Resource
53
+ #
54
+ # property :id, Serial
55
+ # property :state, :String
56
+ #
57
+ # state_machine :initial => :parked do
58
+ # event :ignite do
59
+ # transition :parked => :idling
60
+ # end
61
+ # end
62
+ # end
63
+ #
64
+ # class VehicleObserver
65
+ # include DataMapper::Observer
66
+ #
67
+ # observe Vehicle
68
+ #
69
+ # before :save do
70
+ # # log message
71
+ # end
72
+ #
73
+ # # Target all state machines
74
+ # before_transition :parked => :idling, :on => :ignite do
75
+ # # put on seatbelt
76
+ # end
77
+ #
78
+ # # Target a specific state machine
79
+ # before_transition :state, any => :idling do
80
+ # # put on seatbelt
81
+ # end
82
+ #
83
+ # # Target all state machines without requirements
84
+ # before_transition do |transition|
85
+ # # log message
86
+ # end
87
+ # end
88
+ #
89
+ # *Note* that in each of the above +before_transition+ callbacks, the
90
+ # callback is executed within the context of the object (i.e. the
91
+ # Vehicle instance being transition). This means that +self+ refers
92
+ # to the vehicle record within each callback block.
93
+ def before_transition(*args, &block)
94
+ add_transition_callback(:before_transition, *args, &block)
95
+ end
96
+
97
+ # Creates a callback that will be invoked *after* a transition is
98
+ # performed so long as the given configuration options match the
99
+ # transition.
100
+ #
101
+ # See +before_transition+ for a description of the possible configurations
102
+ # for defining callbacks.
103
+ def after_transition(*args, &block)
104
+ add_transition_callback(:after_transition, *args, &block)
105
+ end
106
+
107
+ # Creates a callback that will be invoked *around* a transition so long
108
+ # as the given requirements match the transition.
109
+ #
110
+ # == Examples
111
+ #
112
+ # class Vehicle
113
+ # include DataMapper::Resource
114
+ #
115
+ # property :id, Serial
116
+ # property :state, :String
117
+ #
118
+ # state_machine :initial => :parked do
119
+ # event :ignite do
120
+ # transition :parked => :idling
121
+ # end
122
+ # end
123
+ # end
124
+ #
125
+ # class VehicleObserver
126
+ # include DataMapper::Observer
127
+ #
128
+ # observe Vehicle
129
+ #
130
+ # around_transition do |transition, block|
131
+ # # track start time
132
+ # block.call
133
+ # # track end time
134
+ # end
135
+ # end
136
+ #
137
+ # See +before_transition+ for a description of the possible configurations
138
+ # for defining callbacks.
139
+ def around_transition(*args, &block)
140
+ add_transition_callback(:around_transition, *args, &block)
141
+ end
142
+
143
+ # Creates a callback that will be invoked *after* a transition failures to
144
+ # be performed so long as the given requirements match the transition.
145
+ #
146
+ # == Example
147
+ #
148
+ # class Vehicle
149
+ # include DataMapper::Resource
150
+ #
151
+ # property :id, Serial
152
+ # property :state, :String
153
+ #
154
+ # state_machine :initial => :parked do
155
+ # event :ignite do
156
+ # transition :parked => :idling
157
+ # end
158
+ # end
159
+ # end
160
+ #
161
+ # class VehicleObserver
162
+ # after_transition_failure do |transition|
163
+ # # log failure
164
+ # end
165
+ #
166
+ # after_transition_failure :on => :ignite do
167
+ # # log failure
168
+ # end
169
+ # end
170
+ #
171
+ # See +before_transition+ for a description of the possible configurations
172
+ # for defining callbacks. *Note* however that you cannot define the state
173
+ # requirements in these callbacks. You may only define event requirements.
174
+ def after_transition_failure(*args, &block)
175
+ add_transition_callback(:after_failure, *args, &block)
176
+ end
177
+
178
+ private
179
+ # Adds the transition callback to a specific machine or all of the
180
+ # state machines for each observed class.
181
+ def add_transition_callback(type, *args, &block)
182
+ if args.any? && !args.first.is_a?(Hash)
183
+ # Specific machine(s) being targeted
184
+ names = args
185
+ args = args.last.is_a?(Hash) ? [args.pop] : []
186
+ else
187
+ # Target all state machines
188
+ names = nil
189
+ end
190
+
191
+ # Add the transition callback to each class being observed
192
+ observing.each do |klass|
193
+ state_machines =
194
+ if names
195
+ names.map {|name| klass.state_machines.fetch(name)}
196
+ else
197
+ klass.state_machines.values
198
+ end
199
+
200
+ state_machines.each {|machine| machine.send(type, *args, &block)}
201
+ end if observing
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+
208
+ DataMapper::Observer::ClassMethods.class_eval do
209
+ include StateMachine::Integrations::DataMapper::Observer
210
+ end
@@ -0,0 +1,62 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ module DataMapper
4
+ version '0.9.x' do
5
+ def self.active?
6
+ ::DataMapper::VERSION =~ /^0\.9\./
7
+ end
8
+
9
+ def action_hook
10
+ action
11
+ end
12
+
13
+ def mark_dirty(object, value)
14
+ object.original_values[self.attribute] = "#{value}-ignored" if object.original_values[self.attribute] == value
15
+ end
16
+ end
17
+
18
+ version '0.9.x - 0.10.x' do
19
+ def self.active?
20
+ ::DataMapper::VERSION =~ /^0\.\d\./ || ::DataMapper::VERSION =~ /^0\.10\./
21
+ end
22
+
23
+ def pluralize(word)
24
+ ::Extlib::Inflection.pluralize(word.to_s)
25
+ end
26
+ end
27
+
28
+ version '1.0.0' do
29
+ def self.active?
30
+ ::DataMapper::VERSION == '1.0.0'
31
+ end
32
+
33
+ def pluralize(word)
34
+ (defined?(::ActiveSupport::Inflector) ? ::ActiveSupport::Inflector : ::Extlib::Inflection).pluralize(word.to_s)
35
+ end
36
+ end
37
+
38
+ version '0.9.4 - 0.9.6' do
39
+ def self.active?
40
+ ::DataMapper::VERSION =~ /^0\.9\.[4-6]/
41
+ end
42
+
43
+ # 0.9.4 - 0.9.6 fails to run after callbacks when validations are
44
+ # enabled because of the way dm-validations integrates
45
+ def define_action_helpers?
46
+ super if action != :save || !supports_validations?
47
+ end
48
+ end
49
+
50
+ version '0.10.x' do
51
+ def self.active?
52
+ ::DataMapper::VERSION =~ /^0\.10\./
53
+ end
54
+
55
+ def mark_dirty(object, value)
56
+ property = owner_class.properties[self.attribute]
57
+ object.original_attributes[property] = "#{value}-ignored" unless object.original_attributes.include?(property)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,272 @@
1
+ require 'state_machine/integrations/active_model'
2
+
3
+ module StateMachine
4
+ module Integrations #:nodoc:
5
+ # Adds support for integrating state machines with MongoMapper models.
6
+ #
7
+ # == Examples
8
+ #
9
+ # Below is an example of a simple state machine defined within a
10
+ # MongoMapper model:
11
+ #
12
+ # class Vehicle
13
+ # include MongoMapper::Document
14
+ #
15
+ # state_machine :initial => :parked do
16
+ # event :ignite do
17
+ # transition :parked => :idling
18
+ # end
19
+ # end
20
+ # end
21
+ #
22
+ # The examples in the sections below will use the above class as a
23
+ # reference.
24
+ #
25
+ # == Actions
26
+ #
27
+ # By default, the action that will be invoked when a state is transitioned
28
+ # is the +save+ action. This will cause the record to save the changes
29
+ # made to the state machine's attribute. *Note* that if any other changes
30
+ # were made to the record prior to transition, then those changes will
31
+ # be saved as well.
32
+ #
33
+ # For example,
34
+ #
35
+ # vehicle = Vehicle.create # => #<Vehicle id: 1, name: nil, state: "parked">
36
+ # vehicle.name = 'Ford Explorer'
37
+ # vehicle.ignite # => true
38
+ # vehicle.reload # => #<Vehicle id: 1, name: "Ford Explorer", state: "idling">
39
+ #
40
+ # == Events
41
+ #
42
+ # As described in StateMachine::InstanceMethods#state_machine, event
43
+ # attributes are created for every machine that allow transitions to be
44
+ # performed automatically when the object's action (in this case, :save)
45
+ # is called.
46
+ #
47
+ # In MongoMapper, these automated events are run in the following order:
48
+ # * before validation - Run before callbacks and persist new states, then validate
49
+ # * before save - If validation was skipped, run before callbacks and persist new states, then save
50
+ # * after save - Run after callbacks
51
+ #
52
+ # For example,
53
+ #
54
+ # vehicle = Vehicle.create # => #<Vehicle id: 1, name: nil, state: "parked">
55
+ # vehicle.state_event # => nil
56
+ # vehicle.state_event = 'invalid'
57
+ # vehicle.valid? # => false
58
+ # vehicle.errors.full_messages # => ["State event is invalid"]
59
+ #
60
+ # vehicle.state_event = 'ignite'
61
+ # vehicle.valid? # => true
62
+ # vehicle.save # => true
63
+ # vehicle.state # => "idling"
64
+ # vehicle.state_event # => nil
65
+ #
66
+ # Note that this can also be done on a mass-assignment basis:
67
+ #
68
+ # vehicle = Vehicle.create(:state_event => 'ignite') # => #<Vehicle id: 1, name: nil, state: "idling">
69
+ # vehicle.state # => "idling"
70
+ #
71
+ # This technique is always used for transitioning states when the +save+
72
+ # action (which is the default) is configured for the machine.
73
+ #
74
+ # === Security implications
75
+ #
76
+ # Beware that public event attributes mean that events can be fired
77
+ # whenever mass-assignment is being used. If you want to prevent malicious
78
+ # users from tampering with events through URLs / forms, the attribute
79
+ # should be protected like so:
80
+ #
81
+ # class Vehicle
82
+ # include MongoMapper::Document
83
+ #
84
+ # attr_protected :state_event
85
+ # # attr_accessible ... # Alternative technique
86
+ #
87
+ # state_machine do
88
+ # ...
89
+ # end
90
+ # end
91
+ #
92
+ # If you want to only have *some* events be able to fire via mass-assignment,
93
+ # you can build two state machines (one public and one protected) like so:
94
+ #
95
+ # class Vehicle
96
+ # include MongoMapper::Document
97
+ #
98
+ # attr_protected :state_event # Prevent access to events in the first machine
99
+ #
100
+ # state_machine do
101
+ # # Define private events here
102
+ # end
103
+ #
104
+ # # Public machine targets the same state as the private machine
105
+ # state_machine :public_state, :attribute => :state do
106
+ # # Define public events here
107
+ # end
108
+ # end
109
+ #
110
+ # == Validation errors
111
+ #
112
+ # If an event fails to successfully fire because there are no matching
113
+ # transitions for the current record, a validation error is added to the
114
+ # record's state attribute to help in determining why it failed and for
115
+ # reporting via the UI.
116
+ #
117
+ # For example,
118
+ #
119
+ # vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle id: 1, name: nil, state: "idling">
120
+ # vehicle.ignite # => false
121
+ # vehicle.errors.full_messages # => ["State cannot transition via \"ignite\""]
122
+ #
123
+ # If an event fails to fire because of a validation error on the record and
124
+ # *not* because a matching transition was not available, no error messages
125
+ # will be added to the state attribute.
126
+ #
127
+ # == Scopes
128
+ #
129
+ # To assist in filtering models with specific states, a series of basic
130
+ # scopes are defined on the model for finding records with or without a
131
+ # particular set of states.
132
+ #
133
+ # These scopes are essentially the functional equivalent of the following
134
+ # definitions:
135
+ #
136
+ # class Vehicle
137
+ # include MongoMapper::Document
138
+ #
139
+ # def self.with_states(*states)
140
+ # all(:conditions => {:state => {'$in' => states}})
141
+ # end
142
+ # # with_states also aliased to with_state
143
+ #
144
+ # def self.without_states(*states)
145
+ # all(:conditions => {:state => {'$nin' => states}})
146
+ # end
147
+ # # without_states also aliased to without_state
148
+ # end
149
+ #
150
+ # *Note*, however, that the states are converted to their stored values
151
+ # before being passed into the query.
152
+ #
153
+ # Because of the way named scopes work in MongoMapper, they *cannot* be
154
+ # chained.
155
+ #
156
+ # == Callbacks
157
+ #
158
+ # All before/after transition callbacks defined for MongoMapper models
159
+ # behave in the same way that other MongoMapper callbacks behave. The
160
+ # object involved in the transition is passed in as an argument.
161
+ #
162
+ # For example,
163
+ #
164
+ # class Vehicle
165
+ # include MongoMapper::Document
166
+ #
167
+ # state_machine :initial => :parked do
168
+ # before_transition any => :idling do |vehicle|
169
+ # vehicle.put_on_seatbelt
170
+ # end
171
+ #
172
+ # before_transition do |vehicle, transition|
173
+ # # log message
174
+ # end
175
+ #
176
+ # event :ignite do
177
+ # transition :parked => :idling
178
+ # end
179
+ # end
180
+ #
181
+ # def put_on_seatbelt
182
+ # ...
183
+ # end
184
+ # end
185
+ #
186
+ # Note, also, that the transition can be accessed by simply defining
187
+ # additional arguments in the callback block.
188
+ module MongoMapper
189
+ include Base
190
+ include ActiveModel
191
+
192
+ require 'state_machine/integrations/mongo_mapper/versions'
193
+
194
+ # The default options to use for state machines using this integration
195
+ @defaults = {:action => :save}
196
+
197
+ # Whether this integration is available. Only true if MongoMapper::Document
198
+ # is defined.
199
+ def self.available?
200
+ defined?(::MongoMapper::Document)
201
+ end
202
+
203
+ # Should this integration be used for state machines in the given class?
204
+ # Classes that include MongoMapper::Document will automatically use the
205
+ # MongoMapper integration.
206
+ def self.matches?(klass)
207
+ klass <= ::MongoMapper::Document
208
+ end
209
+
210
+ protected
211
+ # Only runs validations on the action if using <tt>:save</tt>
212
+ def runs_validations_on_action?
213
+ action == :save
214
+ end
215
+
216
+ # Defines an initialization hook into the owner class for setting the
217
+ # initial state of the machine *before* any attributes are set on the
218
+ # object
219
+ def define_state_initializer
220
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
221
+ def initialize(*args)
222
+ self.class.state_machines.initialize_states(self) { super }
223
+ end
224
+ end_eval
225
+ end
226
+
227
+ # Skips defining reader/writer methods since this is done automatically
228
+ def define_state_accessor
229
+ owner_class.key(attribute, String) unless owner_class.keys.include?(attribute)
230
+ super
231
+ end
232
+
233
+ # Uses around callbacks to run state events if using the :save hook
234
+ def define_action_hook
235
+ if action_hook == :save
236
+ owner_class.set_callback(:save, :around, self, :prepend => true)
237
+ else
238
+ super
239
+ end
240
+ end
241
+
242
+ # Runs state events around the machine's :save action
243
+ def around_save(object)
244
+ object.class.state_machines.transitions(object, action).perform { yield }
245
+ end
246
+
247
+ # Creates a scope for finding records *with* a particular state or
248
+ # states for the attribute
249
+ def create_with_scope(name)
250
+ define_scope(name, lambda {|values| {:conditions => {attribute => {'$in' => values}}}})
251
+ end
252
+
253
+ # Creates a scope for finding records *without* a particular state or
254
+ # states for the attribute
255
+ def create_without_scope(name)
256
+ define_scope(name, lambda {|values| {:conditions => {attribute => {'$nin' => values}}}})
257
+ end
258
+
259
+ # Defines a new scope with the given name
260
+ def define_scope(name, scope)
261
+ lambda {|model, values| model.query.merge(model.query(scope.call(values)))}
262
+ end
263
+
264
+ # ActiveModel's use of method_missing / respond_to for attribute methods
265
+ # breaks both ancestor lookups and defined?(super). Need to special-case
266
+ # the existence of query attribute methods.
267
+ def owner_class_ancestor_has_method?(scope, method)
268
+ scope == :instance && method == "#{name}?" || super
269
+ end
270
+ end
271
+ end
272
+ end