state_attr 0.1.9
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +40 -0
- data/Rakefile +24 -0
- data/lib/state_attr/state.rb +72 -0
- data/lib/state_attr.rb +36 -0
- data/rails/init.rb +2 -0
- data/state_attr.gemspec +22 -0
- data/test/test_helper.rb +46 -0
- data/test/unit/state_attr_test.rb +79 -0
- metadata +74 -0
data/README.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# About
|
2
|
+
|
3
|
+
StateAttr is an minimalistic state machine approach for rails allowing multiple state attributes at the same time.
|
4
|
+
|
5
|
+
# Installation
|
6
|
+
|
7
|
+
gem install state_attr
|
8
|
+
|
9
|
+
# Examples
|
10
|
+
|
11
|
+
state_attr :state, {
|
12
|
+
nil => :first,
|
13
|
+
:first => [:second, :third],
|
14
|
+
:second => :last,
|
15
|
+
:third => nil,
|
16
|
+
}
|
17
|
+
|
18
|
+
state_attr :invitation_state, {
|
19
|
+
:invited => %w{approved rejected},
|
20
|
+
:approved => :rejected,
|
21
|
+
:rejected => :approved,
|
22
|
+
}, :initial => :invited
|
23
|
+
|
24
|
+
state_attr :progress, {
|
25
|
+
nil => :one,
|
26
|
+
:one => :two,
|
27
|
+
:two => :three
|
28
|
+
} do |old, new|
|
29
|
+
if new == :three
|
30
|
+
send_mail "You just finished."
|
31
|
+
else
|
32
|
+
send_mail "You just made to #{new}."
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Options
|
37
|
+
|
38
|
+
- :initial => :value - initial `value` (not yet implemented)
|
39
|
+
- :setter => :exception - raise exception when calling `state=`
|
40
|
+
- :switch_not_allowed => :silent - do not raise exception when switch is not allowed
|
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 the session_management plugin.'
|
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 session_management plugin.'
|
17
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
18
|
+
rdoc.rdoc_dir = 'rdoc'
|
19
|
+
rdoc.title = 'SessionManagement'
|
20
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
21
|
+
rdoc.rdoc_files.include('README.md')
|
22
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module StateAttr
|
2
|
+
module ClassMethods
|
3
|
+
class State
|
4
|
+
|
5
|
+
def initialize(parent, field, machine, logger, options)
|
6
|
+
@parent = parent
|
7
|
+
@field = field
|
8
|
+
#convert different types of input to array of symbols
|
9
|
+
@machine = {}
|
10
|
+
machine.each { |key, value| @machine[key] = value.nil? ? [nil] : Array(value).map(&:to_sym) }
|
11
|
+
@logger = logger
|
12
|
+
@options = options
|
13
|
+
@callback = "on_#{@field}_change".to_sym
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
read_state.to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
# current state value
|
21
|
+
def value
|
22
|
+
read_state
|
23
|
+
end
|
24
|
+
alias_method :current, :value
|
25
|
+
|
26
|
+
# validates if state is one of the given states
|
27
|
+
def is?(*symbols)
|
28
|
+
!symbols.flatten.select { |state| read_state == state }.empty?
|
29
|
+
end
|
30
|
+
|
31
|
+
# array of allowed stated
|
32
|
+
def allowed
|
33
|
+
@machine[read_state] || []
|
34
|
+
end
|
35
|
+
|
36
|
+
#validate if can switch to given state
|
37
|
+
def allowed?(state)
|
38
|
+
allowed.include?(state) || is?(state)
|
39
|
+
end
|
40
|
+
|
41
|
+
# if allowed switch to given state, if not raise exception
|
42
|
+
def switch(state)
|
43
|
+
if (allowed?(state))
|
44
|
+
write_state(state)
|
45
|
+
elsif @options[:switch_not_allowed] == :silent
|
46
|
+
@logger.error "#{@parent.class.name} changing #{@field} from '#{read_state}' to '#{state}' is not allowed, allowed states are '#{allowed*'\', \''}'"
|
47
|
+
false
|
48
|
+
else
|
49
|
+
raise "#{@parent.class.name} error, changing #{@field} from '#{read_state}' to '#{state}' is not allowed, allowed states are '#{allowed*'\', \''}'"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def read_state
|
56
|
+
@parent.read_attribute(@field).try(:to_sym)
|
57
|
+
end
|
58
|
+
|
59
|
+
def write_state(state)
|
60
|
+
if @parent.methods.include? @callback.to_s
|
61
|
+
unless @parent.send @callback, read_state, state
|
62
|
+
@logger.warn "#{@parent.class.name}: change #{@field} from '#{read_state}' to '#{state}' was rejected by callback"
|
63
|
+
return false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
@logger.debug "#{@parent.class.name}: changing #{@field} from '#{read_state}' to '#{state}'"
|
67
|
+
@parent.write_attribute(@field, state.try(:to_s))
|
68
|
+
true
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
data/lib/state_attr.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'state_attr/state'
|
2
|
+
module StateAttr
|
3
|
+
|
4
|
+
module ClassMethods
|
5
|
+
def state_attr(attr, machine, options={}, &block)
|
6
|
+
#attr, initial = attr
|
7
|
+
self.send :define_method, attr do
|
8
|
+
@state_handlers ||= {}
|
9
|
+
@state_handlers[attr] ||= State.new(self, attr, machine, logger, options)
|
10
|
+
@state_handlers[attr]
|
11
|
+
end
|
12
|
+
|
13
|
+
if options[:setter] == :exception
|
14
|
+
self.send :define_method, "#{attr}=".to_sym do |state|
|
15
|
+
raise "#{self.class.name} error, manual setting of state is not allowed (new value '#{state}')"
|
16
|
+
end
|
17
|
+
else
|
18
|
+
self.send :define_method, "#{attr}=".to_sym do |state|
|
19
|
+
@state_handlers ||= {}
|
20
|
+
@state_handlers[attr] ||= State.new(self, attr, machine, logger, options)
|
21
|
+
@state_handlers[attr].switch(state)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
if block_given?
|
26
|
+
self.send :define_method, "on_#{attr}_change".to_sym do |*args|
|
27
|
+
block.call *args
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.included(base)
|
34
|
+
base.extend ClassMethods
|
35
|
+
end
|
36
|
+
end
|
data/rails/init.rb
ADDED
data/state_attr.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "state_attr"
|
3
|
+
s.version = "0.1.9"
|
4
|
+
s.date = "2011-05-16"
|
5
|
+
s.summary = "Minimalistic state machine approach allowing multiple state attributes at the same time."
|
6
|
+
s.email = "mpapis@gmail.com"
|
7
|
+
s.homepage = "http://github.com/mpapis/state_attr/tree/master"
|
8
|
+
s.description = "Minimalistic state machine."
|
9
|
+
s.has_rdoc = false
|
10
|
+
s.authors = ["Michal Papis"]
|
11
|
+
s.files = [
|
12
|
+
"lib/state_attr.rb",
|
13
|
+
"lib/state_attr/state.rb",
|
14
|
+
"rails/init.rb",
|
15
|
+
"Rakefile",
|
16
|
+
"README.md",
|
17
|
+
"state_attr.gemspec",
|
18
|
+
"test/test_helper.rb",
|
19
|
+
"test/unit/state_attr_test.rb",
|
20
|
+
]
|
21
|
+
#s.add_dependency("rails",["~>2","~>3"])
|
22
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'state_attr'
|
3
|
+
|
4
|
+
# fake rails
|
5
|
+
class << nil
|
6
|
+
def try *args
|
7
|
+
nil
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Object
|
12
|
+
def try *args
|
13
|
+
send *args
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Logger
|
18
|
+
def info msg
|
19
|
+
end
|
20
|
+
def warn msg
|
21
|
+
end
|
22
|
+
def error msg
|
23
|
+
end
|
24
|
+
def debug msg
|
25
|
+
end
|
26
|
+
end
|
27
|
+
RAILS_DEFAULT_LOGGER = Logger.new
|
28
|
+
|
29
|
+
module ActiveRecord
|
30
|
+
class Base
|
31
|
+
def write_attribute(name, value)
|
32
|
+
@attributes ||= {}
|
33
|
+
@attributes[name] = value
|
34
|
+
end
|
35
|
+
def read_attribute(name)
|
36
|
+
attributes[name]
|
37
|
+
end
|
38
|
+
def attributes
|
39
|
+
@attributes || {}
|
40
|
+
end
|
41
|
+
def logger
|
42
|
+
RAILS_DEFAULT_LOGGER
|
43
|
+
end
|
44
|
+
include StateAttr
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class FakeModel < ActiveRecord::Base
|
4
|
+
state_attr :state1, {
|
5
|
+
nil => :a,
|
6
|
+
:a => :b,
|
7
|
+
}, :setter => :exception
|
8
|
+
|
9
|
+
state_attr :state2, {
|
10
|
+
nil => [:d,:f],
|
11
|
+
:d => [:e,:f],
|
12
|
+
:f => [:d,:e],
|
13
|
+
:e => nil,
|
14
|
+
}, :setter => :exception
|
15
|
+
|
16
|
+
state_attr :state3, {
|
17
|
+
nil => :g,
|
18
|
+
:g => :h,
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
class StateAttrTest < Test::Unit::TestCase
|
23
|
+
def test_single_set_read
|
24
|
+
model = FakeModel.new
|
25
|
+
assert_equal nil, model.state1.value
|
26
|
+
assert_equal "", model.state1.to_s
|
27
|
+
model.state1.switch(:a)
|
28
|
+
assert_equal :a, model.state1.value
|
29
|
+
assert_equal "a", "#{model.state1}"
|
30
|
+
end
|
31
|
+
def test_single_set_fail
|
32
|
+
model = FakeModel.new
|
33
|
+
assert_raises RuntimeError do
|
34
|
+
model.state1.switch(:c)
|
35
|
+
end
|
36
|
+
assert_equal nil, model.state1.value
|
37
|
+
end
|
38
|
+
def test_single_assignment_fail
|
39
|
+
model = FakeModel.new
|
40
|
+
assert_raises RuntimeError do
|
41
|
+
model.state1=:a
|
42
|
+
end
|
43
|
+
assert_equal nil, model.state1.value
|
44
|
+
end
|
45
|
+
def test_single_assignment_success
|
46
|
+
model = FakeModel.new
|
47
|
+
model.state3=:g
|
48
|
+
assert_equal :g, model.state3.value
|
49
|
+
end
|
50
|
+
def test_single_set_nil
|
51
|
+
model = FakeModel.new
|
52
|
+
model.state2.switch(:d)
|
53
|
+
model.state2.switch(:e)
|
54
|
+
assert_equal :e, model.state2.value
|
55
|
+
model.state2.switch(nil)
|
56
|
+
assert_equal nil, model.state2.value
|
57
|
+
end
|
58
|
+
def test_double_set
|
59
|
+
model = FakeModel.new
|
60
|
+
model.state1.switch(:a)
|
61
|
+
model.state2.switch(:d)
|
62
|
+
assert_equal :a, model.state1.value
|
63
|
+
assert_equal :d, model.state2.value
|
64
|
+
end
|
65
|
+
def test_single_is?
|
66
|
+
model = FakeModel.new
|
67
|
+
model.state2.switch(:d)
|
68
|
+
assert model.state2.is?(:d)
|
69
|
+
assert !model.state2.is?(:e)
|
70
|
+
assert model.state2.is?(:d,:e)
|
71
|
+
end
|
72
|
+
def test_single_allowed?
|
73
|
+
model = FakeModel.new
|
74
|
+
model.state2.switch(:d)
|
75
|
+
assert !model.state2.allowed?(nil)
|
76
|
+
assert model.state2.allowed?(:d)
|
77
|
+
assert model.state2.allowed?(:e)
|
78
|
+
end
|
79
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: state_attr
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 9
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 9
|
10
|
+
version: 0.1.9
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Michal Papis
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-05-16 00:00:00 +02:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: Minimalistic state machine.
|
23
|
+
email: mpapis@gmail.com
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files: []
|
29
|
+
|
30
|
+
files:
|
31
|
+
- lib/state_attr.rb
|
32
|
+
- lib/state_attr/state.rb
|
33
|
+
- rails/init.rb
|
34
|
+
- Rakefile
|
35
|
+
- README.md
|
36
|
+
- state_attr.gemspec
|
37
|
+
- test/test_helper.rb
|
38
|
+
- test/unit/state_attr_test.rb
|
39
|
+
has_rdoc: true
|
40
|
+
homepage: http://github.com/mpapis/state_attr/tree/master
|
41
|
+
licenses: []
|
42
|
+
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
hash: 3
|
54
|
+
segments:
|
55
|
+
- 0
|
56
|
+
version: "0"
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
hash: 3
|
63
|
+
segments:
|
64
|
+
- 0
|
65
|
+
version: "0"
|
66
|
+
requirements: []
|
67
|
+
|
68
|
+
rubyforge_project:
|
69
|
+
rubygems_version: 1.5.2
|
70
|
+
signing_key:
|
71
|
+
specification_version: 3
|
72
|
+
summary: Minimalistic state machine approach allowing multiple state attributes at the same time.
|
73
|
+
test_files: []
|
74
|
+
|