stator 0.0.13

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.
@@ -0,0 +1,103 @@
1
+ module Stator
2
+ class Transition
3
+
4
+ ANY = '__any__'
5
+
6
+ attr_reader :name
7
+ attr_reader :full_name
8
+
9
+ def initialize(class_name, name, namespace = nil)
10
+ @class_name = class_name
11
+ @name = name
12
+ @namespace = namespace
13
+ @full_name = [@namespace, @name].compact.join('_') if @name
14
+ @froms = []
15
+ @to = nil
16
+ @callbacks = {}
17
+ end
18
+
19
+ def from(*froms)
20
+ @froms |= froms.map{|f| f.try(:to_s) } # nils are ok
21
+ end
22
+
23
+ def to(to)
24
+ @to = to.to_s
25
+ end
26
+
27
+ def to_state
28
+ @to
29
+ end
30
+
31
+ def from_states
32
+ @froms
33
+ end
34
+
35
+ def can?(current_state)
36
+ @froms.include?(current_state) || @froms.include?(ANY) || current_state == ANY
37
+ end
38
+
39
+ def valid?(from, to)
40
+ can?(from) &&
41
+ (@to == to || @to == ANY || to == ANY)
42
+ end
43
+
44
+ def conditional(options = {}, &block)
45
+ klass.instance_exec(conditional_string(options), &block)
46
+ end
47
+
48
+ def any
49
+ ANY
50
+ end
51
+
52
+ def evaluate
53
+ generate_methods unless @full_name.blank?
54
+ end
55
+
56
+ protected
57
+
58
+ def klass
59
+ @class_name.constantize
60
+ end
61
+
62
+ def callbacks(kind)
63
+ @callbacks[kind] || []
64
+ end
65
+
66
+ def conditional_string(options = {})
67
+ options[:use_previous] ||= false
68
+ %Q{
69
+ (
70
+ #{@froms.inspect}.include?(self._stator(#{@namespace.inspect}).integration(self).state_was(#{options[:use_previous].inspect})) ||
71
+ #{@froms.inspect}.include?(::Stator::Transition::ANY)
72
+ ) && (
73
+ self._stator(#{@namespace.inspect}).integration(self).state == #{@to.inspect} ||
74
+ #{@to.inspect} == ::Stator::Transition::ANY
75
+ )
76
+ }
77
+ end
78
+
79
+ def generate_methods
80
+ klass.class_eval <<-EV, __FILE__, __LINE__ + 1
81
+ def #{@full_name}(should_save = true)
82
+ integration = self._stator(#{@namespace.inspect}).integration(self)
83
+ integration.state = #{@to.inspect}
84
+ self.save if should_save
85
+ end
86
+
87
+ def #{@full_name}!
88
+ integration = self._stator(#{@namespace.inspect}).integration(self)
89
+ integration.state = #{@to.inspect}
90
+ self.save!
91
+ end
92
+
93
+ def can_#{@full_name}?
94
+ machine = self._stator(#{@namespace.inspect})
95
+ integration = machine.integration(self)
96
+ transition = machine.transitions.detect{|t| t.full_name.to_s == #{@full_name.inspect}.to_s }
97
+ transition.can?(integration.state)
98
+ end
99
+ EV
100
+ end
101
+
102
+ end
103
+ end
@@ -0,0 +1,8 @@
1
+ module Stator
2
+ MAJOR = 0
3
+ MINOR = 0
4
+ PATCH = 13
5
+ PRERELEASE = nil
6
+
7
+ VERSION = [MAJOR, MINOR, PATCH, PRERELEASE].compact.join('.')
8
+ end
data/lib/stator.rb ADDED
@@ -0,0 +1,11 @@
1
+ require "stator/version"
2
+
3
+ module Stator
4
+
5
+ autoload :Alias, 'stator/alias'
6
+ autoload :Integration, 'stator/integration'
7
+ autoload :Machine, 'stator/machine'
8
+ autoload :Model, 'stator/model'
9
+ autoload :Transition, 'stator/transition'
10
+
11
+ end
@@ -0,0 +1,214 @@
1
+ require 'spec_helper'
2
+
3
+ describe Stator::Model do
4
+
5
+ it 'should set the default state after initialization' do
6
+ u = User.new
7
+ u.state.should eql('pending')
8
+ end
9
+
10
+ it 'should see the initial setting of the state as a change with the initial state as the previous value' do
11
+ u = User.new
12
+ u.state = 'activated'
13
+ u.state_was.should eql('pending')
14
+ end
15
+
16
+ it 'should not obstruct normal validations' do
17
+ u = User.new
18
+ u.should_not be_valid
19
+ u.errors[:email].grep(/length/).should_not be_empty
20
+ end
21
+
22
+ it 'should ensure a valid state transition when given a bogus state' do
23
+ u = User.new
24
+ u.state = 'anythingelse'
25
+
26
+ u.should_not be_valid
27
+ u.errors[:state].should eql(['is not a valid state'])
28
+ end
29
+
30
+ it 'should allow creation at any state' do
31
+ u = User.new(:email => 'doug@example.com')
32
+ u.state = 'hyperactivated'
33
+
34
+ u.should be_valid
35
+ end
36
+
37
+ it 'should ensure a valid state transition when given an illegal state based on the current state' do
38
+ u = User.new
39
+ u.stub(:new_record?).and_return(false)
40
+ u.state = 'hyperactivated'
41
+
42
+ u.should_not be_valid
43
+ u.errors[:state].should_not be_empty
44
+
45
+ end
46
+
47
+ it 'should run conditional validations' do
48
+ u = User.new
49
+ u.state = 'semiactivated'
50
+ u.should_not be_valid
51
+
52
+ u.errors[:state].should be_empty
53
+ u.errors[:email].grep(/format/).should_not be_empty
54
+ end
55
+
56
+ it 'should invoke callbacks' do
57
+ u = User.new(:activated => true, :email => 'doug@example.com', :name => 'doug')
58
+ u.activated.should == true
59
+
60
+ u.deactivate
61
+
62
+ u.activated.should == false
63
+ u.state.should eql('deactivated')
64
+ u.activated_state_at.should be_nil
65
+ u.should be_persisted
66
+ end
67
+
68
+ it 'should blow up if the record is invalid and a bang method is used' do
69
+ u = User.new(:email => 'doug@other.com', :name => 'doug')
70
+ lambda{
71
+ u.activate!
72
+ }.should raise_error(ActiveRecord::RecordInvalid)
73
+ end
74
+
75
+ it 'should allow for other fields to be used other than state' do
76
+ a = Animal.new
77
+ a.should be_valid
78
+
79
+ a.birth!
80
+ end
81
+
82
+ it 'should create implicit transitions for state declarations' do
83
+ a = Animal.new
84
+ a.should_not be_grown_up
85
+ a.status = 'grown_up'
86
+ a.save
87
+ end
88
+
89
+ it 'should allow multiple machines in the same model' do
90
+ f = Farm.new
91
+ f.should be_dirty
92
+ f.should be_house_dirty
93
+
94
+ f.cleanup
95
+
96
+ f.should_not be_dirty
97
+ f.should be_house_dirty
98
+
99
+ f.house_cleanup
100
+
101
+ f.should_not be_house_dirty
102
+ end
103
+
104
+ it 'should allow saving to be skipped' do
105
+ f = Farm.new
106
+ f.cleanup(false)
107
+
108
+ f.should_not be_persisted
109
+ end
110
+
111
+ it 'should allow no initial state' do
112
+ f = Factory.new
113
+ f.state.should be_nil
114
+
115
+ f.construct.should be_true
116
+
117
+ f.state.should eql('constructed')
118
+ end
119
+
120
+ describe 'helper methods' do
121
+
122
+ it 'should answer the question of whether the state is currently the one invoked' do
123
+ a = Animal.new
124
+ a.should be_unborn
125
+ a.should_not be_born
126
+
127
+ a.birth
128
+
129
+ a.should be_born
130
+ a.should_not be_unborn
131
+ end
132
+
133
+ it 'should determine if it can validly execute a transition' do
134
+ a = Animal.new
135
+ a.can_birth?.should be_true
136
+
137
+ a.birth
138
+
139
+ a.can_birth?.should be_false
140
+ end
141
+
142
+ end
143
+
144
+ describe 'tracker methods' do
145
+
146
+ before do
147
+ Time.zone = 'Eastern Time (US & Canada)'
148
+ end
149
+
150
+ it 'should store when a record changed state for the first time' do
151
+ a = Animal.new
152
+ a.unborn_status_at.should be_nil
153
+ a.born_status_at.should be_nil
154
+ a.birth
155
+ a.unborn_status_at.should be_within(1).of(Time.zone.now)
156
+ a.born_status_at.should be_within(1).of(Time.zone.now)
157
+ end
158
+
159
+ end
160
+
161
+ describe 'aliasing' do
162
+ it 'should allow aliasing within the dsl' do
163
+ u = User.new(:email => 'doug@example.com')
164
+ u.should respond_to(:active?)
165
+ u.should respond_to(:inactive?)
166
+
167
+ u.should_not be_active
168
+
169
+ u.inactive?
170
+ u.should be_inactive
171
+
172
+ u.activate!
173
+ u.should be_active
174
+ u.should_not be_inactive
175
+
176
+ u.hyperactivate!
177
+ u.should be_active
178
+ u.should_not be_inactive
179
+
180
+ User::ACTIVE_STATES.should eql(['activated', 'hyperactivated'])
181
+ User::INACTIVE_STATES.should eql(['pending', 'deactivated', 'semiactivated'])
182
+
183
+ u2 = User.create(:email => 'phil@example.com')
184
+
185
+ User.active.to_sql.gsub(' ', ' ').should eq("SELECT users.* FROM users WHERE users.state IN ('activated', 'hyperactivated')")
186
+ User.inactive.to_sql.gsub(' ', ' ').should eq("SELECT users.* FROM users WHERE users.state IN ('pending', 'deactivated', 'semiactivated')")
187
+ end
188
+
189
+ it 'should namespace aliases just like everything else' do
190
+ f = Farm.new
191
+ f.should respond_to(:house_cleaned?)
192
+
193
+ f.should_not be_house_cleaned
194
+ f.house_cleanup!
195
+
196
+ f.should be_house_cleaned
197
+ end
198
+
199
+ it 'should allow for explicit constant and scope names to be provided' do
200
+ User.should respond_to(:luke_warmers)
201
+ defined?(User::LUKE_WARMERS).should be_true
202
+ u = User.new
203
+ u.should respond_to(:luke_warm?)
204
+ end
205
+
206
+ it 'should not create constants or scopes by default' do
207
+ u = User.new
208
+ u.should respond_to(:iced_tea?)
209
+ defined?(User::ICED_TEA_STATES).should be_false
210
+ User.should_not respond_to(:iced_tea)
211
+ end
212
+ end
213
+
214
+ end
@@ -0,0 +1,34 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+
8
+ require 'active_record'
9
+ require 'nulldb/core'
10
+ require 'active_support/core_ext'
11
+ require 'stator'
12
+
13
+ RSpec.configure do |config|
14
+ config.treat_symbols_as_metadata_keys_with_true_values = true
15
+ config.run_all_when_everything_filtered = true
16
+ config.filter_run :focus
17
+
18
+ NullDB.configure do |c|
19
+ c.project_root = File.dirname(__FILE__)
20
+ end
21
+
22
+ ActiveRecord::Base.establish_connection(
23
+ :adapter => :nulldb,
24
+ :schema => 'support/schema.rb'
25
+ )
26
+
27
+ require 'support/models'
28
+
29
+ # Run specs in random order to surface order dependencies. If you find an
30
+ # order dependency and want to debug it, you can fix the order by providing
31
+ # the seed, which is printed after each run.
32
+ # --seed 1234
33
+ config.order = 'random'
34
+ end
@@ -0,0 +1,177 @@
1
+ class User < ActiveRecord::Base
2
+ extend Stator::Model
3
+
4
+
5
+ # initial state = pending
6
+ stator do
7
+
8
+ transition :activate do
9
+ from :pending, :semiactivated
10
+ to :activated
11
+ end
12
+
13
+ transition :deactivate do
14
+ from any
15
+ to :deactivated
16
+
17
+ conditional do |condition|
18
+ before_save :set_deactivated, :if => condition
19
+ end
20
+ end
21
+
22
+ transition :semiactivate do
23
+ from :pending
24
+ to :semiactivated
25
+
26
+ conditional do |condition|
27
+ validate :check_email_validity, :if => condition
28
+ end
29
+ end
30
+
31
+ transition :hyperactivate do
32
+ from :activated
33
+ to :hyperactivated
34
+ end
35
+
36
+ conditional :semiactivated, :activated do |condition|
37
+ validate :check_email_presence, :if => condition
38
+ end
39
+
40
+ state_alias :active, :constant => true, :scope => true do
41
+ is :activated, :hyperactivated
42
+ opposite :inactive, :constant => true, :scope => true
43
+ end
44
+
45
+ state_alias :luke_warm, :constant => :luke_warmers, :scope => :luke_warmers do
46
+ is :semiactivated
47
+ opposite :iced_tea
48
+ end
49
+
50
+ end
51
+
52
+ validate :email_is_right_length
53
+
54
+ protected
55
+
56
+ def check_email_presence
57
+ unless self.email.present?
58
+ self.errors.add(:email, 'needs to be present')
59
+ return false
60
+ end
61
+
62
+ true
63
+ end
64
+
65
+ def check_email_validity
66
+ unless self.email.to_s =~ /example\.com$/
67
+ self.errors.add(:email, 'format needs to be example.com')
68
+ return false
69
+ end
70
+
71
+ true
72
+ end
73
+
74
+ def email_is_right_length
75
+ unless self.email.to_s.length == 'four@example.com'.length
76
+ self.errors.add(:email, 'needs to be the right length')
77
+ return false
78
+ end
79
+
80
+ true
81
+ end
82
+
83
+ def set_deactivated
84
+ self.activated = false
85
+ true
86
+ end
87
+
88
+ end
89
+
90
+ class Animal < ActiveRecord::Base
91
+ extend Stator::Model
92
+
93
+ # initial state = unborn
94
+ stator :field => :status, :helpers => true, :track => true do
95
+
96
+ transition :birth do
97
+ from :unborn
98
+ to :born
99
+ end
100
+
101
+ state :grown_up
102
+
103
+ end
104
+ end
105
+
106
+ class Zoo < ActiveRecord::Base
107
+ extend Stator::Model
108
+
109
+ # initial state = closed
110
+ stator do
111
+
112
+ transition :open do
113
+ from :closed
114
+ to :opened
115
+ end
116
+
117
+ transition :close do
118
+ from :opened
119
+ to :closed
120
+ end
121
+
122
+ conditional :opened do |c|
123
+ validate :validate_lights_are_on, :if => c
124
+ end
125
+ end
126
+
127
+ protected
128
+
129
+ def validate_lights_are_on
130
+ true
131
+ end
132
+ end
133
+
134
+
135
+ class Farm < ActiveRecord::Base
136
+ extend Stator::Model
137
+
138
+ # initial state = dirty
139
+ stator do
140
+ transition :cleanup do
141
+ from :dirty
142
+ to :clean
143
+ end
144
+ end
145
+
146
+
147
+ # initial state = dirty
148
+ stator :field => 'house_state', :namespace => 'house' do
149
+ transition :cleanup do
150
+ from :dirty
151
+ to :clean
152
+ end
153
+
154
+ state_alias :cleaned do
155
+ is_not :dirty
156
+ end
157
+ end
158
+
159
+ end
160
+
161
+ class Factory < ActiveRecord::Base
162
+ extend Stator::Model
163
+
164
+ # initial state = nil
165
+ stator do
166
+ transition :construct do
167
+ from nil
168
+ to :constructed
169
+ end
170
+
171
+ transition :destruct do
172
+ from :constructed
173
+ to :on_the_ground
174
+ end
175
+ end
176
+
177
+ end
@@ -0,0 +1,43 @@
1
+ ActiveRecord::Schema.define(:version => 20130628161227) do
2
+
3
+ create_table "users", :force => true do |t|
4
+ t.string "name"
5
+ t.string "email"
6
+ t.string "state", :default => 'pending'
7
+ t.boolean "activated", :default => true
8
+ t.datetime "created_at", :null => false
9
+ t.datetime "updated_at", :null => false
10
+ t.datetime "semiactivated_state_at"
11
+ t.datetime "activated_state_at"
12
+ end
13
+
14
+ create_table "animals", :force => true do |t|
15
+ t.string "name"
16
+ t.string "status", :default => 'unborn'
17
+ t.datetime "created_at", :null => false
18
+ t.datetime "updated_at", :null => false
19
+ t.datetime "unborn_status_at"
20
+ t.datetime "born_status_at"
21
+ end
22
+
23
+ create_table "zoos", :force => true do |t|
24
+ t.string "name"
25
+ t.string "state", :default => 'closed'
26
+ t.datetime "created_at", :null => false
27
+ t.datetime "updated_at", :null => false
28
+ end
29
+
30
+ create_table "farms", :force => true do |t|
31
+ t.string "name"
32
+ t.string "state", :default => 'dirty'
33
+ t.string "house_state", :default => 'dirty'
34
+ t.datetime "created_at", :null => false
35
+ t.datetime "updated_at", :null => false
36
+ end
37
+
38
+ create_table "factories", :force => true do |t|
39
+ t.string "name"
40
+ t.string "state"
41
+ end
42
+
43
+ end
data/stator.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'stator/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "stator"
8
+ gem.version = Stator::VERSION
9
+ gem.authors = ["Mike Nelson"]
10
+ gem.email = ["mike@mikeonrails.com"]
11
+ gem.description = %q{The simplest of ActiveRecord state machines. Intended to be lightweight and minimalistic.}
12
+ gem.summary = %q{The simplest of ActiveRecord state machines}
13
+ gem.homepage = "http://www.mikeonrails.com"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency 'activerecord'
21
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.13
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Mike Nelson
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-03-22 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: The simplest of ActiveRecord state machines. Intended to be lightweight
31
+ and minimalistic.
32
+ email:
33
+ - mike@mikeonrails.com
34
+ executables: []
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - .gitignore
39
+ - .rspec
40
+ - .ruby-gemset
41
+ - .ruby-version
42
+ - .travis.yml
43
+ - Gemfile
44
+ - LICENSE.txt
45
+ - README.md
46
+ - Rakefile
47
+ - gemfiles/ar30.gemfile
48
+ - gemfiles/ar31.gemfile
49
+ - gemfiles/ar32.gemfile
50
+ - gemfiles/ar40.gemfile
51
+ - lib/stator.rb
52
+ - lib/stator/alias.rb
53
+ - lib/stator/integration.rb
54
+ - lib/stator/machine.rb
55
+ - lib/stator/model.rb
56
+ - lib/stator/transition.rb
57
+ - lib/stator/version.rb
58
+ - spec/model_spec.rb
59
+ - spec/spec_helper.rb
60
+ - spec/support/models.rb
61
+ - spec/support/schema.rb
62
+ - stator.gemspec
63
+ homepage: http://www.mikeonrails.com
64
+ licenses: []
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ! '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubyforge_project:
83
+ rubygems_version: 1.8.25
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: The simplest of ActiveRecord state machines
87
+ test_files:
88
+ - spec/model_spec.rb
89
+ - spec/spec_helper.rb
90
+ - spec/support/models.rb
91
+ - spec/support/schema.rb