stateful 0.0.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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +115 -0
- data/Rakefile +1 -0
- data/lib/stateful/mongoid.rb +27 -0
- data/lib/stateful/state_info.rb +57 -0
- data/lib/stateful/version.rb +3 -0
- data/lib/stateful.rb +142 -0
- data/spec/mongoid_spec.rb +44 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/stateful_spec.rb +158 -0
- data/stateful.gemspec +28 -0
- metadata +131 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 85ae4d99568569f667a41d4b1959cc511f300dea
|
4
|
+
data.tar.gz: af2c257fdfcedf21ea5b760537c9d32c004ac74e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8b714fc19974ee231d8baf208c655f835a6c002fa12c984740f0a14a29449a8b3595b7e7297ecf203dc318f077ec49f7ad8c0be396b818d3b915502f5db59255
|
7
|
+
data.tar.gz: 1b6d4c06b40dad7123e6a6f6c946596ca0f1a1665cff1b0e4fd9f7e68012703def03a97c36417584ccfb8e1ac221f48a84233809907e146a6b49dd966bb1d801
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 jake hoffner
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
# Stateful
|
2
|
+
|
3
|
+
A simple state machine gem. Works with plain ruby objects and Mongoid. This gem aims
|
4
|
+
to keep things simple. It supports the following:
|
5
|
+
|
6
|
+
- Single state attribute/field per object
|
7
|
+
- Simple event model, just use plain ruby methods as your events and use the change_state helper to change the state.
|
8
|
+
- Supports virtual/grouped states that can be used to break down top level states into more granular sub-states.
|
9
|
+
- Utilizes ActiveSupport::Callbacks
|
10
|
+
- Simple hash structure for defining states and their possible transitions. No complicated DSL to learn.
|
11
|
+
- ActiveSupport is the only dependency.
|
12
|
+
- Very small code footprint.
|
13
|
+
- Mongoid support, automatically creates field, validations and scopes for you.
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add this line to your application's Gemfile:
|
18
|
+
|
19
|
+
gem 'stateful'
|
20
|
+
|
21
|
+
And then execute:
|
22
|
+
|
23
|
+
$ bundle
|
24
|
+
|
25
|
+
Or install it yourself as:
|
26
|
+
|
27
|
+
$ gem install stateful
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
class Project
|
33
|
+
include Stateful
|
34
|
+
|
35
|
+
attr_reader :published_at
|
36
|
+
|
37
|
+
stateful default: :new,
|
38
|
+
events: [:publish, :unpublish, :approve, :close, :mark_as_duplicate],
|
39
|
+
states: {
|
40
|
+
active: {
|
41
|
+
new: :published,
|
42
|
+
published: {
|
43
|
+
needs_approval: [:approved, :duplicate],
|
44
|
+
approved: :closed
|
45
|
+
}
|
46
|
+
},
|
47
|
+
inactive: {
|
48
|
+
closed: nil,
|
49
|
+
duplicate: nil
|
50
|
+
}
|
51
|
+
}
|
52
|
+
|
53
|
+
def publish
|
54
|
+
# change the state to needs_approval and fire publish events. The block will only be
|
55
|
+
# called if the state can successfully be changed.
|
56
|
+
change_state(:needs_approval, :publish) do
|
57
|
+
@published_at = Time.now
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# use callbacks if you want
|
62
|
+
after_publish do |project|
|
63
|
+
NotificationService.notify_project_published(project)
|
64
|
+
end
|
65
|
+
|
66
|
+
# define other event methods ...
|
67
|
+
end
|
68
|
+
|
69
|
+
project = Project.new
|
70
|
+
project.active? # => true
|
71
|
+
project.new? # => true
|
72
|
+
project.published? # => false
|
73
|
+
```
|
74
|
+
|
75
|
+
If you are using with Mongoid a field called state will automatically be created for you.
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
class Project
|
79
|
+
include Mongoid::Document # must be included first
|
80
|
+
include Stateful
|
81
|
+
|
82
|
+
field :published_at, type: Time
|
83
|
+
|
84
|
+
stateful default: :new,
|
85
|
+
events: [:publish, :unpublish, :approve, :close, :mark_as_duplicate],
|
86
|
+
states: {
|
87
|
+
active: {
|
88
|
+
new: :published,
|
89
|
+
published: {
|
90
|
+
needs_approval: [:approved, :duplicate],
|
91
|
+
approved: :closed
|
92
|
+
}
|
93
|
+
},
|
94
|
+
inactive: {
|
95
|
+
closed: nil,
|
96
|
+
duplicate: nil
|
97
|
+
}
|
98
|
+
}
|
99
|
+
|
100
|
+
|
101
|
+
# ...
|
102
|
+
end
|
103
|
+
|
104
|
+
# scopes are automatically created for you
|
105
|
+
Project.active.count
|
106
|
+
Project.published.count
|
107
|
+
```
|
108
|
+
|
109
|
+
## Contributing
|
110
|
+
|
111
|
+
1. Fork it
|
112
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
113
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
114
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
115
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Stateful
|
2
|
+
module Mongoid
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
protected
|
7
|
+
|
8
|
+
def define_state_attribute(options)
|
9
|
+
field :state, type: Symbol, default: options[:default]
|
10
|
+
validates_inclusion_of :state,
|
11
|
+
in: state_infos.keys,
|
12
|
+
message: options.has_key?(:message) ? options[:message] : 'invalid state value',
|
13
|
+
allow_nil: !!options[:allow_nil]
|
14
|
+
|
15
|
+
# configure scopes to query the attribute value
|
16
|
+
state_infos.values.each do |info|
|
17
|
+
states = info.collect_child_states
|
18
|
+
if states.length == 1
|
19
|
+
scope info.name, where(state: states.first)
|
20
|
+
else
|
21
|
+
scope info.name, where(:state.in => states)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Stateful
|
2
|
+
class StateInfo
|
3
|
+
attr_reader :parent, :children, :name, :to_transitions
|
4
|
+
def initialize(state_class, parent, name, config)
|
5
|
+
@state_class = state_class
|
6
|
+
if parent
|
7
|
+
@parent = parent
|
8
|
+
parent.children << self
|
9
|
+
end
|
10
|
+
|
11
|
+
@name = name
|
12
|
+
@to_transitions = []
|
13
|
+
|
14
|
+
if config.is_a?(Hash)
|
15
|
+
@groupConfig = config
|
16
|
+
@children = []
|
17
|
+
else
|
18
|
+
@to_transitions = config ? (config.is_a?(Array) ? config : [config]) : []
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def is?(state)
|
23
|
+
(@name == state or (parent and parent.is?(state)))
|
24
|
+
end
|
25
|
+
|
26
|
+
def is_group?
|
27
|
+
!!@groupConfig
|
28
|
+
end
|
29
|
+
|
30
|
+
def can_transition_to?(state)
|
31
|
+
state_info = @state_class.state_infos[state]
|
32
|
+
if is_group? or state_info.nil? or state_info.is_group?
|
33
|
+
false
|
34
|
+
else
|
35
|
+
to_transitions.include?(state)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def collect_child_states
|
40
|
+
is_group? ? children.flat_map(&:collect_child_states) : [name]
|
41
|
+
end
|
42
|
+
|
43
|
+
def expand_to_transitions
|
44
|
+
if to_transitions.any?
|
45
|
+
@to_transitions = to_transitions.flat_map do |to|
|
46
|
+
info = @state_class.state_infos[to]
|
47
|
+
|
48
|
+
if info.is_group?
|
49
|
+
info.collect_child_states
|
50
|
+
else
|
51
|
+
[info.name]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/stateful.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
require "stateful/version"
|
2
|
+
require "stateful/state_info"
|
3
|
+
|
4
|
+
module Stateful
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
include ActiveSupport::Callbacks
|
7
|
+
|
8
|
+
included do
|
9
|
+
if defined?(Mongoid)
|
10
|
+
require 'mongoid/document'
|
11
|
+
include Stateful::Mongoid if included_modules.include?(::Mongoid::Document)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
def state_infos
|
17
|
+
@state_infos ||= {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def stateful(options)
|
21
|
+
options[:events] ||= []
|
22
|
+
|
23
|
+
define_method 'state_events' do
|
24
|
+
options[:events]
|
25
|
+
end
|
26
|
+
|
27
|
+
define_method 'state_info' do
|
28
|
+
self.class.state_infos[self.state]
|
29
|
+
end
|
30
|
+
|
31
|
+
define_method 'state_valid?' do
|
32
|
+
self.class.state_infos.keys.include?(state)
|
33
|
+
end
|
34
|
+
|
35
|
+
define_method 'change_state' do |new_state, options = {}, &block|
|
36
|
+
return false if new_state == state
|
37
|
+
return false unless state_info.can_transition_to?(new_state)
|
38
|
+
|
39
|
+
# convert shortcut event name to options hash
|
40
|
+
options = {event: options} if options.is_a? Symbol
|
41
|
+
options[:persist_methods] = [:persist_state, :save]
|
42
|
+
|
43
|
+
_change_state(new_state, options, &block)
|
44
|
+
end
|
45
|
+
|
46
|
+
define_method 'change_state!' do |new_state, options = {}, &block|
|
47
|
+
return false if new_state == state
|
48
|
+
raise "transition from #{state} to #{new_state} not allowed" unless state_info.can_transition_to?(new_state)
|
49
|
+
|
50
|
+
# convert shortcut event name to options hash
|
51
|
+
options = {event: options} if options.is_a? Symbol
|
52
|
+
options[:persist_methods] = [:persist_state!, :save!]
|
53
|
+
_change_state(new_state, options, &block)
|
54
|
+
end
|
55
|
+
|
56
|
+
define_method '_change_state' do |new_state, options, &block|
|
57
|
+
# do a little magic and infer the event name from the method name used to call change_state
|
58
|
+
# TODO: decide if this is too magical, for now it has been commented out.
|
59
|
+
#unless options[:event]
|
60
|
+
# calling_method = caller[1][/`.*'/][1..-2].gsub('!', '').to_sym
|
61
|
+
# options[:event] = calling_method if state_events.include? calling_method
|
62
|
+
#end
|
63
|
+
|
64
|
+
if block and block.call == false
|
65
|
+
false
|
66
|
+
else
|
67
|
+
callbacks = [:state_change]
|
68
|
+
callbacks << options[:event] if options[:event]
|
69
|
+
run_callbacks *callbacks do
|
70
|
+
self.state = new_state
|
71
|
+
if options[:persist_methods]
|
72
|
+
method = options[:persist_methods].find {|m| respond_to?(m)}
|
73
|
+
__send__(method) if method
|
74
|
+
end
|
75
|
+
if respond_to?(:persist_state)
|
76
|
+
persist_state
|
77
|
+
elsif respond_to?(:save!)
|
78
|
+
save!
|
79
|
+
end
|
80
|
+
end
|
81
|
+
true
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
private :_change_state
|
86
|
+
|
87
|
+
define_method 'can_transition_to?' do |new_state|
|
88
|
+
state_info.can_transition_to?(new_state)
|
89
|
+
end
|
90
|
+
|
91
|
+
# init and configure state info
|
92
|
+
init_state_info(options[:states])
|
93
|
+
state_infos.values.each do |info|
|
94
|
+
info.expand_to_transitions
|
95
|
+
|
96
|
+
define_method "#{info.name}?" do
|
97
|
+
info.is?(self.state)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
define_state_attribute(options)
|
103
|
+
|
104
|
+
# define the event callbacks
|
105
|
+
events = ([:state_change] + options[:events])
|
106
|
+
define_callbacks *events
|
107
|
+
|
108
|
+
# define callback helpers
|
109
|
+
events.each do |event|
|
110
|
+
define_singleton_method "before_#{event}" do |method = nil, &block|
|
111
|
+
set_callback(event, :before, method ? method : block)
|
112
|
+
end
|
113
|
+
|
114
|
+
define_singleton_method "after_#{event}" do |method = nil, &block|
|
115
|
+
set_callback(event, :after, method ? method : block)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
protected
|
121
|
+
def define_state_attribute(options)
|
122
|
+
define_method 'state' do
|
123
|
+
instance_variable_get(:@state) || options[:default]
|
124
|
+
end
|
125
|
+
|
126
|
+
define_method 'state=' do |val|
|
127
|
+
instance_variable_set(:@state, val)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def init_state_info(values, parent = nil)
|
134
|
+
values.each do |name, config|
|
135
|
+
info = state_infos[name] = Stateful::StateInfo.new(self, parent, name, config)
|
136
|
+
init_state_info(config, info) if info.is_group?
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
require 'stateful/mongoid' if defined?(Mongoid)
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require './lib/stateful'
|
3
|
+
require 'mongoid'
|
4
|
+
|
5
|
+
|
6
|
+
class Kata
|
7
|
+
include Mongoid::Document
|
8
|
+
include Stateful
|
9
|
+
|
10
|
+
stateful default: :draft, events: [:publish, :approve, :retire], states: {
|
11
|
+
:draft => :beta,
|
12
|
+
beta: {
|
13
|
+
:needs_testing => :needs_approval,
|
14
|
+
:needs_approval => :approved
|
15
|
+
},
|
16
|
+
:approved => :retired,
|
17
|
+
:retired => nil
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
describe Stateful::Mongoid do
|
22
|
+
let(:kata) {Kata.new}
|
23
|
+
|
24
|
+
it 'should support creating a state field' do
|
25
|
+
Kata.fields.keys.include?('state').should be_true
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should support validating state values' do
|
29
|
+
kata.state.should == :draft
|
30
|
+
kata.valid?.should be_true
|
31
|
+
kata.state = :invalid
|
32
|
+
kata.valid?.should be_false
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should support can_transition_to?' do
|
36
|
+
kata.can_transition_to?(:beta).should be_true
|
37
|
+
kata.can_transition_to?(:retired).should be_false
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should create scopes for each state and virtual state' do
|
41
|
+
Kata.beta.selector.should == {"state" => {"$in" => [:needs_testing, :needs_approval]}}
|
42
|
+
Kata.draft.selector.should == {"state" => :draft}
|
43
|
+
end
|
44
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
require 'mongoid'
|
6
|
+
require 'mongoid/document'
|
7
|
+
|
8
|
+
#
|
9
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
10
|
+
RSpec.configure do |config|
|
11
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
12
|
+
config.run_all_when_everything_filtered = true
|
13
|
+
config.filter_run :focus
|
14
|
+
|
15
|
+
# Run specs in random order to surface order dependencies. If you find an
|
16
|
+
# order dependency and want to debug it, you can fix the order by providing
|
17
|
+
# the seed, which is printed after each run.
|
18
|
+
# --seed 1234
|
19
|
+
config.order = 'random'
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require './lib/stateful'
|
3
|
+
|
4
|
+
class Kata
|
5
|
+
include Stateful
|
6
|
+
|
7
|
+
attr_accessor :approved_by, :ready_score, :published_at, :state_changes
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@ready_score = 0
|
11
|
+
@state_changes = 0
|
12
|
+
end
|
13
|
+
|
14
|
+
stateful default: :draft,
|
15
|
+
events: [:publish, :unpublish, :approve, :retire],
|
16
|
+
states: {
|
17
|
+
:draft => :beta,
|
18
|
+
published: {
|
19
|
+
beta: {
|
20
|
+
:needs_feedback => [:draft, :needs_approval],
|
21
|
+
:needs_approval => [:draft, :approved]
|
22
|
+
},
|
23
|
+
:approved => :retired
|
24
|
+
},
|
25
|
+
:retired => nil
|
26
|
+
}
|
27
|
+
|
28
|
+
|
29
|
+
after_state_change do |doc|
|
30
|
+
doc.state_changes += 1
|
31
|
+
end
|
32
|
+
|
33
|
+
def vote(ready)
|
34
|
+
@ready_score += ready ? 1 : -1
|
35
|
+
|
36
|
+
# votes only affect state when in beta
|
37
|
+
if beta?
|
38
|
+
if enough_votes_for_approval? and needs_feedback?
|
39
|
+
change_state(:needs_approval)
|
40
|
+
elsif not enough_votes_for_approval? and needs_approval?
|
41
|
+
change_state(:needs_feedback)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def publish
|
47
|
+
change_state(enough_votes_for_approval? ? :needs_approval : :needs_feedback) do
|
48
|
+
@published_at = Time.now
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def unpublish
|
53
|
+
change_state(:draft)
|
54
|
+
end
|
55
|
+
|
56
|
+
def approve(approved_by)
|
57
|
+
change_state(:approved) do
|
58
|
+
@approved_by = approved_by
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def retire
|
63
|
+
change_state(:retire)
|
64
|
+
end
|
65
|
+
|
66
|
+
def enough_votes_for_approval?
|
67
|
+
ready_score >= 10
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe Kata do
|
72
|
+
let(:kata) {Kata.new}
|
73
|
+
|
74
|
+
it 'should support state_infos' do
|
75
|
+
Kata.state_infos.should_not be_nil
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'should support default state' do
|
79
|
+
kata.state.should == :draft
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'should support state_info' do
|
83
|
+
kata.state_info.should_not be_nil
|
84
|
+
kata.state_info.name.should == :draft
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'should support simple boolean helper methods' do
|
88
|
+
kata.draft?.should be_true
|
89
|
+
kata.published?.should be_false
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'change_state' do
|
93
|
+
it 'should raise error when an invalid transition state is provided' do
|
94
|
+
expect{kata.change_state(:retired)}.to raise_error
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'should raise error when a group state is provided' do
|
98
|
+
expect{kata.change_state(:beta)}.to raise_error
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'should return false when state is the same' do
|
102
|
+
kata.change_state(:draft).should be_false
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'should support state_valid?' do
|
106
|
+
kata.state_valid?.should be_true
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'should change the state when a proper state is provided' do
|
110
|
+
kata.change_state(:needs_feedback).should be_true
|
111
|
+
kata.state.should == :needs_feedback
|
112
|
+
kata.change_state(:needs_approval).should be_true
|
113
|
+
kata.state.should == :needs_approval
|
114
|
+
kata.change_state(:draft).should be_true
|
115
|
+
kata.state.should == :draft
|
116
|
+
kata.change_state(:needs_approval).should be_true
|
117
|
+
kata.change_state(:approved).should be_true
|
118
|
+
kata.state.should == :approved
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'should support calling passed blocks when state is valid' do
|
122
|
+
kata.published_at.should be_nil
|
123
|
+
kata.publish
|
124
|
+
kata.published_at.should_not be_nil
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'should support ingoring passed blocked when state is not valid' do
|
128
|
+
kata.approve('test')
|
129
|
+
kata.approved?.should be_false
|
130
|
+
kata.approved_by.should be_nil
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'should support after callbacks methods' do
|
134
|
+
kata.publish
|
135
|
+
kata.state_changes.should == 1
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
describe Stateful::StateInfo do
|
140
|
+
it 'should support is?' do
|
141
|
+
Kata.state_infos[:draft].is?(:draft).should be_true
|
142
|
+
Kata.state_infos[:needs_feedback].is?(:published).should be_true
|
143
|
+
Kata.state_infos[:needs_feedback].is?(:beta).should be_true
|
144
|
+
Kata.state_infos[:approved].is?(:published).should be_true
|
145
|
+
Kata.state_infos[:approved].is?(:beta).should be_false
|
146
|
+
Kata.state_infos[:retired].is?(:beta).should be_false
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'should support expanded to transitions' do
|
150
|
+
Kata.state_infos[:draft].to_transitions.should == [:needs_feedback, :needs_approval]
|
151
|
+
Kata.state_infos[:needs_approval].to_transitions.should == [:draft, :approved]
|
152
|
+
|
153
|
+
Kata.state_infos[:retired].to_transitions.should be_empty
|
154
|
+
|
155
|
+
p Kata.instance_methods
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
data/stateful.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'stateful/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "stateful"
|
8
|
+
spec.version = Stateful::VERSION
|
9
|
+
spec.authors = ["jake hoffner"]
|
10
|
+
spec.email = ["jake@codewars.com"]
|
11
|
+
spec.description = %q{A simple state machine gem}
|
12
|
+
spec.summary = %q{A simple state machine gem. Works with plain ruby objects and Mongoid. This gem aims
|
13
|
+
to keep things simple.}
|
14
|
+
spec.homepage = ""
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files`.split($/)
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_dependency 'activesupport'
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency "rspec"
|
26
|
+
spec.add_development_dependency "mongoid", "~> 3.0"
|
27
|
+
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stateful
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- jake hoffner
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-12-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: mongoid
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.0'
|
83
|
+
description: A simple state machine gem
|
84
|
+
email:
|
85
|
+
- jake@codewars.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- .gitignore
|
91
|
+
- Gemfile
|
92
|
+
- LICENSE.txt
|
93
|
+
- README.md
|
94
|
+
- Rakefile
|
95
|
+
- lib/stateful.rb
|
96
|
+
- lib/stateful/mongoid.rb
|
97
|
+
- lib/stateful/state_info.rb
|
98
|
+
- lib/stateful/version.rb
|
99
|
+
- spec/mongoid_spec.rb
|
100
|
+
- spec/spec_helper.rb
|
101
|
+
- spec/stateful_spec.rb
|
102
|
+
- stateful.gemspec
|
103
|
+
homepage: ''
|
104
|
+
licenses:
|
105
|
+
- MIT
|
106
|
+
metadata: {}
|
107
|
+
post_install_message:
|
108
|
+
rdoc_options: []
|
109
|
+
require_paths:
|
110
|
+
- lib
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - '>='
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - '>='
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
requirements: []
|
122
|
+
rubyforge_project:
|
123
|
+
rubygems_version: 2.0.3
|
124
|
+
signing_key:
|
125
|
+
specification_version: 4
|
126
|
+
summary: A simple state machine gem. Works with plain ruby objects and Mongoid. This
|
127
|
+
gem aims to keep things simple.
|
128
|
+
test_files:
|
129
|
+
- spec/mongoid_spec.rb
|
130
|
+
- spec/spec_helper.rb
|
131
|
+
- spec/stateful_spec.rb
|