class_state 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []