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.
Files changed (7) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +4 -0
  3. data/README.md +104 -0
  4. data/Rakefile +2 -0
  5. data/lib/solidstate.rb +92 -0
  6. data/solidstate.gemspec +23 -0
  7. 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
@@ -0,0 +1,4 @@
1
+ source :rubygems
2
+
3
+ # specified in tuktuk.gemspec
4
+ gemspec
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
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
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
@@ -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: