stator 0.0.13
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +17 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +203 -0
- data/Rakefile +9 -0
- data/gemfiles/ar30.gemfile +10 -0
- data/gemfiles/ar31.gemfile +10 -0
- data/gemfiles/ar32.gemfile +10 -0
- data/gemfiles/ar40.gemfile +10 -0
- data/lib/stator/alias.rb +81 -0
- data/lib/stator/integration.rb +82 -0
- data/lib/stator/machine.rb +123 -0
- data/lib/stator/model.rb +71 -0
- data/lib/stator/transition.rb +103 -0
- data/lib/stator/version.rb +8 -0
- data/lib/stator.rb +11 -0
- data/spec/model_spec.rb +214 -0
- data/spec/spec_helper.rb +34 -0
- data/spec/support/models.rb +177 -0
- data/spec/support/schema.rb +43 -0
- data/stator.gemspec +21 -0
- metadata +91 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
stator
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.9.3-p194
|
data/.travis.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
language: ruby
|
2
|
+
|
3
|
+
rvm:
|
4
|
+
- 1.8.7
|
5
|
+
- 1.9.3
|
6
|
+
- 2.0.0
|
7
|
+
|
8
|
+
gemfile:
|
9
|
+
- gemfiles/ar30.gemfile
|
10
|
+
- gemfiles/ar31.gemfile
|
11
|
+
- gemfiles/ar32.gemfile
|
12
|
+
- gemfiles/ar40.gemfile
|
13
|
+
|
14
|
+
matrix:
|
15
|
+
exclude:
|
16
|
+
- rvm: 1.8.7
|
17
|
+
gemfile: gemfiles/ar40.gemfile
|
data/Gemfile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in stator.gemspec
|
4
|
+
gem 'activerecord', '4.0.0'
|
5
|
+
|
6
|
+
gemspec
|
7
|
+
|
8
|
+
gem 'rake'
|
9
|
+
gem 'activerecord-nulldb-adapter', :require => false, :git => 'git@github.com:nulldb/nulldb.git', :ref => 'ffc7dae4697c6b9fb15bed9edca3acb1f00eb5f0'
|
10
|
+
gem 'rspec'
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Mike Nelson
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
# Stator
|
2
|
+
|
3
|
+
Stator is a minimalist's state machine. It's a simple dsl that uses existing ActiveRecord functionality to accomplish common state machine functionality. This is not a full-featured computer-science driven gem, it's a gem that covers the 98% of use cases that I've run into.
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
gem 'stator', github: 'mnelson/stator', tag: 'v0.0.1'
|
7
|
+
```
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
If you've used the state_machine it's a pretty similar dsl. You define your state machine, transitions, states, and your callbacks (if any). One difference is that stator assumes you've set your db column to the initial state.
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
class User < ActiveRecord::Base
|
15
|
+
extend Stator::Model
|
16
|
+
|
17
|
+
stator do
|
18
|
+
|
19
|
+
transition :semiactivate do
|
20
|
+
from :unactivated
|
21
|
+
to :semiactivated
|
22
|
+
end
|
23
|
+
|
24
|
+
transition :activate do
|
25
|
+
from :unactivated, :semiactivated
|
26
|
+
to :activated
|
27
|
+
end
|
28
|
+
|
29
|
+
transition :deactivate do
|
30
|
+
from any
|
31
|
+
to :deactivate
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
Then you use like this:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
u = User.new
|
42
|
+
u.state
|
43
|
+
# => 'unactivated'
|
44
|
+
u.persisted?
|
45
|
+
# => false
|
46
|
+
u.semiactivate
|
47
|
+
# => true
|
48
|
+
u.state
|
49
|
+
# => 'semiactivated'
|
50
|
+
u.persisted?
|
51
|
+
# => true
|
52
|
+
```
|
53
|
+
|
54
|
+
## Advanced Usage
|
55
|
+
|
56
|
+
The intention of stator was to avoid hijacking ActiveRecord or reinvent the wheel. You can conditionally validate, invoke callbacks, etc. via a conditional block - no magic:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
class User < ActiveRecord::Base
|
60
|
+
extend Stator::Model
|
61
|
+
|
62
|
+
stator field: :status, track: true do
|
63
|
+
|
64
|
+
transition :activate do
|
65
|
+
from :unactivated
|
66
|
+
to :activated
|
67
|
+
|
68
|
+
# conditions is a string condition which will ensure the state
|
69
|
+
# was one of the `from` states and is one of the `to` states.
|
70
|
+
conditional do |conditions|
|
71
|
+
validate :validate_user_ip_not_blacklisted, if: conditions
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
# conditions is a string condition which will ensure the state
|
77
|
+
# is one of the ones provided.
|
78
|
+
conditional :unactivated do |conditions|
|
79
|
+
validates :email, presence: true, unless: conditions
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
```
|
85
|
+
|
86
|
+
Within a transition, the `conditional` block accepts a `use_previous` option which tells the state checks to use the record's previous_changes rather than the current changes. This is especially useful for after_commit scenarios where the record's changes hash is cleared before the execution begins.
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
transition :activate do
|
90
|
+
from :unactivated
|
91
|
+
to :activated
|
92
|
+
|
93
|
+
conditional(use_previous: true) do |conditions|
|
94
|
+
after_commit :send_things, if: conditions
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
The instance has some convenience methods which are generated by the state machine:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
u = User.new
|
102
|
+
u.activated?
|
103
|
+
# => false
|
104
|
+
u.can_activate?
|
105
|
+
# => true
|
106
|
+
```
|
107
|
+
|
108
|
+
Note that asking if a transition can take place via `can_[transition_name]?` does not invoke validations. It simply determines whether the record is in a state which the transition can take place from.
|
109
|
+
|
110
|
+
|
111
|
+
The `track: true` option enables timekeeping of the state transition. It will try to set a field in the format of "state_field_at" before saving the record. For example, in the previous state machine the following would occur:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
u = User.new
|
115
|
+
u.activate
|
116
|
+
|
117
|
+
u.activated_status_at
|
118
|
+
# => (now)
|
119
|
+
```
|
120
|
+
|
121
|
+
You can have multiple state machines for your model:
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
|
125
|
+
class User < ActiveRecord::Base
|
126
|
+
extend Stator::Model
|
127
|
+
|
128
|
+
# initial state = asleep
|
129
|
+
stator do
|
130
|
+
# wake up
|
131
|
+
end
|
132
|
+
|
133
|
+
# initial state = incomplete
|
134
|
+
stator namespace: 'homework', field: 'homework_state' do
|
135
|
+
# get it done
|
136
|
+
end
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
|
141
|
+
If you need to access the state machine directly, you can do so via the class:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
User._stator(namespace)
|
145
|
+
```
|
146
|
+
|
147
|
+
#### Aliasing
|
148
|
+
|
149
|
+
It's a really common case to have a set of states evaluated as a single concept. For example, many apps have a concept of "active" users. You generally see something like this:
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
class User < ActiveRecord::Base
|
153
|
+
ACTIVE_STATES = %w(semiactivated activated)
|
154
|
+
|
155
|
+
scope :active, -> { where(state: ACTIVE_STATES) }
|
156
|
+
|
157
|
+
def active?
|
158
|
+
self.state.in?(ACTIVE_STATES)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
To this point, we're doing ok. But how about defining inactive as well? At this point things start getting a little dirtier since a change to ACTIVE_STATES should impact INACTIVE_STATES. For this reason, stator allows you to define state aliases:
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
class User < ActiveRecord::Base
|
167
|
+
extend Stator::Model
|
168
|
+
|
169
|
+
stator do
|
170
|
+
# forgoing state definitions...
|
171
|
+
|
172
|
+
state_alias :active do
|
173
|
+
is :semiactivated, :activated
|
174
|
+
opposite :inactive
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
```
|
179
|
+
|
180
|
+
The provided example will define an `active?` and `inactive?` method. If you want to create the constant and/or the scope, just pass them as options to the state_alias method:
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
# will generate a User::ACTIVE_STATES constant, User.active scope, and User#active? instance method
|
184
|
+
state_alias :active, scope: true, constant: true do
|
185
|
+
# ...
|
186
|
+
end
|
187
|
+
```
|
188
|
+
|
189
|
+
Passing `true` for the scope or constant will result in default naming conventions. You can pass your own names if you'd rather:
|
190
|
+
|
191
|
+
```ruby
|
192
|
+
# will generate a User::THE_ACTIVE_STATES constant, User.the_active_ones scope, and User#active? instance method
|
193
|
+
state_alias :active, scope: :the_active_ones, constant: :the_active_states do
|
194
|
+
# ...
|
195
|
+
end
|
196
|
+
```
|
197
|
+
|
198
|
+
The `opposite` method also accepts the scope and constant options, but does not yield to a block since the state definitions are inheritenly tied to the ones described in the parent state_alias block.
|
199
|
+
|
200
|
+
#### TODO
|
201
|
+
|
202
|
+
* Allow for multiple variations of a transition (shift_down style - :third_gear => :second_gear, :second_gear => :first_gear)
|
203
|
+
* Create adapters for different backends (not just ActiveRecord)
|
data/Rakefile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in stator.gemspec
|
4
|
+
gem 'activerecord', '4.0.0'
|
5
|
+
|
6
|
+
gemspec :path => '../'
|
7
|
+
|
8
|
+
gem 'rake'
|
9
|
+
gem 'activerecord-nulldb-adapter', :require => false, :github => 'nulldb/nulldb', :ref => 'ffc7dae4697c6b9fb15bed9edca3acb1f00eb5f0'
|
10
|
+
gem 'rspec'
|
data/lib/stator/alias.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
module Stator
|
2
|
+
class Alias
|
3
|
+
|
4
|
+
def initialize(machine, name, options = {})
|
5
|
+
@machine = machine
|
6
|
+
@name = name
|
7
|
+
@namespace = @machine.namespace
|
8
|
+
@full_name = [@namespace, @name].compact.join('_')
|
9
|
+
@states = []
|
10
|
+
@not = false
|
11
|
+
@opposite = nil
|
12
|
+
@constant = options[:constant]
|
13
|
+
@scope = options[:scope]
|
14
|
+
end
|
15
|
+
|
16
|
+
def is(*args)
|
17
|
+
@states |= args.map(&:to_s)
|
18
|
+
end
|
19
|
+
|
20
|
+
def is_not(*args)
|
21
|
+
@not = true
|
22
|
+
is(*args)
|
23
|
+
end
|
24
|
+
|
25
|
+
def opposite(*args)
|
26
|
+
@opposite = args
|
27
|
+
end
|
28
|
+
|
29
|
+
def evaluate
|
30
|
+
generate_methods
|
31
|
+
|
32
|
+
if @opposite
|
33
|
+
op = @machine.state_alias(*@opposite)
|
34
|
+
|
35
|
+
op.is(*@states) if @not
|
36
|
+
op.is_not(*@states) if !@not
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
|
42
|
+
def inferred_constant_name
|
43
|
+
[@full_name.upcase, @machine.field.to_s.pluralize.upcase].join('_')
|
44
|
+
end
|
45
|
+
|
46
|
+
def generate_methods
|
47
|
+
|
48
|
+
not_states = (@machine.states - @states)
|
49
|
+
|
50
|
+
if @scope
|
51
|
+
name = @scope == true ? @full_name : @scope
|
52
|
+
@machine.klass.class_eval <<-EV, __FILE__, __LINE__ + 1
|
53
|
+
scope #{name.inspect}, lambda {
|
54
|
+
where(_stator(#{@namespace.inspect}).field => #{(@not ? not_states : @states).inspect})
|
55
|
+
}
|
56
|
+
EV
|
57
|
+
end
|
58
|
+
|
59
|
+
if @constant
|
60
|
+
name = @constant == true ? inferred_constant_name : @constant.to_s.upcase
|
61
|
+
if @not
|
62
|
+
@machine.klass.class_eval <<-EV, __FILE__, __LINE__ + 1
|
63
|
+
#{name} = #{not_states.inspect}.freeze
|
64
|
+
EV
|
65
|
+
else
|
66
|
+
@machine.klass.class_eval <<-EV, __FILE__, __LINE__ + 1
|
67
|
+
#{name} = #{@states.inspect}.freeze
|
68
|
+
EV
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
@machine.klass.class_eval <<-EV, __FILE__, __LINE__ + 1
|
73
|
+
def #{@full_name}?
|
74
|
+
integration = _stator(#{@namespace.inspect}).integration(self)
|
75
|
+
#{(@not ? not_states : @states).inspect}.include?(integration.state)
|
76
|
+
end
|
77
|
+
EV
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Stator
|
2
|
+
class Integration
|
3
|
+
|
4
|
+
delegate :states, :to => :@machine
|
5
|
+
delegate :transitions, :to => :@machine
|
6
|
+
delegate :namespace, :to => :@machine
|
7
|
+
|
8
|
+
def initialize(machine, record)
|
9
|
+
@machine = machine
|
10
|
+
@record = record
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
|
16
|
+
def state=(new_value)
|
17
|
+
@record.send("#{@machine.field}=", new_value)
|
18
|
+
end
|
19
|
+
|
20
|
+
def state
|
21
|
+
@record.send(@machine.field)
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def state_was(use_previous = false)
|
26
|
+
if use_previous
|
27
|
+
@record.previous_changes[@machine.field.to_s].try(:[], 0)
|
28
|
+
else
|
29
|
+
@record.send("#{@machine.field}_was")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def state_changed?
|
35
|
+
@record.send("#{@machine.field}_changed?")
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
|
40
|
+
def validate_transition
|
41
|
+
return unless self.state_changed?
|
42
|
+
|
43
|
+
was = self.state_was
|
44
|
+
is = self.state
|
45
|
+
|
46
|
+
if @record.new_record?
|
47
|
+
unless @machine.matching_transition(::Stator::Transition::ANY, is)
|
48
|
+
@record.errors.add(@machine.field, "is not a valid state")
|
49
|
+
end
|
50
|
+
else
|
51
|
+
unless @machine.matching_transition(was, is)
|
52
|
+
@record.errors.add(@machine.field, "cannot transition to #{is.inspect} from #{was.inspect}")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def track_transition
|
58
|
+
self.attempt_to_track_state(self.state_was)
|
59
|
+
self.attempt_to_track_state(self.state)
|
60
|
+
|
61
|
+
true
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
protected
|
66
|
+
|
67
|
+
def attempt_to_track_state(state_to_track)
|
68
|
+
return unless state_to_track
|
69
|
+
|
70
|
+
field_name = "#{state_to_track}_#{@machine.field}_at"
|
71
|
+
|
72
|
+
return unless @record.respond_to?(field_name)
|
73
|
+
return unless @record.respond_to?("#{field_name}=")
|
74
|
+
|
75
|
+
unless @record.send(field_name)
|
76
|
+
@record.send("#{field_name}=", (Time.zone || Time).now)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module Stator
|
2
|
+
class Machine
|
3
|
+
|
4
|
+
attr_reader :initial_state
|
5
|
+
attr_reader :field
|
6
|
+
attr_reader :transition_names
|
7
|
+
attr_reader :transitions
|
8
|
+
attr_reader :states
|
9
|
+
attr_reader :namespace
|
10
|
+
|
11
|
+
|
12
|
+
def initialize(klass, options = {})
|
13
|
+
@class_name = klass.name
|
14
|
+
@field = options[:field] || :state
|
15
|
+
@namespace = options[:namespace] || nil
|
16
|
+
|
17
|
+
# rescue nil since the table may not exist yet.
|
18
|
+
@initial_state = klass.columns_hash[@field.to_s].default rescue nil
|
19
|
+
|
20
|
+
@transitions = []
|
21
|
+
@aliases = []
|
22
|
+
|
23
|
+
# pushed out into their own variables for performance reasons (AR integration can use method missing - see the HelperMethods module)
|
24
|
+
@transition_names = []
|
25
|
+
@states = [@initial_state].compact
|
26
|
+
|
27
|
+
@options = options
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
def integration(record)
|
32
|
+
::Stator::Integration.new(self, record)
|
33
|
+
end
|
34
|
+
|
35
|
+
def get_transition(name)
|
36
|
+
@transitions.detect{|t| t.name.to_s == name.to_s}
|
37
|
+
end
|
38
|
+
|
39
|
+
def transition(name, &block)
|
40
|
+
|
41
|
+
t = ::Stator::Transition.new(@class_name, name, @namespace)
|
42
|
+
t.instance_eval(&block) if block_given?
|
43
|
+
|
44
|
+
verify_transition_validity(t)
|
45
|
+
|
46
|
+
@transitions << t
|
47
|
+
@transition_names |= [t.full_name] unless t.full_name.blank?
|
48
|
+
@states |= [t.to_state] unless t.to_state.nil?
|
49
|
+
|
50
|
+
t
|
51
|
+
end
|
52
|
+
|
53
|
+
def state_alias(name, options = {}, &block)
|
54
|
+
a = ::Stator::Alias.new(self, name, options)
|
55
|
+
a.instance_eval(&block) if block_given?
|
56
|
+
@aliases << a
|
57
|
+
a
|
58
|
+
end
|
59
|
+
|
60
|
+
def state(name, &block)
|
61
|
+
transition(nil) do
|
62
|
+
from any
|
63
|
+
to name
|
64
|
+
instance_eval(&block) if block_given?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def conditional(*states, &block)
|
69
|
+
klass.instance_exec("#{states.map(&:to_s).inspect}.include?(self._stator(#{@namespace.inspect}).integration(self).state)", &block)
|
70
|
+
end
|
71
|
+
|
72
|
+
def matching_transition(from, to)
|
73
|
+
@transitions.detect do |transition|
|
74
|
+
transition.valid?(from, to)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def evaluate
|
79
|
+
@transitions.each(&:evaluate)
|
80
|
+
@aliases.each(&:evaluate)
|
81
|
+
generate_methods
|
82
|
+
end
|
83
|
+
|
84
|
+
def klass
|
85
|
+
@class_name.constantize
|
86
|
+
end
|
87
|
+
|
88
|
+
protected
|
89
|
+
|
90
|
+
def verify_transition_validity(transition)
|
91
|
+
verify_state_singularity_of_transition(transition)
|
92
|
+
verify_name_singularity_of_transition(transition)
|
93
|
+
end
|
94
|
+
|
95
|
+
def verify_state_singularity_of_transition(transition)
|
96
|
+
transition.from_states.each do |from|
|
97
|
+
if other = matching_transition(from, transition.to_state)
|
98
|
+
raise "[Stator] another transition already exists which moves #{@class_name} from #{from.inspect} to #{transition.to_state.inspect}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def verify_name_singularity_of_transition(transition)
|
104
|
+
if other = @transitions.detect{|other| transition.name && transition.name == other.name }
|
105
|
+
raise "[Stator] another transition already exists with the name of #{transition.name.inspect} in the #{@class_name} class"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def generate_methods
|
110
|
+
self.states.each do |state|
|
111
|
+
method_name = [@namespace, state].compact.join('_')
|
112
|
+
klass.class_eval <<-EV, __FILE__, __LINE__ + 1
|
113
|
+
def #{method_name}?
|
114
|
+
integration = self._stator(#{@namespace.inspect}).integration(self)
|
115
|
+
integration.state == #{state.to_s.inspect}
|
116
|
+
end
|
117
|
+
EV
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
data/lib/stator/model.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
module Stator
|
2
|
+
module Model
|
3
|
+
|
4
|
+
def stator(options = {}, &block)
|
5
|
+
|
6
|
+
class_attribute :_stators unless respond_to?(:_stators)
|
7
|
+
|
8
|
+
include InstanceMethods unless self.included_modules.include?(InstanceMethods)
|
9
|
+
include TrackerMethods if options[:track] == true
|
10
|
+
|
11
|
+
self._stators ||= {}
|
12
|
+
machine = (self._stators[options[:namespace].to_s] ||= ::Stator::Machine.new(self, options))
|
13
|
+
|
14
|
+
if block_given?
|
15
|
+
machine.instance_eval(&block)
|
16
|
+
machine.evaluate
|
17
|
+
end
|
18
|
+
|
19
|
+
machine
|
20
|
+
end
|
21
|
+
|
22
|
+
def _stator(namespace)
|
23
|
+
self._stators[namespace.to_s]
|
24
|
+
end
|
25
|
+
|
26
|
+
module TrackerMethods
|
27
|
+
|
28
|
+
def self.included(base)
|
29
|
+
base.class_eval do
|
30
|
+
before_save :_stator_track_transition
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
|
38
|
+
def _stator_track_transition
|
39
|
+
|
40
|
+
self._stators.each do |namespace, machine|
|
41
|
+
machine.integration(self).track_transition
|
42
|
+
end
|
43
|
+
|
44
|
+
true
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
module InstanceMethods
|
50
|
+
|
51
|
+
def self.included(base)
|
52
|
+
base.class_eval do
|
53
|
+
validate :_stator_validate_transition
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
def _stator_validate_transition
|
60
|
+
self._stators.each do |namespace, machine|
|
61
|
+
machine.integration(self).validate_transition
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def _stator(namespace)
|
66
|
+
self.class._stator(namespace)
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|