solidstate 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.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/README.md +104 -0
- data/Rakefile +2 -0
- data/lib/solidstate.rb +92 -0
- data/solidstate.gemspec +23 -0
- metadata +125 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 10ee93a954561985d002e7f8db5ffe186a792bba
|
4
|
+
data.tar.gz: e979280cea908e17a924718f7f20a247d79c11b9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3480386e65adfdd682c7bd05a4a67d49a50660ac102b16fc7cbe7c334d50ab3829989b0e710af6a6e6f8d54d7b1ec352736727adb2f7955a2664a4103c70ce01
|
7
|
+
data.tar.gz: 1ccbbca8ab19e933ff4894036fdf46f9c48daa1c264d37d573917303848f1b34d90e269f2de771a451e508028d697ebf381c8d3cacabf497701e61c41f0c9ace
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
SolidState
|
2
|
+
==========
|
3
|
+
|
4
|
+
Minuscule but solid state machine for Ruby classes. The only dependency is that your model responds to a getter and setter for `state`.
|
5
|
+
|
6
|
+
``` ruby
|
7
|
+
|
8
|
+
# simplest example, using just an accessor
|
9
|
+
class Post
|
10
|
+
include SolidState
|
11
|
+
|
12
|
+
attr_accessor :state
|
13
|
+
states :draft, :published
|
14
|
+
end
|
15
|
+
|
16
|
+
# in its simplest form you just declare the possible states.
|
17
|
+
# if it's a simple class you just get boolean methods for checking
|
18
|
+
# whether the current status is X or Y.
|
19
|
+
|
20
|
+
p = Post.new
|
21
|
+
p.state # => 'draft'
|
22
|
+
p.draft? # true
|
23
|
+
p.published? # => false
|
24
|
+
p.state = 'published'
|
25
|
+
p.published? # => true
|
26
|
+
|
27
|
+
# now, if the model class responds to validates_inclusion_of, it will
|
28
|
+
# mark the record invalid if an unknown state is set.
|
29
|
+
|
30
|
+
# let's assume this is actually an ActiveRecord class, and the
|
31
|
+
# table contains a column named 'state'.
|
32
|
+
|
33
|
+
class Post < ActiveRecord::Base
|
34
|
+
include SolidState
|
35
|
+
|
36
|
+
states :draft, :published
|
37
|
+
end
|
38
|
+
|
39
|
+
p = Post.new
|
40
|
+
p.state = 'published'
|
41
|
+
p.valid? # => true
|
42
|
+
p.state = 'deleted'
|
43
|
+
p.valid? # => false
|
44
|
+
|
45
|
+
# ok, now let's gets get fancier. we're going to declare transitions
|
46
|
+
# which will govern the possible directions in which an object's state
|
47
|
+
# can move to.
|
48
|
+
|
49
|
+
class Subscriber < ActiveRecord::Base
|
50
|
+
include SolidState
|
51
|
+
|
52
|
+
states :inactive, :active, :unsubscribed, :disabled do
|
53
|
+
transitions :from => :inactive, :to => :active
|
54
|
+
transitions :from => :active, :to => [:unsubscribed, :disabled]
|
55
|
+
transitions :from => :unsubscribed, :to => :active
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
s = Subscriber.new
|
60
|
+
s.state # => 'inactive'
|
61
|
+
|
62
|
+
# since we declared transitions, we can now call #{state}! which
|
63
|
+
# checks whether the instance can transition to that state and
|
64
|
+
# if so, sets the new state and optionally saves the record.
|
65
|
+
|
66
|
+
s.active! # => true
|
67
|
+
|
68
|
+
s.inactive! # => raises InvalidTransitionError
|
69
|
+
|
70
|
+
# this also works outside transition methods, of course.
|
71
|
+
s.reload # => true
|
72
|
+
s.active? # => true
|
73
|
+
|
74
|
+
s.state = 'inactive'
|
75
|
+
s.valid? # => false
|
76
|
+
|
77
|
+
# the last trick this library does is that it optionally lets you
|
78
|
+
# declare callback methods that are called whenever a transition
|
79
|
+
# method succeeds. just define a method called #once_[state] in
|
80
|
+
# your model.
|
81
|
+
|
82
|
+
class Subscriber
|
83
|
+
def once_unsubscribed
|
84
|
+
puts "Sorry to see you go!"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# ...
|
89
|
+
s.unsubscribed! # => prints "Sorry to see you go!"
|
90
|
+
```
|
91
|
+
|
92
|
+
That's about it. For examples check the `examples` directory in this repo.
|
93
|
+
|
94
|
+
# Contributions
|
95
|
+
|
96
|
+
You're more than welcome. Send a pull request, including tests, and make sure you don't break anything. That's it.
|
97
|
+
|
98
|
+
# Author
|
99
|
+
|
100
|
+
Tomás Pollak
|
101
|
+
|
102
|
+
# Copyright
|
103
|
+
|
104
|
+
(c) Fork Limited. MIT license.
|
data/Rakefile
ADDED
data/lib/solidstate.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
module SolidState
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
class InvalidTransitionError < ArgumentError; end
|
5
|
+
|
6
|
+
STATE_ATTRIBUTE = :state.freeze
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
def states(*list, &block)
|
11
|
+
raise "This is not a list of names" unless list.first.is_a?(String)
|
12
|
+
list = list.collect(&:to_s)
|
13
|
+
|
14
|
+
@@states = list
|
15
|
+
@@state_transitions = {}
|
16
|
+
|
17
|
+
def self.states
|
18
|
+
@@states
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.state_transitions
|
22
|
+
@@state_transitions
|
23
|
+
end
|
24
|
+
|
25
|
+
if respond_to?(:validates_inclusion_of)
|
26
|
+
validates_inclusion_of STATE_ATTRIBUTE, in: list
|
27
|
+
end
|
28
|
+
|
29
|
+
scope :with_state, lambda { |state|
|
30
|
+
return query if state.blank?
|
31
|
+
where(STATE_ATTRIBUTE => state)
|
32
|
+
} if respond_to?(:scope)
|
33
|
+
|
34
|
+
list.each do |s|
|
35
|
+
scope(s, lambda { where(STATE_ATTRIBUTE => s) }) if respond_to?(:scope)
|
36
|
+
|
37
|
+
define_method "#{s}?" do
|
38
|
+
state.to_sym == s.to_sym
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
yield if block_given?
|
43
|
+
end
|
44
|
+
|
45
|
+
def transitions(opts)
|
46
|
+
validate :ensure_valid_transition if respond_to?(:validate)
|
47
|
+
|
48
|
+
from = opts.delete(:from) or raise ":from required"
|
49
|
+
to = opts.delete(:to) or raise ":to required"
|
50
|
+
to = [to] unless to.is_a?(Array)
|
51
|
+
|
52
|
+
# puts "From #{from} to #{to.join(' or ')}"
|
53
|
+
to.each do |dest|
|
54
|
+
|
55
|
+
state_transitions[from.to_sym] ||= []
|
56
|
+
state_transitions[from.to_sym].push(dest.to_sym)
|
57
|
+
|
58
|
+
define_method("#{dest}!") do
|
59
|
+
unless set_state(dest.to_sym)
|
60
|
+
raise InvalidTransition.new("Cannot transition from #{state} to #{dest}")
|
61
|
+
end
|
62
|
+
|
63
|
+
if !respond_to?(:valid?) or (valid? && save)
|
64
|
+
send("once_#{dest}", from) if respond_to?("once_#{dest}")
|
65
|
+
true
|
66
|
+
else
|
67
|
+
false
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def set_state(new_state)
|
75
|
+
return false unless can_transition_to?(new_state)
|
76
|
+
self.state = new_state
|
77
|
+
end
|
78
|
+
|
79
|
+
def ensure_valid_transition
|
80
|
+
if send("#{STATE_ATTRIBUTE}_changed?") and !can_transition_to?(state)
|
81
|
+
errors.add(STATE_ATTRIBUTE, "can't transition from current state to #{state}")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def can_transition_to?(new_state)
|
86
|
+
return true if state.to_sym == new_state.to_sym
|
87
|
+
|
88
|
+
possible = self.class.state_transitions[state.to_sym] || []
|
89
|
+
possible.include?(new_state.to_sym)
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
data/solidstate.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "solidstate"
|
5
|
+
s.version = '0.1.0'
|
6
|
+
s.platform = Gem::Platform::RUBY
|
7
|
+
s.authors = ['Tomás Pollak']
|
8
|
+
s.email = ['tomas@forkhq.com']
|
9
|
+
s.homepage = "https://github.com/tomas/solidstate"
|
10
|
+
s.summary = "Minuscule state machine."
|
11
|
+
s.description = "Minuscule state machine."
|
12
|
+
|
13
|
+
s.required_rubygems_version = ">= 1.3.6"
|
14
|
+
s.add_development_dependency "bundler", ">= 1.0.0"
|
15
|
+
s.add_development_dependency "rspec", '~> 3.0', '>= 3.0.0'
|
16
|
+
s.add_development_dependency "activerecord" #, ">= 4.0.0"
|
17
|
+
s.add_development_dependency "sqlite3" #, ">= 4.0.0"
|
18
|
+
s.add_development_dependency "mongo_mapper" #, ">= 4.0.0"
|
19
|
+
|
20
|
+
s.files = `git ls-files`.split("\n")
|
21
|
+
s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
|
22
|
+
s.require_path = 'lib'
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: solidstate
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tomás Pollak
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-03-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.0.0
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: 3.0.0
|
37
|
+
type: :development
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - "~>"
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '3.0'
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 3.0.0
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: activerecord
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: sqlite3
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: mongo_mapper
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
description: Minuscule state machine.
|
90
|
+
email:
|
91
|
+
- tomas@forkhq.com
|
92
|
+
executables: []
|
93
|
+
extensions: []
|
94
|
+
extra_rdoc_files: []
|
95
|
+
files:
|
96
|
+
- Gemfile
|
97
|
+
- README.md
|
98
|
+
- Rakefile
|
99
|
+
- lib/solidstate.rb
|
100
|
+
- solidstate.gemspec
|
101
|
+
homepage: https://github.com/tomas/solidstate
|
102
|
+
licenses: []
|
103
|
+
metadata: {}
|
104
|
+
post_install_message:
|
105
|
+
rdoc_options: []
|
106
|
+
require_paths:
|
107
|
+
- lib
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 1.3.6
|
118
|
+
requirements: []
|
119
|
+
rubyforge_project:
|
120
|
+
rubygems_version: 2.2.0
|
121
|
+
signing_key:
|
122
|
+
specification_version: 4
|
123
|
+
summary: Minuscule state machine.
|
124
|
+
test_files: []
|
125
|
+
has_rdoc:
|