/
sequel.rb
195 lines (188 loc) · 6.85 KB
/
sequel.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
module StateMachine
module Integrations #:nodoc:
# Adds support for integrating state machines with Sequel models.
#
# == Examples
#
# Below is an example of a simple state machine defined within a
# Sequel model:
#
# class Vehicle < Sequel::Model
# state_machine :initial => :parked do
# event :ignite do
# transition :parked => :idling
# end
# end
# end
#
# The examples in the sections below will use the above class as a
# reference.
#
# == Actions
#
# By default, the action that will be invoked when a state is transitioned
# is the +save+ action. This will cause the resource to save the changes
# made to the state machine's attribute. *Note* that if any other changes
# were made to the resource prior to transition, then those changes will
# be made as well.
#
# For example,
#
# vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state="parked">
# vehicle.name = 'Ford Explorer'
# vehicle.ignite # => true
# vehicle.refresh # => #<Vehicle id=1 name="Ford Explorer" state="idling">
#
# == Transactions
#
# In order to ensure that any changes made during transition callbacks
# are rolled back during a failed attempt, every transition is wrapped
# within a transaction.
#
# For example,
#
# class Message < Sequel::Model
# end
#
# Vehicle.state_machine do
# before_transition do |transition|
# Message.create(:content => transition.inspect)
# false
# end
# end
#
# vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state="parked">
# vehicle.ignite # => false
# Message.count # => 0
#
# *Note* that only before callbacks that halt the callback chain and
# failed attempts to save the record will result in the transaction being
# rolled back. If an after callback halts the chain, the previous result
# still applies and the transaction is *not* rolled back.
#
# == Validation errors
#
# If an event fails to successfully fire because there are no matching
# transitions for the current record, a validation error is added to the
# record's state attribute to help in determining why it failed and for
# reporting via the UI.
#
# For example,
#
# vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle id=1 name=nil state="idling">
# vehicle.ignite # => false
# vehicle.errors.full_messages # => ["state cannot be transitioned via :ignite from :idling"]
#
# If an event fails to fire because of a validation error on the record and
# *not* because a matching transition was not available, no error messages
# will be added to the state attribute.
#
# == Scopes
#
# To assist in filtering models with specific states, a series of class
# methods are defined on the model for finding records with or without a
# particular set of states.
#
# These named scopes are the functional equivalent of the following
# definitions:
#
# class Vehicle < Sequel::Model
# class << self
# def with_states(*states)
# filter(:state => states)
# end
# alias_method :with_state, :with_states
#
# def without_states(*states)
# filter(~{:state => states})
# end
# alias_method :without_state, :without_states
# end
# end
#
# *Note*, however, that the states are converted to their stored values
# before being passed into the query.
#
# Because of the way scopes work in Sequel, they can be chained like so:
#
# Vehicle.with_state(:parked).order(:id.desc)
#
# == Callbacks
#
# All before/after transition callbacks defined for Sequel resources
# behave in the same way that other Sequel hooks behave. Rather than
# passing in the record as an argument to the callback, the callback is
# instead bound to the object and evaluated within its context.
#
# For example,
#
# class Vehicle < Sequel::Model
# state_machine :initial => :parked do
# before_transition any => :idling do
# put_on_seatbelt
# end
#
# before_transition do |transition|
# # log message
# end
#
# event :ignite do
# transition :parked => :idling
# end
# end
#
# def put_on_seatbelt
# ...
# end
# end
#
# Note, also, that the transition can be accessed by simply defining
# additional arguments in the callback block.
module Sequel
# Should this integration be used for state machines in the given class?
# Classes that include Sequel::Model will automatically use the Sequel
# integration.
def self.matches?(klass)
defined?(::Sequel::Model) && klass <= ::Sequel::Model
end
# Adds a validation error to the given object after failing to fire a
# specific event
def invalidate(object, event)
object.errors.add(attribute, invalid_message(object, event))
end
# Runs a new database transaction, rolling back any changes if the
# yielded block fails (i.e. returns false).
def within_transaction(object)
object.db.transaction {raise ::Sequel::Error::Rollback unless yield}
end
protected
# Sets the default action for all Sequel state machines to +save+
def default_action
:save
end
# Skips defining reader/writer methods since this is done automatically
def define_attribute_accessor
end
# Creates a scope for finding records *with* a particular state or
# states for the attribute
def create_with_scope(name)
attribute = self.attribute
lambda {|model, values| model.filter(attribute.to_sym => values)}
end
# Creates a scope for finding records *without* a particular state or
# states for the attribute
def create_without_scope(name)
attribute = self.attribute
lambda {|model, values| model.filter(~{attribute.to_sym => values})}
end
# Creates a new callback in the callback chain, always ensuring that
# it's configured to bind to the object as this is the convention for
# Sequel callbacks
def add_callback(type, options, &block)
options[:bind_to_object] = true
options[:terminator] = @terminator ||= lambda {|result| result == false}
super
end
end
end
end