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.
@@ -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
@@ -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
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'byebug'
7
+ end
@@ -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
+
@@ -0,0 +1,9 @@
1
+ require 'rdoc/task'
2
+ require "rspec/core/rake_task"
3
+
4
+ task :default => :rspec do; end
5
+
6
+ desc "Run all specs"
7
+ RSpec::Core::RakeTask.new('rspec') do |t|
8
+ t.pattern = 'spec/**/*_spec.rb'
9
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ require 'class_state/class_state'
2
+ require 'class_state/owner'
3
+
@@ -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
+
@@ -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: []