super_state 0.1.0

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.
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
+