statelogic 1.1

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/CHANGELOG ADDED
@@ -0,0 +1,8 @@
1
+ 1.1 - 2009-08-11
2
+ - adds finder methods in the form of find_all_#{state}(*args)
3
+ - adds named scope named after corresponding state
4
+ - no longer redefines existing methods with the same names, issues warnings instead
5
+ - forces state names to underscore notation when used as part of method/scope names
6
+
7
+ 0.1.2 - 2009-01-10
8
+ - Support scoping arbitrary callbacks beyond ActiveRecord's
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 [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.rdoc ADDED
@@ -0,0 +1,81 @@
1
+ === Introduction
2
+ Statelogic is a yet another take on state management for your models.
3
+ It's tailored specially for ActiveRecord and is different from other
4
+ frameworks in that it has its main goal in scoping validation rules and
5
+ callbacks depending on the model's state to emphasis _actual_ model state
6
+ consistency over merely formal... whatever...
7
+
8
+ === Features
9
+ - State transitions validation.
10
+ - State-scoped validations.
11
+ - State-scoped lifecycle callbacks (before|after_save, etc)
12
+ - ???????
13
+ - PROFIT!!!11
14
+
15
+ === Changes in v1.1
16
+ _Please review these carefully as they may be incompatible with your existing code._
17
+ - adds finder methods in the form of +find_all_#{state}(*args)+ (Order.find_all_ready) sugg. by ares
18
+ - adds named scope named after corresponding state (Order.paid) sugg. by ares
19
+ - no longer redefines existing methods with the same names, issues warnings instead
20
+ - forces state names to underscore notation when used as part of method names
21
+
22
+ === Installation
23
+ gem install omg-statelogic --source http://gems.github.com
24
+
25
+ Installable as a plugin too.
26
+
27
+ === Docs
28
+ http://rdoc.info/projects/omg/statelogic
29
+
30
+ === Bugs & such
31
+ Please report via Github issue tracking.
32
+
33
+ === Example
34
+ class Order < ActiveRecord::Base
35
+ ...
36
+ statelogic :attribute => :state do # +:state+ is the default value, may be omitted
37
+ # you get methods +unpaid?+ and +was_unpaid?+.
38
+ # may be more than one initial state.
39
+ initial_state 'unpaid' do
40
+ transitions_to 'ready', 'suspended' # won't let you change to wrong states
41
+ end
42
+ state 'ready' do # you get +ready?+, +was_ready?+, +Order.ready+ named scope and +Order.find_all_ready+ finder
43
+ transitions_to 'redeemed', 'suspended'
44
+ validates_presence_of :txref # scoped validations
45
+ before_save :prepare_for_plucking # scoped callbacks
46
+ end
47
+ state 'redeemed' do # likewise
48
+ transitions_to 'suspended'
49
+ validates_presence_of :txref, :redeemed_at, :facility_id # scoped validations
50
+ end
51
+ state 'suspended' do # you guess
52
+ transitions_to 'unpaid', 'ready', 'redeemed'
53
+ validate do |order|
54
+ order.errors.add(:txref, :invalid) if order.txref && order.txref !~ /\AREF/
55
+ end
56
+ end
57
+ end
58
+ ...
59
+ end
60
+
61
+ order = Order.new
62
+ order.state = 'wtf'
63
+ order.txref = 'orly'
64
+ order.valid? # Please note that state transition checks are done during
65
+ # the validation step as well, and the error is added to
66
+ # the model's +errors+ collection on the state column and
67
+ # will appear on your form. Standard ActiveRecord's error
68
+ # message +:inclusion+ is used. Override it with Rails i18n
69
+ # for something more specific if it's to be displayed to user.
70
+
71
+
72
+ === See also
73
+ * http://github.com/omg/threadpool -- Thread pool implementation
74
+ * http://github.com/omg/peanuts -- Ruby <-> XML mapping
75
+
76
+
77
+ Hint: If you feel like generous today you can tip me at http://tipjoy.com/u/pisuka
78
+
79
+
80
+ Copyright (c) 2009 Igor Gunko, released under the MIT license
81
+
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/gempackagetask'
5
+
6
+
7
+ desc 'Default: run unit tests.'
8
+ task :default => :test
9
+
10
+ desc 'Test Statelogic'
11
+ Rake::TestTask.new(:test) do |t|
12
+ t.libs << 'lib'
13
+ t.libs << 'test'
14
+ t.pattern = 'test/**/*_test.rb'
15
+ t.verbose = true
16
+ end
17
+
18
+ desc 'Generate documentation for Statelogic.'
19
+ Rake::RDocTask.new(:rdoc) do |rdoc|
20
+ files =['README.rdoc', 'CHANGELOG', 'MIT-LICENSE', 'lib/**/*.rb']
21
+ rdoc.rdoc_files.add(files)
22
+ rdoc.main = "README.rdoc" # page to start on
23
+ rdoc.title = "Statelogic Documentation"
24
+ rdoc.rdoc_dir = 'doc/rdoc' # rdoc output folder
25
+ rdoc.options << '--line-numbers' << '--inline-source'
26
+ end
27
+
28
+ Rake::GemPackageTask.new(Gem::Specification.load('statelogic.gemspec')) do |p|
29
+ p.need_tar = true
30
+ p.need_zip = true
31
+ end
32
+
@@ -0,0 +1 @@
1
+ require 'statelogic'
@@ -0,0 +1,119 @@
1
+ require 'statelogic/callbacks_ext' unless ([ActiveRecord::VERSION::MAJOR, ActiveRecord::VERSION::MINOR] <=> [2, 3]) >= 0
2
+
3
+ module Statelogic
4
+ module Util
5
+ def self.debug(msg = nil, &block)
6
+ ::ActiveRecord::Base.logger.debug(msg, &block) if ::ActiveRecord::Base.logger
7
+ end
8
+
9
+ def self.warn(msg = nil, &block)
10
+ ::ActiveRecord::Base.logger.warn(msg, &block) if ::ActiveRecord::Base.logger
11
+ end
12
+
13
+ def self.defmethod(cls, name, meta = false, &block)
14
+ c = meta ? cls.metaclass : cls
15
+ unless c.method_defined?(name)
16
+ c.send(:define_method, name, &block)
17
+ Util.debug { "Statelogic created #{meta ? 'class' : 'instance'} method #{name} on #{cls.name}." }
18
+ else
19
+ warn { "Statelogic won't override #{meta ? 'class' : 'instance'} method #{name} already defined on #{cls.name}." }
20
+ nil
21
+ end
22
+ end
23
+ end
24
+
25
+ module ActiveRecord
26
+ def self.included(other)
27
+ other.extend(ClassMethods)
28
+ end
29
+
30
+ module ClassMethods
31
+ DEFAULT_OPTIONS = {:attribute => :state}.freeze
32
+
33
+ class StateScopeHelper
34
+ MACROS_PATTERN = /\Avalidates_/.freeze
35
+
36
+ def initialize(cl, state, config)
37
+ @class, @state, @config = cl, state, config
38
+ end
39
+
40
+ def validates_transition_to(*states)
41
+ attr = @config[:attribute]
42
+ options = states.extract_options!.update(
43
+ :in => states,
44
+ :if => [:"#{attr}_changed?", :"was_#{@state}?"]
45
+ )
46
+ @class.validates_inclusion_of(attr, options)
47
+ end
48
+
49
+ alias transitions_to validates_transition_to
50
+
51
+ def method_missing(method, *args, &block)
52
+ if method.to_s =~ MACROS_PATTERN || @class.respond_to?("#{method}_callback_chain")
53
+ options = args.last
54
+ args.push(options = {}) unless options.is_a?(Hash)
55
+ options[:if] = Array(options[:if]).unshift(:"#{@state}?")
56
+ @class.send(method, *args, &block)
57
+ else
58
+ super
59
+ end
60
+ end
61
+ end
62
+
63
+ class ConfigHelper
64
+ def initialize(cl, config)
65
+ @class, @config = cl, config
66
+ end
67
+
68
+ def initial_state(name, options = {}, &block)
69
+ state(name, options.update(:initial => true), &block)
70
+ end
71
+
72
+ alias initial initial_state
73
+
74
+ def state(name, options = {}, &block)
75
+ name = name.to_s
76
+ uname = name.underscore
77
+ attr = @config[:attribute]
78
+ attr_was = :"#{attr}_was"
79
+ find_all_by_attr = "find_all_by_#{attr}"
80
+
81
+ Util.defmethod(@class, "#{uname}?") { send(attr) == name }
82
+ Util.defmethod(@class, "was_#{uname}?") { send(attr_was) == name }
83
+
84
+ unless @class.respond_to?(name)
85
+ @class.send(:named_scope, uname, :conditions => {attr.to_sym => name })
86
+ Util.debug { "Statelogic has defined named scope #{uname} on #{@class.name}." }
87
+ else
88
+ Util.warn { "Statelogic won't override class method #{uname} already defined on #{@class.name}." }
89
+ end
90
+
91
+ Util.defmethod(@class, "find_all_#{uname}", true) {|*args| send(find_all_by_attr, name, *args) }
92
+
93
+ StateScopeHelper.new(@class, name, @config).instance_eval(&block) if block_given?
94
+
95
+ @config[:states] << name
96
+ @config[:initial] << name if options[:initial]
97
+ end
98
+ end
99
+
100
+ def statelogic(options = {}, &block)
101
+ options = DEFAULT_OPTIONS.merge(options)
102
+ attr = options[:attribute] = options[:attribute].to_sym
103
+
104
+ options[:states], options[:initial] = [], Array(options[:initial])
105
+
106
+ ConfigHelper.new(self, options).instance_eval(&block)
107
+
108
+ initial = [options[:initial], options[:states]].find(&:present?)
109
+ validates_inclusion_of attr, :in => initial, :on => :create if initial
110
+
111
+ const = attr.to_s.pluralize.upcase
112
+ const_set(const, options[:states].freeze.each(&:freeze)) unless const_defined?(const)
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ ActiveRecord::Base.send :include, Statelogic::ActiveRecord
119
+
@@ -0,0 +1,7 @@
1
+ # :stopdoc:
2
+ class ActiveSupport::Callbacks::Callback
3
+ def should_run_callback?(*args)
4
+ [options[:if]].flatten.compact.all? { |a| evaluate_method(a, *args) } &&
5
+ ![options[:unless]].flatten.compact.any? { |a| evaluate_method(a, *args) }
6
+ end
7
+ end
data/lib/statelogic.rb ADDED
@@ -0,0 +1 @@
1
+ # undisturbed void
data/rails/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'statelogic'
2
+ require 'statelogic/activerecord'
@@ -0,0 +1,13 @@
1
+ class Test::Unit::TestCase
2
+ def self.should_not_require_attributes(*attributes)
3
+ get_options!(attributes)
4
+ klass = described_type
5
+
6
+ attributes.each do |attribute|
7
+ should "not require #{attribute} to be set" do
8
+ assert_good_value(klass, attribute, nil)
9
+ end
10
+ end
11
+ end
12
+ end
13
+
@@ -0,0 +1,13 @@
1
+ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
2
+
3
+ unpaid:
4
+ state: unpaid
5
+
6
+ ready:
7
+ state: ready
8
+
9
+ redeemed:
10
+ state: redeemed
11
+
12
+ suspended:
13
+ state: suspended
data/test/schema.rb ADDED
@@ -0,0 +1,8 @@
1
+ ActiveRecord::Schema.define do
2
+ create_table 'orders', :force => true do |t|
3
+ t.column 'state', :text
4
+ t.column 'redeemed_at', :datetime
5
+ t.column 'txref', :string
6
+ t.references :facility
7
+ end
8
+ end
@@ -0,0 +1,75 @@
1
+ require 'test_helper'
2
+
3
+ #ActiveRecord::Base.logger = Logger.new(STDERR)
4
+
5
+ class OrderTest < ActiveSupport::TestCase
6
+ include ActiveRecord::TestFixtures
7
+ self.fixture_path = 'test/fixtures'
8
+
9
+ fixtures :orders
10
+
11
+ should_have_class_methods :unpaid, :ready, :redeemed, :suspended
12
+ should_have_class_methods :find_all_unpaid, :find_all_ready, :find_all_redeemed, :find_all_suspended
13
+
14
+ should_have_instance_methods :unpaid?, :ready?, :redeemed?, :suspended?
15
+ should_have_instance_methods :was_unpaid?, :was_ready?, :was_redeemed?, :was_suspended?
16
+
17
+ context 'Finders and scopes' do
18
+ should 'return adequate shit' do
19
+ for st in %w(unpaid ready redeemed suspended)
20
+ assert_same_elements Order.find_all_by_state(st), Order.send(st)
21
+ assert_same_elements Order.find_all_by_state(st), Order.send("find_all_#{st}")
22
+ end
23
+ end
24
+ end
25
+
26
+ should_validate_presence_of :state, :message => default_error_message(:inclusion)
27
+
28
+ context 'A fresh order' do
29
+ subject { Order.new }
30
+
31
+ should_allow_values_for :state, 'unpaid'
32
+ should_not_allow_values_for :state, 'ready', 'redeemed', 'suspended', 'screwed_up',
33
+ :message => default_error_message(:inclusion)
34
+ should_not_require_attributes :txref, :redeemed_at, :facility_id
35
+ end
36
+
37
+ context 'An unpaid order' do
38
+ subject { orders(:unpaid) }
39
+
40
+ should_allow_values_for :state, 'unpaid', 'ready', 'suspended'
41
+ should_not_allow_values_for :state, 'redeemed', 'screwed_up',
42
+ :message => default_error_message(:inclusion)
43
+ should_not_require_attributes :txref, :redeemed_at, :facility_id
44
+ end
45
+
46
+ context 'A ready order' do
47
+ subject { orders(:ready) }
48
+
49
+ should_allow_values_for :state, 'ready', 'redeemed', 'suspended'
50
+ should_not_allow_values_for :state, 'unpaid', 'screwed_up',
51
+ :message => default_error_message(:inclusion)
52
+ should_validate_presence_of :txref
53
+ should_not_require_attributes :redeemed_at, :facility_id
54
+ end
55
+
56
+ context 'A redeemed order' do
57
+ subject { orders(:redeemed) }
58
+
59
+ should_allow_values_for :state, 'redeemed', 'suspended'
60
+ should_not_allow_values_for :state, 'unpaid', 'ready', 'screwed_up',
61
+ :message => default_error_message(:inclusion)
62
+ should_validate_presence_of :txref, :redeemed_at, :facility_id
63
+ end
64
+
65
+ context 'A suspended order' do
66
+ subject { orders(:suspended) }
67
+
68
+ should_allow_values_for :state, 'suspended', 'redeemed', 'ready', 'unpaid'
69
+ should_not_allow_values_for :state, 'screwed_up', :message => default_error_message(:inclusion)
70
+ should_allow_values_for :txref, 'REF12345'
71
+ should_not_allow_values_for :txref, '12345'
72
+ should_not_require_attributes :txref, :redeemed_at, :facility_id
73
+ end
74
+ end
75
+
@@ -0,0 +1,19 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'active_support'
4
+ require 'active_support/test_case'
5
+ require 'active_record'
6
+ require 'shoulda/active_record'
7
+ require 'active_record/fixtures'
8
+ require 'statelogic/activerecord'
9
+
10
+ require 'shoulda_macros/statelogic'
11
+
12
+ #ActiveRecord::TestFixtures.fixture_path = 'test/fixtures'
13
+
14
+ ActiveRecord::Base.configurations = {
15
+ 'test' => {:adapter => 'sqlite3', :dbfile => ':memory:'}
16
+ }
17
+ ActiveRecord::Base.establish_connection(:test)
18
+
19
+ load 'schema.rb'
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: statelogic
3
+ version: !ruby/object:Gem::Version
4
+ version: "1.1"
5
+ platform: ruby
6
+ authors:
7
+ - Igor Gunko
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-08-11 00:00:00 +03:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activerecord
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 2.3.2
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: thoughtbot-shoulda
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 2.10.2
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: sqlite3-ruby
37
+ type: :development
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 1.2.4
44
+ version:
45
+ description: " Statelogic does kinda this and that... you know.\n"
46
+ email: tekmon@gmail.com
47
+ executables: []
48
+
49
+ extensions: []
50
+
51
+ extra_rdoc_files:
52
+ - README.rdoc
53
+ - CHANGELOG
54
+ - MIT-LICENSE
55
+ files:
56
+ - README.rdoc
57
+ - CHANGELOG
58
+ - MIT-LICENSE
59
+ - Rakefile
60
+ - lib/statelogic.rb
61
+ - lib/omg-statelogic.rb
62
+ - lib/statelogic/activerecord.rb
63
+ - lib/statelogic/callbacks_ext.rb
64
+ - rails/init.rb
65
+ has_rdoc: true
66
+ homepage: http://github.com/omg/statelogic
67
+ licenses: []
68
+
69
+ post_install_message: v1.1 introduces changes that may (or may not) be incompatible for you. Please review them at http://github.com/omg/statelogic
70
+ rdoc_options:
71
+ - --line-numbers
72
+ - --main
73
+ - README.rdoc
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: "0"
81
+ version:
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: "0"
87
+ version:
88
+ requirements: []
89
+
90
+ rubyforge_project:
91
+ rubygems_version: 1.3.5
92
+ signing_key:
93
+ specification_version: 2
94
+ summary: Another state machine for ActiveRecord
95
+ test_files:
96
+ - test/statelogic_test.rb
97
+ - test/test_helper.rb
98
+ - test/schema.rb
99
+ - test/fixtures/orders.yml
100
+ - shoulda_macros/statelogic.rb