class_state 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/.gitignore +38 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +36 -0
- data/LICENSE +22 -0
- data/Rakefile +9 -0
- data/class_state.gemspec +19 -0
- data/lib/class_state.rb +3 -0
- data/lib/class_state/class_state.rb +150 -0
- data/lib/class_state/owner.rb +63 -0
- data/spec/class_state_owner_spec.rb +215 -0
- data/spec/class_state_spec.rb +289 -0
- data/spec/spec_helper.rb +17 -0
- metadata +83 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 87cb7c7d36cf428d6d9189beedc5f6f07e2ee8a2
|
4
|
+
data.tar.gz: 7c6b831852faa2b02c5e507b5a2e908c6fcc286e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 369146d994e3f024d0402f4a8b746415a41b02019659a94346130689e9e4980346635aad180ac4f62cd3d345de74b4d57426857545b8438b68e5ea0d778aa641
|
7
|
+
data.tar.gz: ef2483a890b291b12d72bd614a360d06656f0d4aeb81e9fb1ce1ff2604c80ebb9c42f9cc5a03094d8f7e7927508d2a3502af218e353991cb7458875ef63e75cd
|
data/.gitignore
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/test/tmp/
|
9
|
+
/test/version_tmp/
|
10
|
+
/tmp/
|
11
|
+
|
12
|
+
## Specific to RubyMotion:
|
13
|
+
.dat*
|
14
|
+
.repl_history
|
15
|
+
build/
|
16
|
+
|
17
|
+
## Documentation cache and generated files:
|
18
|
+
/.yardoc/
|
19
|
+
/_yardoc/
|
20
|
+
/doc/
|
21
|
+
/rdoc/
|
22
|
+
|
23
|
+
## Environment normalisation:
|
24
|
+
/.bundle/
|
25
|
+
/vendor/bundle
|
26
|
+
/lib/bundler/man/
|
27
|
+
|
28
|
+
# for a library or gem, you might want to ignore these files since the code is
|
29
|
+
# intended to run in multiple environments; otherwise, check them in:
|
30
|
+
# Gemfile.lock
|
31
|
+
.ruby-version
|
32
|
+
.ruby-gemset
|
33
|
+
|
34
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
35
|
+
.rvmrc
|
36
|
+
|
37
|
+
# debugging
|
38
|
+
.byebug_history
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
class_state (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
byebug (8.2.1)
|
10
|
+
diff-lcs (1.2.5)
|
11
|
+
rake (10.5.0)
|
12
|
+
rspec (3.3.0)
|
13
|
+
rspec-core (~> 3.3.0)
|
14
|
+
rspec-expectations (~> 3.3.0)
|
15
|
+
rspec-mocks (~> 3.3.0)
|
16
|
+
rspec-core (3.3.1)
|
17
|
+
rspec-support (~> 3.3.0)
|
18
|
+
rspec-expectations (3.3.0)
|
19
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
20
|
+
rspec-support (~> 3.3.0)
|
21
|
+
rspec-mocks (3.3.1)
|
22
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
23
|
+
rspec-support (~> 3.3.0)
|
24
|
+
rspec-support (3.3.0)
|
25
|
+
|
26
|
+
PLATFORMS
|
27
|
+
ruby
|
28
|
+
|
29
|
+
DEPENDENCIES
|
30
|
+
byebug
|
31
|
+
class_state!
|
32
|
+
rake (~> 10.5)
|
33
|
+
rspec (~> 3.3)
|
34
|
+
|
35
|
+
BUNDLED WITH
|
36
|
+
1.11.2
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Mark
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
22
|
+
|
data/Rakefile
ADDED
data/class_state.gemspec
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "class_state"
|
3
|
+
s.version = '0.1.0'
|
4
|
+
s.date = '2016-02-21'
|
5
|
+
|
6
|
+
s.files = `git ls-files`.split($/)
|
7
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
8
|
+
s.require_paths = ["lib"]
|
9
|
+
|
10
|
+
s.add_development_dependency 'rake', '~> 10.5'
|
11
|
+
s.add_development_dependency 'rspec', '~> 3.3'
|
12
|
+
|
13
|
+
s.author = "Mark van de Korput"
|
14
|
+
s.email = "dr.theman@gmail.com"
|
15
|
+
s.description = %q{A ruby class for managing states class-states}
|
16
|
+
s.summary = %q{Provides logic and a framework for thinking about the state of your classes}
|
17
|
+
s.homepage = %q{https://github.com/markkorput/class_state}
|
18
|
+
s.license = "MIT"
|
19
|
+
end
|
data/lib/class_state.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
class ClassState
|
4
|
+
attr_reader :values
|
5
|
+
attr_reader :callback_definitions
|
6
|
+
|
7
|
+
def initialize(_values = {})
|
8
|
+
@callback_definitions = []
|
9
|
+
self.set(_values)
|
10
|
+
end
|
11
|
+
|
12
|
+
def logger
|
13
|
+
@_logger ||= Logger.new(STDOUT).tap do |l|
|
14
|
+
l.level = Logger::WARNING
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
# readers
|
20
|
+
|
21
|
+
def get(attr_name)
|
22
|
+
values[attr_name]
|
23
|
+
end
|
24
|
+
|
25
|
+
def [](attr_name)
|
26
|
+
values[attr_name]
|
27
|
+
end
|
28
|
+
|
29
|
+
def data
|
30
|
+
self.values
|
31
|
+
end
|
32
|
+
|
33
|
+
# writers
|
34
|
+
|
35
|
+
def []=(key, val)
|
36
|
+
return self.update(key => val)
|
37
|
+
end
|
38
|
+
|
39
|
+
def update(_values)
|
40
|
+
self.set(self.values.merge(_values))
|
41
|
+
return self
|
42
|
+
end
|
43
|
+
|
44
|
+
def set(_values)
|
45
|
+
original = self.values || {}
|
46
|
+
@values = _values
|
47
|
+
changes = get_changes_hash(original, _values)
|
48
|
+
|
49
|
+
changes.keys.each do |changed_attr|
|
50
|
+
trigger_attribute_callbacks(:change_attribute, changed_attr, self, changes)
|
51
|
+
end
|
52
|
+
|
53
|
+
if !changes.empty?
|
54
|
+
trigger_callbacks(:change, self, changes)
|
55
|
+
end
|
56
|
+
|
57
|
+
return self
|
58
|
+
end
|
59
|
+
|
60
|
+
def unset(attrs)
|
61
|
+
unsets = {}
|
62
|
+
|
63
|
+
[attrs].flatten.each do |attr_name|
|
64
|
+
if self.values.keys.include?(attr_name)
|
65
|
+
unsets.merge!(attr_name => self.values.delete(attr_name))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
unsets.keys.each do |changed_attr|
|
70
|
+
trigger_attribute_callbacks(:unset_attribute, changed_attr, self, unsets)
|
71
|
+
end
|
72
|
+
|
73
|
+
if !unsets.empty?
|
74
|
+
trigger_callbacks(:unset, self, unsets)
|
75
|
+
end
|
76
|
+
|
77
|
+
return unsets
|
78
|
+
end
|
79
|
+
|
80
|
+
def on(*args, &block)
|
81
|
+
callback_definition = {:event => args.first}
|
82
|
+
|
83
|
+
if block
|
84
|
+
callback_definition[:block] = block
|
85
|
+
callback_definition[:attribute] = args[1] # could be nil
|
86
|
+
else
|
87
|
+
if args.first == :change_attribute or args.first == :unset_attribute
|
88
|
+
callback_definition[:attribute] = args[1]
|
89
|
+
callback_definition[:subject] = args[2]
|
90
|
+
callback_definition[:method] = args[3]
|
91
|
+
else
|
92
|
+
callback_definition[:subject] = args[1]
|
93
|
+
callback_definition[:method] = args[2]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
if callback_definition[:block].nil? and callback_definition[:method].nil?
|
98
|
+
logger.warn "ClassState.on didn't get a callback method or block"
|
99
|
+
end
|
100
|
+
|
101
|
+
if !callback_definition[:method].nil?
|
102
|
+
callback_definition[:method] = callback_definition[:method].to_s.to_sym
|
103
|
+
end
|
104
|
+
|
105
|
+
# save definition
|
106
|
+
@callback_definitions << callback_definition
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def get_changes_hash(before, after)
|
112
|
+
# assume all new values as changes, but reject the ones that are still the same as before
|
113
|
+
changes = after.reject do |key, value|
|
114
|
+
before[key] == value # new value is same as old value; don't include in 'changes' data hash
|
115
|
+
end
|
116
|
+
|
117
|
+
# also include the removed attributes in the changes
|
118
|
+
before.each_pair do |key, value|
|
119
|
+
if !after.keys.include?(key)
|
120
|
+
changes.merge!(key => nil)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
return changes
|
125
|
+
end
|
126
|
+
|
127
|
+
def trigger_callbacks(event, *args)
|
128
|
+
self.callback_definitions.each do |callback_def|
|
129
|
+
if callback_def[:event] == event and callback_def[:attribute].nil?
|
130
|
+
if callback_def[:block]
|
131
|
+
callback_def[:block].call(*args)
|
132
|
+
else
|
133
|
+
callback_def[:subject].send(callback_def[:method], *args)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def trigger_attribute_callbacks(event, attr, *args)
|
140
|
+
self.callback_definitions.each do |callback_def|
|
141
|
+
if callback_def[:event] == event and callback_def[:attribute] == attr
|
142
|
+
if callback_def[:block].nil?
|
143
|
+
callback_def[:subject].send(callback_def[:method], *args)
|
144
|
+
else
|
145
|
+
callback_def[:block].call(*args)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end # of class ClassState
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'class_state/class_state'
|
2
|
+
|
3
|
+
# monkey patch the Owner module insto the ClassState class scope
|
4
|
+
class ClassState
|
5
|
+
module Owner
|
6
|
+
attr_reader :state
|
7
|
+
|
8
|
+
def self.included(cls)
|
9
|
+
cls.extend(ClassMethods)
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(_state_values = {})
|
13
|
+
@state ||= ClassState.new(_state_values)
|
14
|
+
end
|
15
|
+
|
16
|
+
def method_missing(method_name, *args)
|
17
|
+
# byebug
|
18
|
+
|
19
|
+
if method_name =~ /=$/
|
20
|
+
# byebug
|
21
|
+
if writer = self.class.state_writers.find{|state_writer| "#{state_writer[:name]}=" == method_name.to_s}
|
22
|
+
return self.state[writer[:attribute] || writer[:name]] = args.first
|
23
|
+
end
|
24
|
+
|
25
|
+
# throw NoMethodError
|
26
|
+
return super(method_name, *args)
|
27
|
+
end
|
28
|
+
|
29
|
+
if reader = self.class.state_readers.find{|state_reader| state_reader[:name].to_s == method_name.to_s}
|
30
|
+
return self.state[reader[:attribute] || reader[:name]] || reader[:default]
|
31
|
+
end
|
32
|
+
|
33
|
+
# throw NoMethodError
|
34
|
+
return super(method_name, *args)
|
35
|
+
end
|
36
|
+
|
37
|
+
module ClassMethods
|
38
|
+
def state_readers
|
39
|
+
@state_readers ||= []
|
40
|
+
end
|
41
|
+
|
42
|
+
def state_writers
|
43
|
+
@state_writers ||= []
|
44
|
+
end
|
45
|
+
|
46
|
+
def state_reader(name, opts = {})
|
47
|
+
state_readers << opts.merge(:name => name)
|
48
|
+
# self.define_method(name) do
|
49
|
+
# self.state[opts[:attribute] || name] || opts[:default]
|
50
|
+
# end
|
51
|
+
end
|
52
|
+
|
53
|
+
def state_writer(name, opts = {})
|
54
|
+
state_writers << opts.merge(:name => name)
|
55
|
+
end
|
56
|
+
|
57
|
+
def state_accessor(name, opts = {})
|
58
|
+
state_readers << opts.merge(:name => name)
|
59
|
+
state_writers << opts.merge(:name => name)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'class_state'
|
3
|
+
|
4
|
+
describe ClassState::Owner do
|
5
|
+
let(:klass){
|
6
|
+
Class.new do
|
7
|
+
include ClassState::Owner
|
8
|
+
end
|
9
|
+
}
|
10
|
+
|
11
|
+
let(:data){
|
12
|
+
{:treshold => 45, :verbose => false}
|
13
|
+
}
|
14
|
+
|
15
|
+
describe '.state' do
|
16
|
+
let(:instance){
|
17
|
+
klass.new(data)
|
18
|
+
}
|
19
|
+
|
20
|
+
it 'gives the instance\'s configuration object' do
|
21
|
+
expect(instance.state.class).to eq ClassState
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'allows you to access all ClassState functionality directly on the object itself' do
|
25
|
+
expect(instance.state.update(:treshold => 50))
|
26
|
+
expect(instance.state[:treshold]).to eq 50
|
27
|
+
expect(instance.state.data).to eq(:treshold => 50, :verbose => false)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '.initialize' do
|
32
|
+
it 'creates a default initialize method that accepts a hash parameter with configuration values' do
|
33
|
+
# without
|
34
|
+
expect(klass.new(:name => 'bill', :age => 45).state.data).to eq(:name => 'bill', :age => 45)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'configuration through initialize is optional' do
|
38
|
+
expect(klass.new.state.data).to eq({})
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# state attribute proxy methods
|
43
|
+
|
44
|
+
describe 'self.state_reader' do
|
45
|
+
let(:klass){
|
46
|
+
Class.new do
|
47
|
+
include ClassState::Owner
|
48
|
+
state_writer :name
|
49
|
+
end
|
50
|
+
}
|
51
|
+
|
52
|
+
let(:instance){
|
53
|
+
klass.new
|
54
|
+
}
|
55
|
+
|
56
|
+
it 'creates a state attribute-writer proxy method in the owner' do
|
57
|
+
expect(instance.state[:name]).to eq nil
|
58
|
+
instance.name = 'freddy'
|
59
|
+
expect(instance.state[:name]).to eq 'freddy'
|
60
|
+
end
|
61
|
+
|
62
|
+
describe ':attribute option' do
|
63
|
+
it 'lets the caller specify a state-attribute with a different name than the setter method' do
|
64
|
+
klass.state_writer :age, :attribute => :value
|
65
|
+
instance = klass.new
|
66
|
+
expect(instance.state.data).to eq({})
|
67
|
+
instance.age = 35
|
68
|
+
expect(instance.state.data).to eq(:value => 35)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe 'self.state_writer' do
|
74
|
+
let(:klass){
|
75
|
+
Class.new do
|
76
|
+
include ClassState::Owner
|
77
|
+
state_reader :name
|
78
|
+
end
|
79
|
+
}
|
80
|
+
|
81
|
+
let(:instance){
|
82
|
+
klass.new
|
83
|
+
}
|
84
|
+
|
85
|
+
it 'creates a state attribute-writer proxy method in the owner' do
|
86
|
+
instance.state.set(:name => 'bobby')
|
87
|
+
expect(instance.name).to eq 'bobby'
|
88
|
+
end
|
89
|
+
|
90
|
+
describe ':attribute option' do
|
91
|
+
it 'lets the caller specify a state-attribute with a different name than the setter method' do
|
92
|
+
# expect(instance.respond_to?(:age)).to eq false
|
93
|
+
instance.class.state_reader :age, :attribute => :year
|
94
|
+
# expect(instance.respond_to?(:age)).to eq true
|
95
|
+
# age getter method doesn't read the age state attribute
|
96
|
+
instance.state.set(:age => 23)
|
97
|
+
expect(instance.age).to eq nil
|
98
|
+
# it reads the year state attribute
|
99
|
+
instance.state.set(:year => 24)
|
100
|
+
expect(instance.age).to eq 24
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe ':default option' do
|
105
|
+
it 'lets the caller define a default value for when the ClassState\'s attribute is nil' do
|
106
|
+
# expect(instance.respond_to?(:foo)).to eq false
|
107
|
+
instance.class.state_reader :foo, :default => 'bar'
|
108
|
+
# expect(instance.respond_to?(:foo)).to eq true
|
109
|
+
expect(instance.state[:foo]).to eq nil # foo attribute not set in the ClassState
|
110
|
+
expect(instance.foo).to eq 'bar' # the getter returns the default value
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'plays nice with the :attribute option' do
|
114
|
+
# expect(instance.respond_to?(:foo)).to eq false
|
115
|
+
# expect(instance.respond_to?(:bar)).to eq false
|
116
|
+
instance.class.state_reader :foo, :attribute => :bar, :default => 'foo'
|
117
|
+
# expect(instance.respond_to?(:foo)).to eq true
|
118
|
+
# expect(instance.respond_to?(:bar)).to eq false
|
119
|
+
expect(instance.foo).to eq 'foo' # the getter returns the default value
|
120
|
+
instance.state.set(:bar => 'foobar')
|
121
|
+
expect(instance.foo).to eq 'foobar' # reads bar state attribute
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe 'self.state_accessor' do
|
127
|
+
let(:klass){
|
128
|
+
Class.new do
|
129
|
+
include ClassState::Owner
|
130
|
+
state_accessor :name
|
131
|
+
end
|
132
|
+
}
|
133
|
+
|
134
|
+
let(:instance){
|
135
|
+
klass.new
|
136
|
+
}
|
137
|
+
|
138
|
+
it 'creates state attribute-reader/writer proxy methods in the owner' do
|
139
|
+
expect(instance.state[:name]).to eq nil
|
140
|
+
instance.name = 'john'
|
141
|
+
expect(instance.state[:name]).to eq 'john'
|
142
|
+
expect(instance.name).to eq 'john'
|
143
|
+
end
|
144
|
+
|
145
|
+
describe ':attribute option' do
|
146
|
+
it 'lets the caller specify a state-attribute with a different name than the method' do
|
147
|
+
# no accessor methods
|
148
|
+
# expect(instance.respond_to?(:age)).to eq false
|
149
|
+
# expect(instance.respond_to?(:age=)).to eq false
|
150
|
+
# create methods
|
151
|
+
instance.class.state_accessor :age, :attribute => :years_old
|
152
|
+
# vieryf methods
|
153
|
+
# expect(instance.respond_to?(:age)).to eq true
|
154
|
+
# expect(instance.respond_to?(:age=)).to eq true
|
155
|
+
|
156
|
+
# initial status
|
157
|
+
expect(instance.state[:years_old]).to eq nil
|
158
|
+
expect(instance.age).to eq nil
|
159
|
+
# update
|
160
|
+
instance.age = '99'
|
161
|
+
|
162
|
+
# state verifications
|
163
|
+
expect(instance.state[:age]).to eq nil
|
164
|
+
expect(instance.state[:years_old]).to eq '99'
|
165
|
+
# reader method verification
|
166
|
+
expect(instance.age).to eq '99'
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
describe ':default option' do
|
171
|
+
it 'works like the self.state_reader :default option' do
|
172
|
+
instance.class.state_accessor :v1, :default => '100%'
|
173
|
+
expect(instance.state[:v1]).to eq nil
|
174
|
+
expect(instance.v1).to eq '100%'
|
175
|
+
instance.v1 = '200%'
|
176
|
+
expect(instance.v1).to eq '200%'
|
177
|
+
end
|
178
|
+
|
179
|
+
it 'plays nice with the :attribute option' do
|
180
|
+
instance.class.state_accessor :v2, :attribute => :percentage, :default => '10%'
|
181
|
+
expect(instance.state[:v2]).to eq nil
|
182
|
+
expect(instance.state[:percentage]).to eq nil
|
183
|
+
expect(instance.v2).to eq '10%'
|
184
|
+
instance.v2 = '20%'
|
185
|
+
expect(instance.v2).to eq '20%'
|
186
|
+
expect(instance.state[:percentage]).to eq '20%'
|
187
|
+
expect(instance.state.data.keys.include?(:v2)).to eq false
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
describe '.method_missing' do
|
193
|
+
let(:klass){
|
194
|
+
Class.new do
|
195
|
+
include ClassState::Owner
|
196
|
+
state_reader :name
|
197
|
+
end
|
198
|
+
}
|
199
|
+
|
200
|
+
let(:instance){
|
201
|
+
klass.new(:name => 'doe')
|
202
|
+
}
|
203
|
+
|
204
|
+
it 'is used to implement state_reader/_writer/_accessor behaviour' do
|
205
|
+
expect(instance.respond_to?(:name)).to eq false
|
206
|
+
expect{
|
207
|
+
expect(instance.name).to eq 'doe'
|
208
|
+
}.to_not raise_error
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'still raises the NoMethodError when invoked incorrectly' do
|
212
|
+
expect{ instance.address }.to raise_error(NoMethodError)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
@@ -0,0 +1,289 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'class_state'
|
3
|
+
|
4
|
+
describe ClassState do
|
5
|
+
let(:instance){
|
6
|
+
ClassState.new
|
7
|
+
}
|
8
|
+
|
9
|
+
# READERS
|
10
|
+
|
11
|
+
describe ".data" do
|
12
|
+
it 'gives a hash with the current values' do
|
13
|
+
expect(instance.data).to eq({})
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '.get' do
|
18
|
+
it 'gives the current value of a specified property' do
|
19
|
+
expect(instance.get(:name)).to eq nil
|
20
|
+
expect(instance.set(:name => 'billy').get(:name)).to eq 'billy'
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'returns nil when specified attribute is not set' do
|
24
|
+
expect(instance.get(:foo)).to eq nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '[]' do
|
29
|
+
it 'gives the current value of a specified property' do
|
30
|
+
expect(instance[:name]).to eq nil
|
31
|
+
expect(instance.set(:name => 'billy')[:name]).to eq 'billy'
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'returns nil when specified attribute is not set' do
|
35
|
+
expect(instance[:foo]).to eq nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# WRITERS
|
40
|
+
|
41
|
+
describe '[]=' do
|
42
|
+
it 'updates a specified state attibute' do
|
43
|
+
instance = ClassState.new.update(:name => 'johnny')
|
44
|
+
expect(instance.data).to eq({:name => 'johnny'})
|
45
|
+
instance[:name] = 'cash'
|
46
|
+
expect(instance.data).to eq({:name => 'cash'})
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '.update' do
|
51
|
+
it 'updates the current values with the given hash' do
|
52
|
+
instance.update(:name => 'rachel', :age => 25)
|
53
|
+
expect(instance.data).to eq({:name => 'rachel', :age => 25})
|
54
|
+
instance.update(:shoe_size => 39)
|
55
|
+
expect(instance.data).to eq({:name => 'rachel', :age => 25, :shoe_size => 39})
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'supports linked notation' do
|
59
|
+
expect(ClassState.new.update(:name => 'johnny').update(:name => 'cool-i-o').update(:age => 20).data).to eq({:name => 'cool-i-o', :age => 20})
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe '.set' do
|
64
|
+
it 'sets the current values to the given hash, overwriting any current values' do
|
65
|
+
instance.set(:name => 'rachel', :age => 25)
|
66
|
+
expect(instance.data).to eq({:name => 'rachel', :age => 25})
|
67
|
+
instance.set(:shoe_size => 39)
|
68
|
+
expect(instance.data).to eq({:shoe_size => 39})
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'supports linked notation' do
|
72
|
+
expect(ClassState.new.set(:name => 'johnny').set(:name => 'cool-i-o').set(:age => 20).data).to eq({:age => 20})
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe '.unset' do
|
77
|
+
it 'removes a previously set attribute' do
|
78
|
+
instance = ClassState.new(:percentage => 65, :id => 101)
|
79
|
+
expect(instance.data).to eq({:percentage => 65, :id => 101})
|
80
|
+
expect(instance.unset(:id)).to eq({:id => 101}) # it returns a hash of removed attributes/values
|
81
|
+
expect(instance[:id]).to eq nil
|
82
|
+
expect(instance.data).to eq({:percentage => 65})
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'accepts an array of attribute identifiers' do
|
86
|
+
instance = ClassState.new(:percentage => 65, :id => 101)
|
87
|
+
expect(instance.data).to eq({:percentage => 65, :id => 101})
|
88
|
+
expect(instance.unset([:id, :percentage])).to eq({:percentage => 65, :id => 101}) # it returns a hash of removed attributes/values
|
89
|
+
expect(instance.data).to eq({})
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'ignores specified unknown attributes' do
|
93
|
+
instance = ClassState.new(:percentage => 65, :id => 101)
|
94
|
+
expect(instance.unset(:foo)).to eq({}) # :foo attribute doesn't exist
|
95
|
+
expect(instance.unset([:foo, :bar])).to eq({}) # :foo and :bar attributes dosn't exist
|
96
|
+
expect(instance.data).to eq({:percentage => 65, :id => 101}) # nothing changed
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# CALLBACKS
|
101
|
+
|
102
|
+
describe '.on' do
|
103
|
+
describe 'general :change event' do
|
104
|
+
it 'accepts a block to run' do
|
105
|
+
# we're gonna 'record' changes into this array
|
106
|
+
recording = {}
|
107
|
+
instance.on(:change) do |state, changes|
|
108
|
+
recording.merge!(changes)
|
109
|
+
end
|
110
|
+
|
111
|
+
expect(recording).to eq({})
|
112
|
+
instance.set({:id => 123, :value => 'X'})
|
113
|
+
expect(recording).to eq({:id => 123, :value => 'X'})
|
114
|
+
instance.set({:record_id => 101})
|
115
|
+
expect(instance.data).to eq({:record_id => 101})
|
116
|
+
expect(recording).to eq({:id => nil, :value => nil, :record_id => 101})
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'accepts a subject and method pair' do
|
120
|
+
# create instance of temporary dummy class with callbacker method
|
121
|
+
recorder = Class.new(Object) do
|
122
|
+
def recording
|
123
|
+
@recording ||= {}
|
124
|
+
end
|
125
|
+
|
126
|
+
def callbacker(state, changes)
|
127
|
+
recording.merge!(changes)
|
128
|
+
end
|
129
|
+
end.new
|
130
|
+
|
131
|
+
instance.on(:change, recorder, :callbacker) # on change, call the 'callbacker' method on instance
|
132
|
+
expect(recorder.recording).to eq({}) # callback not triggered yet
|
133
|
+
instance.update(:change => 'is').update(:a => 'sound') # these trigger the callback
|
134
|
+
expect(recorder.recording).to eq({:change => 'is', :a => 'sound'}) # verify
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe ':change_attribute event' do
|
139
|
+
it 'runs a block callback' do
|
140
|
+
recording = {}
|
141
|
+
instance.on(:change_attribute, :id) do |state, changes|
|
142
|
+
recording.merge!(changes)
|
143
|
+
end
|
144
|
+
|
145
|
+
expect(recording).to eq({})
|
146
|
+
# change id to 1
|
147
|
+
instance.set(:name => 'billy', :id => 1)
|
148
|
+
# all changes are given in the callback
|
149
|
+
expect(recording).to eq({:name => 'billy', :id => 1})
|
150
|
+
# no changes to id
|
151
|
+
instance.update(:name => 'johnny')
|
152
|
+
expect(recording).to eq({:name => 'billy', :id => 1})
|
153
|
+
# change id to 2
|
154
|
+
instance.update(:id => 2)
|
155
|
+
expect(recording).to eq({:name => 'billy', :id => 2})
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'triggers a specified method on a given instance' do
|
159
|
+
# create instance of temporary dummy class with callbacker method
|
160
|
+
recorder = Class.new(Object) do
|
161
|
+
def recording
|
162
|
+
@recording ||= {}
|
163
|
+
end
|
164
|
+
|
165
|
+
def record(state, changes)
|
166
|
+
recording.merge!(changes)
|
167
|
+
end
|
168
|
+
end.new
|
169
|
+
|
170
|
+
# when a change to attribute 'id' happens,
|
171
|
+
# call method 'record' on recorder
|
172
|
+
instance.on(:change_attribute, :id, recorder, :record)
|
173
|
+
|
174
|
+
expect(recorder.recording).to eq({})
|
175
|
+
# change id to 1
|
176
|
+
instance.set(:name => 'billy', :id => 1)
|
177
|
+
expect(recorder.recording).to eq(:name => 'billy', :id => 1)
|
178
|
+
# no changes to id
|
179
|
+
instance.update(:name => 'johnny')
|
180
|
+
expect(recorder.recording).to eq(:name => 'billy', :id => 1)
|
181
|
+
# change id to 2
|
182
|
+
instance.update(:id => 2)
|
183
|
+
expect(recorder.recording).to eq(:name => 'billy', :id => 2)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe 'general :unset event' do
|
188
|
+
let(:instance){
|
189
|
+
ClassState.new(:id => 101, :value => 50)
|
190
|
+
}
|
191
|
+
|
192
|
+
it 'takes a callback as a block' do
|
193
|
+
# we're gonna 'record' changes here
|
194
|
+
recording = {}
|
195
|
+
|
196
|
+
instance.on(:unset) do |state, unsets|
|
197
|
+
recording.merge!(unsets)
|
198
|
+
end
|
199
|
+
|
200
|
+
# nothing yet
|
201
|
+
expect(recording).to eq({})
|
202
|
+
# set some initial values
|
203
|
+
instance.update({:id => 123, :value => 'X'})
|
204
|
+
# callback not triggered yet
|
205
|
+
expect(recording).to eq({})
|
206
|
+
# id
|
207
|
+
instance.unset(:id)
|
208
|
+
expect(recording).to eq({:id => 123})
|
209
|
+
# value
|
210
|
+
instance.unset(:value)
|
211
|
+
expect(recording).to eq({:id => 123, :value => 'X'})
|
212
|
+
# nothing
|
213
|
+
instance.unset(:foo)
|
214
|
+
expect(recording).to eq({:id => 123, :value => 'X'})
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'takes a callback as a subject/method pair' do
|
218
|
+
# create instance of temporary dummy class with callbacker method
|
219
|
+
recorder = Class.new(Object) do
|
220
|
+
def recording
|
221
|
+
@recording ||= {}
|
222
|
+
end
|
223
|
+
|
224
|
+
def callbacker(state, unsets)
|
225
|
+
recording.merge!(unsets)
|
226
|
+
end
|
227
|
+
end.new
|
228
|
+
|
229
|
+
instance = ClassState.new(:a => 'b', :c => 'd')
|
230
|
+
# on 'unset' event, call the 'callbacker' method on instance
|
231
|
+
instance.on(:unset, recorder, :callbacker)
|
232
|
+
expect(recorder.recording).to eq({}) # callback not triggered yet
|
233
|
+
instance.unset([:a, :c]) # trigger the callback
|
234
|
+
expect(recorder.recording).to eq({:a => 'b', :c => 'd'}) # verify
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
describe ':unset_attribute event' do
|
239
|
+
let(:instance){
|
240
|
+
ClassState.new(:id => 101, :value => 50)
|
241
|
+
}
|
242
|
+
|
243
|
+
it 'takes a callback as a block' do
|
244
|
+
# we're gonna 'record' changes to id attribute here
|
245
|
+
recording = {}
|
246
|
+
|
247
|
+
instance.on(:unset_attribute, :id) do |state, unsets|
|
248
|
+
recording.merge!(unsets)
|
249
|
+
end
|
250
|
+
|
251
|
+
# nothing yet
|
252
|
+
expect(recording).to eq({})
|
253
|
+
# set some initial values
|
254
|
+
instance.update({:id => 123, :value => 'X'})
|
255
|
+
# callback not triggered yet
|
256
|
+
expect(recording).to eq({})
|
257
|
+
# id
|
258
|
+
instance.unset(:id)
|
259
|
+
expect(recording).to eq({:id => 123})
|
260
|
+
# value
|
261
|
+
instance.unset(:value)
|
262
|
+
expect(recording).to eq({:id => 123})
|
263
|
+
end
|
264
|
+
|
265
|
+
it 'takes a callback as a subject/method pair' do
|
266
|
+
# create instance of temporary dummy class with callbacker method
|
267
|
+
recorder = Class.new(Object) do
|
268
|
+
def recording
|
269
|
+
@recording ||= {}
|
270
|
+
end
|
271
|
+
|
272
|
+
def callbacker(state, unsets)
|
273
|
+
recording.merge!(unsets)
|
274
|
+
end
|
275
|
+
end.new
|
276
|
+
|
277
|
+
instance = ClassState.new(:a => 'b', :c => 'd')
|
278
|
+
# when the :id attribute is unset, call the 'callbacker' method on instance
|
279
|
+
instance.on(:unset_attribute, :a, recorder, :callbacker)
|
280
|
+
expect(recorder.recording).to eq({}) # callback not triggered yet
|
281
|
+
instance.unset(:c) # this doesn't trigger the callback
|
282
|
+
expect(recorder.recording).to eq({}) # callback still not triggered
|
283
|
+
instance.unset(:a) # triggers the callback
|
284
|
+
expect(recorder.recording).to eq({:a => 'b'}) # verify
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# loads and runs all tests for the rxsd project
|
2
|
+
#
|
3
|
+
# Copyright (C) 2010 Mohammed Morsi <movitto@yahoo.com>
|
4
|
+
# Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
|
5
|
+
|
6
|
+
require 'rspec'
|
7
|
+
|
8
|
+
require 'logger'
|
9
|
+
|
10
|
+
begin
|
11
|
+
require 'byebug'
|
12
|
+
rescue LoadError => e
|
13
|
+
Logger.new(STDOUT).warn("Could not load byebug, continuing without it")
|
14
|
+
end
|
15
|
+
|
16
|
+
$: << File.expand_path('../lib', File.dirname(__FILE__))
|
17
|
+
|
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: class_state
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mark van de Korput
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-02-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '10.5'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '10.5'
|
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.3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.3'
|
41
|
+
description: A ruby class for managing states class-states
|
42
|
+
email: dr.theman@gmail.com
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- ".gitignore"
|
48
|
+
- Gemfile
|
49
|
+
- Gemfile.lock
|
50
|
+
- LICENSE
|
51
|
+
- Rakefile
|
52
|
+
- class_state.gemspec
|
53
|
+
- lib/class_state.rb
|
54
|
+
- lib/class_state/class_state.rb
|
55
|
+
- lib/class_state/owner.rb
|
56
|
+
- spec/class_state_owner_spec.rb
|
57
|
+
- spec/class_state_spec.rb
|
58
|
+
- spec/spec_helper.rb
|
59
|
+
homepage: https://github.com/markkorput/class_state
|
60
|
+
licenses:
|
61
|
+
- MIT
|
62
|
+
metadata: {}
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
requirements: []
|
78
|
+
rubyforge_project:
|
79
|
+
rubygems_version: 2.5.1
|
80
|
+
signing_key:
|
81
|
+
specification_version: 4
|
82
|
+
summary: Provides logic and a framework for thinking about the state of your classes
|
83
|
+
test_files: []
|