super_state 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 [name of plugin creator]
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,64 @@
1
+ SuperState
2
+ ==========
3
+
4
+ Super Simple state machine for Ruby and Rails
5
+ (designed for ActiveRecord, but hopefully easily extended to any ActiveModel)
6
+
7
+ Example
8
+ =======
9
+
10
+ a simple multi-state model
11
+
12
+ class MyModel < ActiveRecord::Base
13
+
14
+ include SuperState
15
+
16
+ super_state :pending, :initial => true
17
+ super_state :processing
18
+ super_state :completed
19
+ super_state :failed
20
+
21
+ super_state_group :outstanding, ["pending", "processing"]
22
+
23
+ # first part of a two stage transition
24
+ # eg.
25
+ # def process
26
+ # start_processing!
27
+ # do_the_stuff
28
+ # complete_processing!
29
+ # end
30
+ #
31
+ state_transition :start_processing, :pending => :processing
32
+
33
+ # second part of a two stage transition
34
+ state_transition :complete_processing, :processing => :completed
35
+
36
+ # transition direct from pending to complete
37
+ state_transition :complete, :pending => :completed
38
+
39
+ # failed to process
40
+ state_transition :fail, :processing => :failed
41
+
42
+ # back to processing
43
+ state_transition :restart, :failed => :processing
44
+
45
+ end
46
+
47
+ we can also take arguments in our transitions
48
+
49
+ class Loan < ActiveRecord::Base
50
+
51
+ include SuperState
52
+
53
+ super_state :requested, :initial => true
54
+ super_state :disbursed
55
+
56
+ state_transition :disburse, :requested => :disbursed do |params|
57
+ self.disbursed_amount = params[:amount]
58
+ self.disbursed_date = params[:disbursed_date]
59
+ end
60
+
61
+ end
62
+
63
+
64
+ Copyright (c) 2010 [Matthew Rudy Jacobs], released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,97 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test SuperState.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.libs << 'test'
12
+ t.pattern = 'test/**/*_test.rb'
13
+ t.verbose = true
14
+ end
15
+
16
+ desc 'Generate documentation for the super_slate plugin.'
17
+ Rake::RDocTask.new(:rdoc) do |rdoc|
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = 'SuperState'
20
+ rdoc.options << '--line-numbers' << '--inline-source'
21
+ rdoc.rdoc_files.include('README')
22
+ rdoc.rdoc_files.include('lib/**/*.rb')
23
+ end
24
+
25
+ require "rubygems"
26
+ require "rake/gempackagetask"
27
+ require "rake/rdoctask"
28
+
29
+ require "rake/testtask"
30
+ Rake::TestTask.new do |t|
31
+ t.libs << "test"
32
+ t.test_files = FileList["test/**/*_test.rb"]
33
+ t.verbose = true
34
+ end
35
+
36
+
37
+ task :default => ["test"]
38
+
39
+ # This builds the actual gem. For details of what all these options
40
+ # mean, and other ones you can add, check the documentation here:
41
+ #
42
+ # http://rubygems.org/read/chapter/20
43
+ #
44
+ spec = Gem::Specification.new do |s|
45
+
46
+ # Change these as appropriate
47
+ s.name = "super_state"
48
+ s.version = "0.1.0"
49
+ s.summary = "Super Simple State Machine"
50
+ s.author = "Matthew Rudy Jacobs"
51
+ s.email = "MatthewRudyJacobs@gmail.com"
52
+ s.homepage = "http://github.com/matthewrudy/super_state"
53
+
54
+ s.has_rdoc = true
55
+ s.extra_rdoc_files = %w(README)
56
+ s.rdoc_options = %w(--main README)
57
+
58
+ # Add any extra files to include in the gem
59
+ s.files = %w(MIT-LICENSE Rakefile README) + Dir.glob("{test,lib}/**/*")
60
+ s.require_paths = ["lib"]
61
+
62
+ # If you want to depend on other gems, add them here, along with any
63
+ # relevant versions
64
+ # s.add_dependency("some_other_gem", "~> 0.1.0")
65
+
66
+ # If your tests use any gems, include them here
67
+ # s.add_development_dependency("mocha") # for example
68
+ end
69
+
70
+ # This task actually builds the gem. We also regenerate a static
71
+ # .gemspec file, which is useful if something (i.e. GitHub) will
72
+ # be automatically building a gem for this project. If you're not
73
+ # using GitHub, edit as appropriate.
74
+ #
75
+ # To publish your gem online, install the 'gemcutter' gem; Read more
76
+ # about that here: http://gemcutter.org/pages/gem_docs
77
+ Rake::GemPackageTask.new(spec) do |pkg|
78
+ pkg.gem_spec = spec
79
+ end
80
+
81
+ desc "Build the gemspec file #{spec.name}.gemspec"
82
+ task :gemspec do
83
+ file = File.dirname(__FILE__) + "/#{spec.name}.gemspec"
84
+ File.open(file, "w") {|f| f << spec.to_ruby }
85
+ end
86
+
87
+ # If you don't want to generate the .gemspec file, just remove this line. Reasons
88
+ # why you might want to generate a gemspec:
89
+ # - using bundler with a git source
90
+ # - building the gem without rake (i.e. gem build blah.gemspec)
91
+ # - maybe others?
92
+ task :package => :gemspec
93
+
94
+ desc 'Clear out RDoc and generated packages'
95
+ task :clean => [:clobber_rdoc, :clobber_package] do
96
+ rm "#{spec.name}.gemspec"
97
+ end
@@ -0,0 +1,240 @@
1
+ module SuperState
2
+
3
+ def self.included(klass)
4
+ klass.class_eval do
5
+ cattr_accessor :initial_super_state
6
+ cattr_accessor :__super_states
7
+ cattr_accessor :super_state_groups
8
+ self.__super_states = StateArray.new.all_states!
9
+ self.super_state_groups = StateGroups.new
10
+
11
+ extend ClassMethods
12
+ include InstanceMethods
13
+
14
+ # the initial_state only takes effect when we say record.valid?
15
+ before_validation :set_initial_super_state, :on => :create
16
+ end
17
+ end
18
+
19
+ # states should be stored as strings in all cases
20
+ # (if it needs to be a sym, we should explicitly ask it to be)
21
+ def self.canonicalise(value)
22
+ if value
23
+ if value.is_a?(Array)
24
+ value.map{|v| self.canonicalise(v)}
25
+ else
26
+ value.to_s
27
+ end
28
+ end
29
+ end
30
+
31
+ # I want to know the states are the same
32
+ class StateArray < Array
33
+
34
+ def initialize(array=nil)
35
+ super(SuperState.canonicalise(Array(array)))
36
+ end
37
+
38
+ def <<(value)
39
+ super(SuperState.canonicalise(value))
40
+ end
41
+
42
+ def include?(value)
43
+ super(SuperState.canonicalise(value))
44
+ end
45
+
46
+ def for_select
47
+ self.map do |value|
48
+ [value.humanize, value]
49
+ end
50
+ end
51
+
52
+ def all_states!
53
+ @all_states = true
54
+ self
55
+ end
56
+
57
+ def all_states?
58
+ @all_states
59
+ end
60
+
61
+ end
62
+
63
+ class StateGroups < ActiveSupport::OrderedHash
64
+
65
+ def []=(key, value)
66
+ super(SuperState.canonicalise(key), SuperState.canonicalise(value))
67
+ end
68
+
69
+ def [](key)
70
+ super(SuperState.canonicalise(key))
71
+ end
72
+
73
+ def keys
74
+ StateArray.new(super)
75
+ end
76
+
77
+ end
78
+
79
+ module InstanceMethods
80
+
81
+ def current_super_state
82
+ self[self.class.super_state_column] || SuperState.canonicalise(self.class.initial_super_state)
83
+ end
84
+
85
+ def set_initial_super_state
86
+ set_super_state(self.class.initial_super_state)
87
+ end
88
+
89
+ def set_super_state(state_name, set_timestamps=true)
90
+ self[self.class.super_state_column] = SuperState.canonicalise(state_name)
91
+
92
+ if set_timestamps && self.respond_to?("#{state_name}_at=")
93
+ self.send("#{state_name}_at=", Time.now)
94
+ end
95
+
96
+ self.current_super_state
97
+ end
98
+
99
+ def human_super_state
100
+ self.current_super_state.humanize
101
+ end
102
+
103
+ end
104
+
105
+ class BadState < ArgumentError ; end
106
+
107
+ module ClassMethods
108
+
109
+ def super_states_for_select(options={})
110
+ rtn = []
111
+ if options[:include_all]
112
+ rtn << ["<All>", "all"]
113
+ end
114
+ if options[:include_groups]
115
+ rtn += self.super_state_groups.keys.for_select
116
+ end
117
+ rtn += self.super_states.for_select
118
+
119
+ rtn
120
+ end
121
+
122
+ def super_state(state_name, options={})
123
+ if options[:initial] || self.initial_super_state.nil?
124
+ self.initial_super_state = state_name
125
+ end
126
+ define_state_methods(state_name)
127
+ self.__super_states << state_name
128
+ end
129
+
130
+ # a wrapper around state based scopes
131
+ # to avoid malicious arguments;
132
+ # Loan.in_state(:disbursed) => Loan.disbursed
133
+ # Loan.in_state("active") => Loan.active
134
+ # Loan.in_state("all") => Loan.scoped
135
+ # Loan.in_state("") => Loan.scoped
136
+ # Loan.in_state("something_evil") => Exception!
137
+ def in_state(state_name)
138
+ if state_array = self.super_states(state_name)
139
+ self.scope_by_super_state(state_array)
140
+ else
141
+ raise BadState, "you are trying to scope by something other than a super state (or super state group)"
142
+ end
143
+ end
144
+
145
+ # self.super_states => ["first", "second", "third"]
146
+ # self.super_states("") => ["first", "second", "third"]
147
+ # self.super_states("all") => ["first", "second", "third"]
148
+ #
149
+ # self.super_states("first") => ["first"]
150
+ #
151
+ # self.super_states("final") => ["second", "third"]
152
+ #
153
+ def super_states(state_name=nil)
154
+ if state_name.blank? || state_name == "all"
155
+ self.__super_states
156
+ elsif self.__super_states.include?(state_name)
157
+ StateArray.new(state_name)
158
+ elsif self.super_state_groups.include?(state_name)
159
+ self.super_state_groups[state_name]
160
+ end
161
+ end
162
+
163
+ def scope_by_super_state(state_names)
164
+ if state_names.is_a?(StateArray) && state_names.all_states?
165
+ self.scoped
166
+ else
167
+ self.where(self.super_state_column => SuperState.canonicalise(state_names))
168
+ end
169
+ end
170
+
171
+ # super_state_group(:active, [:approved, :requested, :disbursed])
172
+ def super_state_group(group_name, group_states)
173
+ define_state_methods(group_name, group_states)
174
+ self.super_state_groups[group_name] = group_states
175
+ end
176
+
177
+ # state_transition :complete, :processing => :completed
178
+ def state_transition(transition_name, state_hash, &transition_block)
179
+ state_hash = state_hash.stringify_keys
180
+
181
+ define_state_transition_method(transition_name, state_hash, :save, &transition_block)
182
+ define_state_transition_method("#{transition_name}!", state_hash, :save!, &transition_block)
183
+ end
184
+
185
+ # internal methods
186
+
187
+ def super_state_column
188
+ :status
189
+ end
190
+
191
+ def define_state_methods(method_name, state_names=nil)
192
+ state_names ||= method_name
193
+
194
+ # Loan.completed # scope
195
+ self.scope method_name, self.scope_by_super_state(state_names)
196
+
197
+ # pretty much;
198
+ # def record.completed?
199
+ # record.status == "completed"
200
+ # end
201
+ define_method("#{method_name}?") do
202
+ StateArray.new(state_names).include?(self.current_super_state)
203
+ end
204
+ end
205
+
206
+ def define_state_transition_method(method_name, state_hash, save_method, &transition_block)
207
+
208
+ # pretty much;
209
+ # def record.complete!
210
+ # if record.pending?
211
+ # record.status = "completed"
212
+ # record.save!
213
+ # else
214
+ # raise SuperState::BadState
215
+ # end
216
+ # end
217
+ define_method(method_name) do |*args|
218
+ if to_state = state_hash[self.current_super_state]
219
+
220
+ state_before = self.current_super_state
221
+ self.set_super_state(to_state)
222
+
223
+ if transition_block
224
+ params = args.shift || {}
225
+ self.instance_exec(params, &transition_block)
226
+ end
227
+
228
+ unless rtn = self.send(save_method)
229
+ self.set_super_state(state_before, false) #roll it back on failure
230
+ end
231
+ rtn
232
+ else
233
+ raise SuperState::BadState, "#{method_name} can only be called from states #{state_hash.keys.inspect}"
234
+ end
235
+ end
236
+
237
+ end
238
+
239
+ end
240
+ end
@@ -0,0 +1,12 @@
1
+ require 'test_helper'
2
+
3
+ class SuperStateTest < ActiveSupport::TestCase
4
+
5
+ test "abstract the tests" do
6
+ puts "this has been abstracted from a live project"
7
+ puts "as such the tests have not yet been abstracted"
8
+ puts "give me some time to do this"
9
+ flunk "abstract the tests"
10
+ end
11
+
12
+ end
@@ -0,0 +1,3 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'active_support'
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: super_state
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Matthew Rudy Jacobs
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-12-17 00:00:00 +08:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description:
23
+ email: MatthewRudyJacobs@gmail.com
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files:
29
+ - README
30
+ files:
31
+ - MIT-LICENSE
32
+ - Rakefile
33
+ - README
34
+ - test/super_state_test.rb
35
+ - test/test_helper.rb
36
+ - lib/super_state.rb
37
+ has_rdoc: true
38
+ homepage: http://github.com/matthewrudy/super_state
39
+ licenses: []
40
+
41
+ post_install_message:
42
+ rdoc_options:
43
+ - --main
44
+ - README
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ none: false
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ hash: 3
53
+ segments:
54
+ - 0
55
+ version: "0"
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ hash: 3
62
+ segments:
63
+ - 0
64
+ version: "0"
65
+ requirements: []
66
+
67
+ rubyforge_project:
68
+ rubygems_version: 1.3.7
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: Super Simple State Machine
72
+ test_files: []
73
+