omg-statelogic 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/CHANGELOG ADDED
@@ -0,0 +1,2 @@
1
+ 0.1.2 - 2008-01-10
2
+ - 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,74 @@
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 in order to facilitate
6
+ enforcing _real_ model data consistency instead of merely formal.
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
+ === Installation
16
+ gem install omg-statelogic --source http://gems.github.com
17
+
18
+ Installable as a plugin too.
19
+
20
+ === Docs
21
+ http://rdoc.info/projects/omg/statelogic
22
+
23
+ === Bugs & such
24
+ Please report via Github issue tracking.
25
+
26
+ === Example
27
+ class Order < ActiveRecord::Base
28
+ ...
29
+ statelogic :attribute => :state do # +:state+ is the default value, may be omitted
30
+ # you get methods +unpaid?+ and +was_unpaid?+.
31
+ # may be more than one initial state.
32
+ initial_state 'unpaid' do
33
+ transitions_to 'ready', 'suspended' # won't let you change to wrong states
34
+ end
35
+ state 'ready' do # you get +ready?+, +was_ready?+
36
+ transitions_to 'redeemed', 'suspended'
37
+ validates_presence_of :txref # scoped validations
38
+ before_save :prepare_for_plucking # scoped callbacks
39
+ end
40
+ state 'redeemed' do # likewise
41
+ transitions_to 'suspended'
42
+ validates_presence_of :txref, :redeemed_at, :facility_id # scoped validations
43
+ end
44
+ state 'suspended' do # you guess
45
+ transitions_to 'unpaid', 'ready', 'redeemed'
46
+ validate do |order|
47
+ order.errors.add(:txref, :invalid) if order.txref && order.txref !~ /\AREF/
48
+ end
49
+ end
50
+ end
51
+ ...
52
+ end
53
+
54
+ order = Order.new
55
+ order.state = 'wtf'
56
+ order.txref = 'orly'
57
+ order.valid? # Please note that state transition checks are done during
58
+ # the validation step as well, and the error is added to
59
+ # the model's +errors+ collection on the state column and
60
+ # will appear on your form. Standard ActiveRecord's error
61
+ # message +:inclusion+ is used. Override it with Rails i18n
62
+ # for something more specific if it's to be displayed to user.
63
+
64
+
65
+ === See also
66
+ * http://github.com/omg/threadpool -- Thread pool implementation
67
+ * http://github.com/omg/xmlnuts -- Ruby <-> XML mapping
68
+
69
+
70
+ Hint: If you feel like generous today you can tip me at http://tipjoy.com/u/pisuka
71
+
72
+
73
+ Copyright (c) 2009 Igor Gunko, released under the MIT license
74
+
data/Rakefile ADDED
@@ -0,0 +1,24 @@
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 Statelogic'
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 Statelogic.'
17
+ Rake::RDocTask.new(:rdoc) do |rdoc|
18
+ files =['README.rdoc', 'CHANGELOG', 'MIT-LICENSE', 'lib/**/*.rb']
19
+ rdoc.rdoc_files.add(files)
20
+ rdoc.main = "README.rdoc" # page to start on
21
+ rdoc.title = "Statelogic Documentation"
22
+ rdoc.rdoc_dir = 'doc/rdoc' # rdoc output folder
23
+ rdoc.options << '--line-numbers' << '--inline-source'
24
+ end
@@ -0,0 +1 @@
1
+ require 'statelogic'
@@ -0,0 +1,87 @@
1
+ require 'statelogic/callbacks_ext'
2
+
3
+ module Statelogic
4
+ module ActiveRecord
5
+ def self.included(other)
6
+ other.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ DEFAULT_OPTIONS = {:attribute => :state}.freeze
11
+
12
+ class StateScopeHelper
13
+ MACROS_PATTERN = /\Avalidates_/.freeze
14
+
15
+ def initialize(cl, state, config)
16
+ @class, @state, @config = cl, state, config
17
+ end
18
+
19
+ def validates_transition_to(*states)
20
+ attr = @config[:attribute]
21
+ options = states.extract_options!.update(
22
+ :in => states,
23
+ :if => [:"#{attr}_changed?", :"was_#{@state}?"]
24
+ )
25
+ @class.validates_inclusion_of(attr, options)
26
+ end
27
+
28
+ alias transitions_to validates_transition_to
29
+
30
+ def method_missing(method, *args, &block)
31
+ if method.to_s =~ MACROS_PATTERN || @class.respond_to?("#{method}_callback_chain")
32
+ options = args.last
33
+ args.push(options = {}) unless options.is_a?(Hash)
34
+ options[:if] = Array(options[:if]).unshift(:"#{@state}?")
35
+ @class.send(method, *args, &block)
36
+ else
37
+ super
38
+ end
39
+ end
40
+ end
41
+
42
+ class ConfigHelper
43
+ def initialize(cl, config)
44
+ @class, @config = cl, config
45
+ end
46
+
47
+ def initial_state(name, options = {}, &block)
48
+ state(name, options.update(:initial => true), &block)
49
+ end
50
+
51
+ def state(name, options = {}, &block)
52
+ attr = @config[:attribute]
53
+ attr_was = :"#{attr}_was"
54
+ @class.class_eval do
55
+ define_method("#{name}?") { send(attr) == name }
56
+ define_method("was_#{name}?") { send(attr_was) == name }
57
+ end
58
+
59
+ StateScopeHelper.new(@class, name, @config).instance_eval(&block)
60
+
61
+ @config[:states] << name
62
+ @config[:initial] << name if options[:initial]
63
+ end
64
+ end
65
+
66
+ def statelogic(options = {}, &block)
67
+ options = DEFAULT_OPTIONS.merge(options)
68
+ attr = options[:attribute] = options[:attribute].to_sym
69
+
70
+ options[:states], options[:initial] = [], Array(options[:initial])
71
+
72
+ ConfigHelper.new(self, options).instance_eval(&block)
73
+
74
+ initial = options[:initial] || options[:states]
75
+ validates_inclusion_of attr, :in => initial, :on => :create unless initial.blank?
76
+
77
+ const = attr.to_s.pluralize.upcase
78
+ const_set(const, options[:states].freeze.each(&:freeze)) unless const_defined?(const)
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ # :stopdoc:
85
+ class ActiveRecord::Base
86
+ include Statelogic::ActiveRecord
87
+ end
@@ -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,12 @@
1
+ class Test::Unit::TestCase
2
+ def self.should_not_require_attributes(*attributes)
3
+ get_options!(attributes)
4
+ klass = model_class
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
@@ -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,86 @@
1
+ require 'test_helper'
2
+
3
+ class Order < ActiveRecord::Base
4
+ statelogic do
5
+ initial_state 'unpaid' do
6
+ transitions_to 'ready', 'suspended'
7
+ end
8
+ state 'ready' do
9
+ transitions_to 'redeemed', 'suspended'
10
+ validates_presence_of :txref
11
+ end
12
+ state 'redeemed' do
13
+ transitions_to 'suspended'
14
+ validates_presence_of :txref, :redeemed_at, :facility_id
15
+ end
16
+ state 'suspended' do
17
+ transitions_to 'unpaid', 'ready', 'redeemed'
18
+ validate do |order|
19
+ order.errors.add(:txref, :invalid) if order.txref && order.txref !~ /\AREF/
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ class OrderTest < ActiveSupport::TestCase
26
+ fixtures :orders
27
+
28
+ should_require_attributes :state, :message => default_error_message(:inclusion)
29
+
30
+ context 'A fresh order' do
31
+ setup do
32
+ @order = Order.new
33
+ end
34
+
35
+ should_allow_values_for :state, 'unpaid'
36
+ should_not_allow_values_for :state, 'ready', 'redeemed', 'suspended', 'screwed_up',
37
+ :message => default_error_message(:inclusion)
38
+ should_not_require_attributes :txref, :redeemed_at, :facility_id
39
+ end
40
+
41
+ context 'An unpaid order' do
42
+ setup do
43
+ @order = orders(:unpaid)
44
+ end
45
+
46
+ should_allow_values_for :state, 'unpaid', 'ready', 'suspended'
47
+ should_not_allow_values_for :state, 'redeemed', 'screwed_up',
48
+ :message => default_error_message(:inclusion)
49
+ should_not_require_attributes :txref, :redeemed_at, :facility_id
50
+ end
51
+
52
+ context 'A ready order' do
53
+ setup do
54
+ @order = orders(:ready)
55
+ end
56
+
57
+ should_allow_values_for :state, 'ready', 'redeemed', 'suspended'
58
+ should_not_allow_values_for :state, 'unpaid', 'screwed_up',
59
+ :message => default_error_message(:inclusion)
60
+ should_require_attributes :txref
61
+ should_not_require_attributes :redeemed_at, :facility_id
62
+ end
63
+
64
+ context 'A redeemed order' do
65
+ setup do
66
+ @order = orders(:redeemed)
67
+ end
68
+
69
+ should_allow_values_for :state, 'redeemed', 'suspended'
70
+ should_not_allow_values_for :state, 'unpaid', 'ready', 'screwed_up',
71
+ :message => default_error_message(:inclusion)
72
+ should_require_attributes :txref, :redeemed_at, :facility_id
73
+ end
74
+
75
+ context 'A suspended order' do
76
+ setup do
77
+ @order = orders(:suspended)
78
+ end
79
+
80
+ should_allow_values_for :state, 'suspended', 'redeemed', 'ready', 'unpaid'
81
+ should_not_allow_values_for :state, 'screwed_up', :message => default_error_message(:inclusion)
82
+ should_allow_values_for :txref, 'REF12345'
83
+ should_not_allow_values_for :txref, '12345'
84
+ should_not_require_attributes :txref, :redeemed_at, :facility_id
85
+ end
86
+ end
@@ -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
+ ActiveSupport::TestCase.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,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omg-statelogic
3
+ version: !ruby/object:Gem::Version
4
+ version: "1.0"
5
+ platform: ruby
6
+ authors:
7
+ - Igor Gunko
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-06-21 00:00:00 -07: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.2.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.0.6
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.
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
+ post_install_message:
68
+ rdoc_options:
69
+ - --line-numbers
70
+ - --main
71
+ - README.rdoc
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: "0"
79
+ version:
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: "0"
85
+ version:
86
+ requirements: []
87
+
88
+ rubyforge_project:
89
+ rubygems_version: 1.2.0
90
+ signing_key:
91
+ specification_version: 2
92
+ summary: Another state machine for ActiveRecord
93
+ test_files:
94
+ - test/statelogic_test.rb
95
+ - test/test_helper.rb
96
+ - test/schema.rb
97
+ - test/fixtures/orders.yml
98
+ - shoulda_macros/statelogic.rb