observable_object 0.1.1

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: 849657362f62135a7a2070a2a75b2e06290a7a40
4
+ data.tar.gz: 94c856a7f9dae40da13a877fa13f2acb53b9a94d
5
+ SHA512:
6
+ metadata.gz: f343ec8238c760e0c6fbc10240ae581247b0c7dbea5552ed8383135e01e16b2d7b4ea30b5f3a4f4d46c00989d5d999b3693329b730c029845aa89460d43806d3
7
+ data.tar.gz: 58b714b0e9ef908c0b632a1f9b1b21f13573b9756eaf2b18bc840c67151aec08022283aec620abfd769b3da93ec657418c5aa7fedc063d729f11689f068a739a
@@ -0,0 +1,25 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .env
7
+ .rspec
8
+ Gemfile.lock
9
+ InstalledFiles
10
+ _yardoc
11
+ coverage
12
+ doc/
13
+ lib/bundler/man
14
+ pkg
15
+ rdoc
16
+ spec/reports
17
+ test/tmp
18
+ test/version_tmp
19
+ tmp
20
+ *.bundle
21
+ *.so
22
+ *.o
23
+ *.a
24
+ mkmf.log
25
+ **/.DS_Store
@@ -0,0 +1,11 @@
1
+ ruby:
2
+ enabled: true
3
+ # config_file: config/.rubocop.yml
4
+
5
+ coffee_script:
6
+ enabled: true
7
+ # config_file: config/.coffeelint.json
8
+
9
+ # java_script:
10
+ # enabled: true
11
+ # config_file: config/.jshint.json
@@ -0,0 +1,15 @@
1
+ language: ruby
2
+ cache: bundler
3
+
4
+ rvm:
5
+ - 2.1.2
6
+
7
+ branches:
8
+ only:
9
+ - master
10
+
11
+ script: bundle exec rake spec
12
+
13
+ before_install:
14
+ - gem update --system
15
+ - gem --version
File without changes
@@ -0,0 +1 @@
1
+ * Andrey Pronin <moonfly.msk@gmail.com> aka moonfly (https://github.com/moonfly)
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in observable_object.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2014 [CONTRIBUTORS.md](https://github.com/moonfly/observable_object/master/CONTRIBUTORS.md)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,120 @@
1
+ # ObservableObject
2
+
3
+ This gem provides a delegator class that wraps around an object and triggers events on modification of that object.
4
+ It is useful for objects that serve as accessors to external storage - when an modification to the object should trigger
5
+ the database update, for example.
6
+
7
+ One example of a gem that benefits from ObservableObject is [store_complex](https://github.com/moonfly/store_complex).
8
+
9
+ [![Build Status](https://travis-ci.org/moonfly/observable_object.svg?branch=master)](https://travis-ci.org/moonfly/observable_object)
10
+ [![Coverage Status](https://img.shields.io/coveralls/moonfly/observable_object.svg)](https://coveralls.io/r/moonfly/observable_object?branch=master)
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ gem 'observable_object'
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install observable_object
25
+
26
+ ## Usage
27
+
28
+ ### Shallow wrapper
29
+
30
+ Create an ObservableObject for your existing object using this syntax:
31
+
32
+ ```ruby
33
+ ObservableObject.wrap(your_object[, list_of_methods], &event_callback)
34
+ ```
35
+
36
+ Here:
37
+
38
+ - `your_object` is the object that you need to wrap
39
+ - `list_of_methods` (optional) is the list of your object's methods that change its internal state and should lead to triggering the event
40
+ - `event_callback` is the block to be called when the object is modified, it accepts a single parameter - the object itself
41
+
42
+ The resulting object is a thin delegator around your object, so the ObservableObject will behave almost exactly as your object. That includes results of such methods as `class`, `hash`, `<=>`. It is still a separate object, so it will have it's own `__id__`, and `eql?` will act accordingly. Here is an example:
43
+
44
+ ```ruby
45
+ s = ObservableObject.wrap("some string",[:capitalize!,:downcase!]) do |obj|
46
+ puts "The observed string is now #{obj}"
47
+ end
48
+
49
+ s.class # => String
50
+ s == 'some string' # => true
51
+ h = {}
52
+ h[s] = 100500
53
+ h['some string'] # => 100500
54
+
55
+ s.capitalize! # Will print "The observed string is now Some string"
56
+ ```
57
+
58
+ #### Special options for the list of methods
59
+
60
+ It is also possible to pass `:detect` in place of the list of methods that change the internal state. In this case `ObservableObject` will try to auto-detect such methods. That is also the default behavior. If you don't provide the second parameter, this auto-detection method will be used. Auto-detection works as follows:
61
+
62
+ - It considers the state-changing methods of widely-used classes to be state-changing for this object as well. The widely used classes include `Array`, `Hash`, `Set`, `String`. Please note that `ObservableObject` knows only about the standard methods provided by Ruby. If you have your custom methods defined for these widely-used classes they will not be recognized by the observing wrapper as something that changes the object's state.
63
+ - It considers all methods with a `!` at the end as changing the internal state.
64
+
65
+ Besides auto-detection, there is another method that attempts to automatically determine if the object has changed. It relies on making a clone of an object (using `clone` method) before calling any method and then comparing this copied object with the resulting object after the method call (using `eql?`). This method can be invoked by providing `:compare` in place of the list of methods that change the internal state. Please note that this method has obvious drawbacks and limitations:
66
+
67
+ - it relies on the assumption that cloning the object doesn't have side effects and doesn't change its internal state
68
+ - it relies on the `eql?` operator to compare the object internal states rather than checking the objects' identity
69
+ - it has additional performance drawbacks as cloning an object (especially a complex on) takes time and memory
70
+
71
+ Bottomline: know your object before you wrap it in `ObservableObject` and think several times before using it with
72
+ something complex when a mere operation of cloning an object may have side effects.
73
+
74
+ ### Deep wrapper
75
+
76
+ In addition to the shallow wrapper (`ObservableObject.wrap`) this gem also provides a deep wrapper: `ObservableObject.deep_wrap`.
77
+ A deep wrapper not only wraps the object itself, but in case of Hashes, Arrays, Sets goes ahead and wraps each of its elements in an ObservableObject. Moreover, it repeats the deap wrapping on all nested Arrays, Hashes and Sets. Regardless of which of the nested wrapped object has caused the event, the upper-level object will always be passed as the parameter to the event handler block.
78
+
79
+ For deeply-wrapped objects the following situations will also trigger events:
80
+
81
+ ```ruby
82
+ arr = ObservableObject.deep_wrap([[1,2,3],{a:10,b:10},"string"]) do |obj|
83
+ # Here obj is always arr, never a nested wrapped object
84
+ puts "Event triggered on #{obj}!"
85
+ end
86
+
87
+ arr[0][0] = 100 # Triggers an event
88
+ arr[0].push(7) # Triggers an event
89
+ arr[1].merge!({c:50}) # Triggers an event
90
+ arr[2].upcase! # Triggers an event
91
+ ```
92
+
93
+ Please note that there is a certain additional performance penalty if using deep wrapping since the object is re-wrapped after
94
+ every method that changes its internal state. This re-wrapping is done because methods like `<<` and `unshift` can add new
95
+ nested objects that must be wrapped.
96
+
97
+ ## Versioning
98
+
99
+ Semantic versioning (http://semver.org/spec/v2.0.0.html) is used.
100
+
101
+ For a version number MAJOR.MINOR.PATCH, unless MAJOR is 0:
102
+
103
+ 1. MAJOR version is incremented when incompatible API changes are made,
104
+ 2. MINOR version is incremented when functionality is added in a backwards-compatible manner,
105
+ 3. PATCH version is incremented when backwards-compatible bug fixes are made.
106
+
107
+ Major version "zero" (0.y.z) is for initial development. Anything may change at any time.
108
+ The public API should not be considered stable.
109
+
110
+ ## Dependencies
111
+
112
+ - Ruby >= 2.1
113
+
114
+ ## Contributing
115
+
116
+ 1. Fork it ( https://github.com/moonfly/observable_object/fork )
117
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
118
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
119
+ 4. Push to the branch (`git push origin my-new-feature`)
120
+ 5. Create a new Pull Request
@@ -0,0 +1,5 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ desc "Run RSpec tests (use rake spec SPEC_OPTS='...' to pass rspec options)"
5
+ RSpec::Core::RakeTask.new(:spec)
@@ -0,0 +1,131 @@
1
+ require "observable_object/version"
2
+
3
+ module ObservableObject
4
+ class Notifier
5
+ def initialize(param,&event)
6
+ @param = param
7
+ @event = event
8
+ end
9
+ def call
10
+ @event.call(@param)
11
+ end
12
+ end
13
+
14
+ module DeepWrap
15
+ def self.map_obj(obj,notifier)
16
+ case obj
17
+ when Array then obj.map { |x| wrap_elem(x,notifier) }
18
+ when Set then obj.clone.map! { |x| wrap_elem(x,notifier) }
19
+ when Hash then obj.map { |k,v| [ k, wrap_elem(v,notifier) ] }.to_h
20
+ else obj
21
+ end
22
+ end
23
+
24
+ NonWrappable = [Symbol, Numeric, TrueClass, FalseClass, NilClass]
25
+ def self.is_unwrappable(elem)
26
+ NonWrappable.any? { |t| elem.is_a?(t) }
27
+ end
28
+ def self.wrap_elem(elem,notifier)
29
+ is_unwrappable(elem) ? elem : Wrapper.new(elem,:detect,true,notifier)
30
+ end
31
+ end
32
+
33
+ module Watcher
34
+ class WatcherDetect
35
+ DefaultMethods = [ :<<, :[]=, :add, :add?, :capitalize!, :chomp!, :chop!, :clear, :collect!, :compact!, :concat,
36
+ :delete, :delete!, :delete?, :delete_at, :delete_if, :downcase!, :encode!, :gsub!, :fill,
37
+ :flatten!, :initialize_copy, :insert, :keep_if, :lstrip!, :map!, :merge!, :next!, :pop, :prepend,
38
+ :push, :rehash, :reject!, :replace, :reverse!, :rotate!, :rstrip!, :scrub!, :select!,
39
+ :shift, :shuffle!, :slice!, :sort!, :sort_by!, :squeeze!, :store, :strip!, :sub!, :subtract,
40
+ :succ!, :swapcase!, :tr!, :tr_s!, :uniq!, :unshift, :upcase!, :update ]
41
+ StringExceptionMethods = [ :delete ] # doesn't change the object in case of String
42
+
43
+ def initialize(obj)
44
+ @methods = obj.is_a?(String) ? DefaultMethods-StringExceptionMethods : DefaultMethods
45
+ end
46
+ def remember
47
+ # nothing to do
48
+ end
49
+ def is_state_changing(obj,mname)
50
+ mname.match(/.+\!\z/) || @methods.include?(mname)
51
+ end
52
+ end
53
+
54
+ class WatcherMethods
55
+ def initialize(methods)
56
+ @methods = methods
57
+ end
58
+ def remember
59
+ # nothing to do
60
+ end
61
+ def is_state_changing(obj,mname)
62
+ @methods.include?(mname)
63
+ end
64
+ end
65
+
66
+ class WatcherCompare
67
+ def remember
68
+ obj = yield
69
+ @obj_before = obj.clone rescue obj
70
+ end
71
+ def is_state_changing(obj,mname)
72
+ !obj.eql?(@obj_before)
73
+ end
74
+ end
75
+
76
+ def self.create(obj,methods)
77
+ case methods
78
+ when :detect then WatcherDetect.new(obj)
79
+ when :compare then WatcherCompare.new
80
+ else WatcherMethods.new(methods)
81
+ end
82
+ end
83
+ end
84
+
85
+ class Wrapper < BasicObject
86
+ def initialize(obj,methods,deep,notifier=nil,&event)
87
+ @deep = deep
88
+ @notifier = notifier || Notifier.new(self,&event)
89
+ @watcher = Watcher::create(obj,methods)
90
+ @obj = @deep ? DeepWrap::map_obj(obj,@notifier) : obj
91
+ end
92
+
93
+ def ==(other)
94
+ @obj == other
95
+ end
96
+ def eql?(other)
97
+ @obj.eql?(other)
98
+ end
99
+ def !=(other)
100
+ @obj != other
101
+ end
102
+ def !
103
+ !@obj
104
+ end
105
+
106
+ def respond_to?(mname)
107
+ @obj.respond_to?(mname)
108
+ end
109
+ def method_missing(mname,*args,&block)
110
+ @watcher.remember { @obj }
111
+
112
+ res = @obj.__send__(mname,*args,&block)
113
+ chain = @obj.equal?(res) # did the wrapped object return itself from this method?
114
+
115
+ if @watcher.is_state_changing(@obj,mname)
116
+ @obj = DeepWrap::map_obj(@obj,@notifier) if @deep # remap; some nested objects could have changed
117
+ @notifier.call
118
+ end
119
+
120
+ chain ? self : res # for chaining return self when the underlying object returns self
121
+ end
122
+ end
123
+
124
+ # Main API
125
+ def self.wrap(obj,methods=:detect,&event)
126
+ DeepWrap.is_unwrappable(obj) ? obj : Wrapper.new(obj,methods,false,nil,&event)
127
+ end
128
+ def self.deep_wrap(obj,methods=:detect,&event)
129
+ DeepWrap.is_unwrappable(obj) ? obj : Wrapper.new(obj,methods,true,nil,&event)
130
+ end
131
+ end
@@ -0,0 +1,3 @@
1
+ module ObservableObject
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'observable_object/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'observable_object'
8
+ spec.version = ObservableObject::VERSION
9
+ spec.authors = ['moonfly (Andrey Pronin)']
10
+ spec.email = ['moonfly.msk@gmail.com']
11
+ spec.summary = %q{Thin delegator that wraps objects and triggers events on modification (useful for storage accessor objects)}
12
+ spec.description = %q{Thin delegator that wraps objects and triggers events on modification (useful for storage accessor objects).}
13
+ spec.homepage = 'https://github.com/moonfly/observable_object'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.rdoc_options = ['--charset=UTF-8']
22
+ spec.extra_rdoc_files = %w[README.md CONTRIBUTORS.md LICENSE.md]
23
+
24
+ spec.required_ruby_version = '>= 2.1.0'
25
+
26
+ spec.add_development_dependency 'bundler', '>= 1.6'
27
+ spec.add_development_dependency 'rake'
28
+ spec.add_development_dependency 'rspec', '~> 3.1'
29
+ spec.add_development_dependency 'coveralls'
30
+ end
@@ -0,0 +1,108 @@
1
+ require 'spec_helper'
2
+
3
+ describe ObservableObject::DeepWrap do
4
+ class TestNotificationStatus
5
+ attr_reader :count
6
+ def initialize
7
+ @count = 0
8
+ end
9
+ def notify
10
+ @count += 1
11
+ end
12
+ def notified?
13
+ count > 0
14
+ end
15
+ end
16
+
17
+ let(:notification_status) { TestNotificationStatus.new }
18
+ let(:notifier) { ObservableObject::Notifier.new(notification_status) { |param| param.notify } }
19
+
20
+ it 'keeps wrapped elements equal to original elems' do
21
+ ObservableObjectTest::AllObjects.each do |original|
22
+ expect(ObservableObject::DeepWrap.map_obj(original,notifier)).to eq(original)
23
+ end
24
+ end
25
+
26
+ it 'wraps elements of Array' do
27
+ original = ["string", [1,2,3]]
28
+ wrapped = ObservableObject::DeepWrap.map_obj(original,notifier)
29
+
30
+ wrapped[0][0] = 'S' # should notify
31
+ expect(notification_status.count).to eq(1)
32
+
33
+ wrapped[1] << 5 # should notify
34
+ expect(notification_status.count).to eq(2)
35
+ end
36
+
37
+ it 'wraps elements of Hash' do
38
+ original = {a: "string", b: [1,2,3]}
39
+ wrapped = ObservableObject::DeepWrap.map_obj(original,notifier)
40
+
41
+ wrapped[:a][0] = 'S' # should notify
42
+ expect(notification_status.count).to eq(1)
43
+
44
+ wrapped[:b] << 5 # should notify
45
+ expect(notification_status.count).to eq(2)
46
+ end
47
+
48
+ it 'wraps elements of Set' do
49
+ original = Set[[1,2,3],{0=>1,1=>2}] # note: strings are frozen, don't use them in Set testing
50
+ wrapped = ObservableObject::DeepWrap.map_obj(original,notifier)
51
+
52
+ wrapped.each do |elem|
53
+ elem[0] = 'A' # should notify
54
+ end
55
+ expect(notification_status.count).to eq(original.size)
56
+ end
57
+
58
+ it 'does not wrap Strings' do
59
+ original = "string"
60
+ wrapped = ObservableObject::DeepWrap.map_obj(original,notifier)
61
+
62
+ wrapped[0] = 'S' # should not notify
63
+ expect(notification_status.count).to eq(0)
64
+ end
65
+
66
+ it 'deep wraps Arrays' do
67
+ original = []
68
+ original[0] = []
69
+ original[0][0] = []
70
+ original[0][0][0] = []
71
+ original[0][0][0][0] = []
72
+ original[0][0][0][0][0] = "string"
73
+ wrapped = ObservableObject::DeepWrap.map_obj(original,notifier)
74
+
75
+ wrapped[0][0][0][0][0][0] = 'S'
76
+ expect(notification_status.count).to eq(1)
77
+ end
78
+
79
+ it 'deep wraps Hashes' do
80
+ original = {}
81
+ original[:a] = {}
82
+ original[:a][:a] = {}
83
+ original[:a][:a][:a] = {}
84
+ original[:a][:a][:a][:a] = {}
85
+ original[:a][:a][:a][:a][:a] = "string"
86
+ wrapped = ObservableObject::DeepWrap.map_obj(original,notifier)
87
+
88
+ wrapped[:a][:a][:a][:a][:a][0] = 'S'
89
+ expect(notification_status.count).to eq(1)
90
+ end
91
+
92
+ it 'deep wraps Sets' do
93
+ original = Set[Set[Set[{a:100}]]]
94
+ wrapped = ObservableObject::DeepWrap.map_obj(original,notifier)
95
+
96
+ wrapped.each do |x| # x = Set[Set[{a:100}]]
97
+ x.each do |y| # y = Set[{a:100}]
98
+ y.each do |z| # z = {a:100}
99
+ z[:a] = 200
100
+ end
101
+ end
102
+ end
103
+
104
+ expect(notification_status.count).to eq(1)
105
+ end
106
+
107
+
108
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ describe ObservableObject::Notifier do
4
+ it 'calls the specified event with the specified param' do
5
+ param = "some parameter"
6
+ called_with = nil
7
+ notifier = ObservableObject::Notifier.new(param) do |p|
8
+ called_with = p
9
+ end
10
+ expect(called_with).to be(nil)
11
+ expect{notifier.call}.not_to raise_error
12
+ expect(called_with).to be(param)
13
+ end
14
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe ObservableObject::Watcher::WatcherCompare do
4
+ let(:obj) { [10,30,20] }
5
+ let(:watcher) { ObservableObject::Watcher::WatcherCompare.new }
6
+ it 'returns true when object changes' do
7
+ watcher.remember { obj }
8
+ obj[0] = 100
9
+ expect(watcher.is_state_changing(obj,:any_method)).to be_truthy
10
+ end
11
+ it 'works with un-cloneable objects' do
12
+ [nil,true,false,123,3.1415,:symbol].each do |x|
13
+ watcher.remember { x }
14
+ expect(watcher.is_state_changing(x,:any_method)).to be_falsy
15
+ end
16
+ end
17
+ it 'returns false for a different but equal object' do
18
+ watcher.remember { obj }
19
+ expect(watcher.is_state_changing(obj.clone,:any_method)).to be_falsy
20
+ end
21
+ it 'returns false for the same object' do
22
+ watcher.remember { obj }
23
+ expect(watcher.is_state_changing(obj,:sort!)).to be_falsy
24
+ end
25
+ end
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+
3
+ describe ObservableObject::Watcher::WatcherDetect do
4
+ let(:obj_str) { "String" }
5
+ let(:obj_arr) { [1,2,3] }
6
+ let(:watcher_str) { ObservableObject::Watcher::WatcherDetect.new(obj_str) }
7
+ let(:watcher_arr) { ObservableObject::Watcher::WatcherDetect.new(obj_arr) }
8
+
9
+ it 'ignores the block passed to remember' do
10
+ called = false
11
+ watcher_str.remember { called = true }
12
+ expect(called).to be false
13
+ end
14
+ it 'returns true when delete_at is called for an object' do
15
+ expect(watcher_str.is_state_changing(obj_str,:delete_at)).to be_truthy
16
+ expect(watcher_arr.is_state_changing(obj_arr,:delete_at)).to be_truthy
17
+ end
18
+ it 'returns true when delete is called for non-String objects' do
19
+ expect(watcher_arr.is_state_changing(obj_arr,:delete)).to be_truthy
20
+ end
21
+ it 'returns false when delete is called for String objects' do
22
+ expect(watcher_str.is_state_changing(obj_str,:delete)).to be_falsy
23
+ end
24
+ it 'returns true when a method with a bang is called for an object' do
25
+ expect(watcher_str.is_state_changing(obj_str,:scary!)).to be_truthy
26
+ expect(watcher_arr.is_state_changing(obj_arr,:scary!)).to be_truthy
27
+ end
28
+ it 'returns true when some other method is called for an object' do
29
+ expect(watcher_str.is_state_changing(obj_str,:something)).to be_falsy
30
+ expect(watcher_arr.is_state_changing(obj_arr,:something)).to be_falsy
31
+ end
32
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe ObservableObject::Watcher::WatcherMethods do
4
+ class TestWatcherMethodsObject
5
+ # safe methods
6
+ def safe; false; end
7
+ def safe!; false; end
8
+ def delete_at; false; end
9
+ def sort!; false; end
10
+
11
+ # changing methods
12
+ def changing1; true; end
13
+ def changing2; true; end
14
+ def delete; true; end
15
+ def collect!; true; end
16
+ end
17
+
18
+ test_methods = (TestWatcherMethodsObject.instance_methods - Object.instance_methods).map do |name|
19
+ [name, TestWatcherMethodsObject.new.send(name)]
20
+ end.to_h
21
+ safe_methods = test_methods.select { |k,v| v == false }.map(&:first)
22
+ changing_methods = test_methods.select { |k,v| v == true }.map(&:first)
23
+
24
+ let(:obj) { TestWatcherMethodsObject.new }
25
+ let(:watcher) { ObservableObject::Watcher::WatcherMethods.new(changing_methods) }
26
+
27
+ it 'ignores the block passed to remember' do
28
+ called = false
29
+ watcher.remember { called = true }
30
+ expect(called).to be false
31
+ end
32
+ it 'returns true when called with methods from the list' do
33
+ changing_methods.each do |mname|
34
+ expect(watcher.is_state_changing(obj,mname)).to be_truthy
35
+ end
36
+ end
37
+ it 'returns false when called with methods not from the list' do
38
+ safe_methods.each do |mname|
39
+ expect(watcher.is_state_changing(obj,mname)).to be_falsy
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,10 @@
1
+ require 'spec_helper'
2
+
3
+ describe ObservableObject::Watcher do
4
+ it 'creates the right subclass' do
5
+ expect(ObservableObject::Watcher.create(Hash.new,:detect)).to be_instance_of(ObservableObject::Watcher::WatcherDetect)
6
+ expect(ObservableObject::Watcher.create(Hash.new,:compare)).to be_instance_of(ObservableObject::Watcher::WatcherCompare)
7
+ expect(ObservableObject::Watcher.create(Hash.new,[])).to be_instance_of(ObservableObject::Watcher::WatcherMethods)
8
+ end
9
+ end
10
+
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ describe ObservableObject::Wrapper do
4
+ let(:event_obj_list) { Array.new }
5
+ let(:event) { Proc.new { |obj| event_obj_list << obj } }
6
+ let(:notifier) { ObservableObject::Notifier.new("notifier",&event) }
7
+
8
+ it 'is "equal to" the original object' do
9
+ ObservableObjectTest::AllObjects.each do |obj|
10
+ wrapped = ObservableObject::Wrapper.new(obj,:compare,true,nil) { |p| ; }
11
+ expect(wrapped).to eq(obj)
12
+ end
13
+ end
14
+ it 'can be used as a key in Hashes instead of the original object' do
15
+ hash = Hash.new
16
+ ObservableObjectTest::HashKeyObjects.each do |obj|
17
+ # puts "-> #{obj}"
18
+ wrapped = ObservableObject::Wrapper.new(obj,:compare,true,nil) { |p| ; }
19
+ expect(wrapped.hash).to eq(obj.hash)
20
+ expect(wrapped.eql?(obj)).to be true
21
+
22
+ hash[obj] = obj.__id__
23
+ expect(hash[wrapped]).to eq(obj.__id__)
24
+ end
25
+ end
26
+
27
+ it 'does not call events when the object is not modified' do
28
+ obj = [[1,2,3],[4,5,6]]
29
+ wrapped = ObservableObject::Wrapper.new(obj,:detect,false,nil,&event)
30
+ wrapped.flatten
31
+ expect(event_obj_list.count).to eq(0)
32
+ end
33
+ it 'calls event when the object is modified' do
34
+ obj = [[1,2,3],[4,5,6]]
35
+ wrapped = ObservableObject::Wrapper.new(obj,:detect,false,nil,&event)
36
+ wrapped.flatten!
37
+ expect(event_obj_list.count).to eq(1)
38
+ expect(event_obj_list.all? { |x| x == wrapped }).to be true
39
+ end
40
+ it 'calls notifier when the object is modified if notifier is provided' do
41
+ obj = [[1,2,3],[4,5,6]]
42
+ wrapped = ObservableObject::Wrapper.new(obj,:detect,false,notifier)
43
+ wrapped.flatten!
44
+ expect(event_obj_list.count).to eq(1)
45
+ expect(event_obj_list.all? { |x| x == "notifier" }).to be true
46
+ end
47
+ it 'calls doesn not event when sub-objects are modified (deep=false)' do
48
+ obj = [[1,2,3],[4,5,6]]
49
+ wrapped = ObservableObject::Wrapper.new(obj,:detect,false,nil,&event)
50
+ wrapped[0].sort!
51
+ expect(event_obj_list.count).to eq(0)
52
+ end
53
+ it 'calls event when sub-objects are modified (deep=true)' do
54
+ obj = [[1,2,3],[4,5,6]]
55
+ wrapped = ObservableObject::Wrapper.new(obj,:detect,true,nil,&event)
56
+ wrapped[0].sort!
57
+ expect(event_obj_list.count).to eq(1)
58
+ expect(event_obj_list.all? { |x| x == wrapped }).to be true
59
+ end
60
+
61
+ end
@@ -0,0 +1,121 @@
1
+ require 'spec_helper'
2
+
3
+ describe ObservableObject do
4
+ let(:event_obj_list) { Array.new }
5
+ let(:event) { Proc.new { |obj| event_obj_list << obj } }
6
+
7
+ it 'has version (smoke test)' do
8
+ expect(ObservableObject::VERSION).to be_a(String)
9
+ end
10
+ it 'returns the object itself for unwrappable objects' do
11
+ [:symbol, 1, 1.0, true, false, nil].each do |obj|
12
+ expect(ObservableObject.wrap(obj).__id__).to eq(obj.__id__)
13
+ expect(ObservableObject.deep_wrap(obj).__id__).to eq(obj.__id__)
14
+ end
15
+ end
16
+
17
+ it 'is "equal to" the original object (wrap)' do
18
+ ObservableObjectTest::NonBasicObjects.each do |obj|
19
+ wrapped = ObservableObject.wrap(obj) { |p| ; }
20
+ expect(wrapped).to eq(obj)
21
+ end
22
+ end
23
+ it 'is "equal to" the original object (deep_wrap)' do
24
+ ObservableObjectTest::NonBasicObjects.each do |obj|
25
+ wrapped = ObservableObject.deep_wrap(obj) { |p| ; }
26
+ expect(wrapped).to eq(obj)
27
+ end
28
+ end
29
+
30
+ it 'provides correct "!" operator' do
31
+ ObservableObjectTest::NonBasicObjects.each do |obj|
32
+ wrapped = ObservableObject.wrap(obj) { |p| ; }
33
+ expect(!wrapped).to eq(!obj)
34
+ end
35
+ end
36
+ it 'provides correct "!=" operator' do
37
+ ObservableObjectTest::NonBasicObjects.each do |obj|
38
+ wrapped = ObservableObject.wrap(obj) { |p| ; }
39
+ expect(wrapped != obj).to be false
40
+ end
41
+ end
42
+ it 'provides correct methods' do
43
+ ObservableObjectTest::NonBasicObjects.each do |obj|
44
+ # puts "-> #{obj}"
45
+ wrapped = ObservableObject.wrap(obj) { |p| ; }
46
+
47
+ expect(obj.class.instance_methods.all? do |mname|
48
+ # puts "----> #{mname}"
49
+ wrapped.respond_to?(mname) == obj.respond_to?(mname)
50
+ end).to be true
51
+ end
52
+ end
53
+
54
+ it 'can be used as a key in Hashes instead of the original object (wrap)' do
55
+ hash = Hash.new
56
+ ObservableObjectTest::NonBasicObjects.each do |obj|
57
+ # puts "-> #{obj}"
58
+ wrapped = ObservableObject.wrap(obj) { |p| ; }
59
+ expect(wrapped.hash).to eq(obj.hash)
60
+ expect(wrapped.eql?(obj)).to be true
61
+
62
+ hash[obj] = obj.__id__
63
+ expect(hash[wrapped]).to eq(obj.__id__)
64
+ end
65
+ end
66
+ it 'can be used as a key in Hashes instead of the original object (deep_wrap)' do
67
+ hash = Hash.new
68
+ ObservableObjectTest::NonBasicObjects.each do |obj|
69
+ # puts "-> #{obj}"
70
+ wrapped = ObservableObject.deep_wrap(obj) { |p| ; }
71
+ expect(wrapped.hash).to eq(obj.hash)
72
+ expect(wrapped.eql?(obj)).to be true
73
+
74
+ hash[obj] = obj.__id__
75
+ expect(hash[wrapped]).to eq(obj.__id__)
76
+ end
77
+ end
78
+
79
+ it 'calls event handler when the object is modified' do
80
+ obj = [[1],[2]]
81
+ wrapped = ObservableObject.deep_wrap(obj,&event)
82
+ wrapped[0] << 100
83
+ wrapped[0] << 50
84
+ wrapped[0].sort!
85
+ expect(event_obj_list.count).to eq(3)
86
+ expect(event_obj_list.all? { |x| x == wrapped }).to be true
87
+ end
88
+
89
+ it 'calls event handler after a sub-object is replaced and then modified' do
90
+ obj = [[1],[2]]
91
+ wrapped = ObservableObject.deep_wrap(obj,&event)
92
+ wrapped[0] = [3]
93
+ wrapped[0] << 50
94
+ wrapped[0].sort!
95
+ wrapped[1] = [['a','b'],'c']
96
+ wrapped[1][0][1] = 'd'
97
+ expect(event_obj_list.count).to eq(5)
98
+ expect(event_obj_list.all? { |x| x == wrapped }).to be true
99
+ end
100
+
101
+ it 'calls event handler after a sub-object is added and then modified' do
102
+ obj = [[1],[2]]
103
+ wrapped = ObservableObject.deep_wrap(obj,&event)
104
+ wrapped[1] << ['a','b']
105
+ wrapped[1].last.push('c')
106
+ expect(event_obj_list.count).to eq(2)
107
+ expect(event_obj_list.all? { |x| x == wrapped }).to be true
108
+ end
109
+
110
+ # TODO: fix the case below, if there is an efficient solution. Not a bug, annoyance.
111
+ # it 'does not call event handler after an already deleted sub-object is modified' do
112
+ # obj = [[1,2],[3,4]]
113
+ # wrapped = ObservableObject.deep_wrap(obj,&event)
114
+ # a = wrapped[0]
115
+ # a.compact!
116
+ # wrapped.delete_at(0)
117
+ # a[0] = 100
118
+ # expect(event_obj_list.count).to eq(2)
119
+ # expect(event_obj_list.all? { |x| x == wrapped }).to be true
120
+ # end
121
+ end
@@ -0,0 +1,49 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
3
+
4
+ require 'observable_object'
5
+
6
+ module ObservableObjectTest
7
+ BasicObjects = [ BasicObject.new ]
8
+ DelegatorObjects = [ SimpleDelegator.new("string"), SimpleDelegator.new(1.0) ]
9
+
10
+ # NOTE: using wrapped objects as keys doesn't work for Floats and Symbols, althouth works for Fixnums, Rationals, Strings, etc!
11
+ # NOTE: one way hash key equivalency only, we don't patch eql? in other classes
12
+ NonHashKeyObjects = [ 0.0, 3.1415926, :symbol ]
13
+
14
+ HashKeyObjects = [
15
+ [1,2,"3"],
16
+ {a:1,"a"=>1,1=>2,"1"=>"2"},
17
+ Set['x','y','z'],
18
+ "Some string",
19
+ %w(Array of strings),
20
+ [:one, :two, :three],
21
+ true,
22
+ false,
23
+ nil,
24
+ "",
25
+ 1.0.to_c,
26
+ 1.0.to_r,
27
+ [nil],
28
+ [],
29
+ Hash.new,
30
+ Set.new,
31
+ 0,
32
+ 123,
33
+ 1234567890987654321,
34
+ StringIO.new("String IO"),
35
+ Exception.new("Some exception"),
36
+ Object.new,
37
+ Class.new,
38
+ Module.new,
39
+ lambda { 10 },
40
+ Proc.new { |x| x },
41
+ Mutex.new,
42
+ [ [1,2,3], "bebe", {"key"=>"value", key: :value}, Time.now, -> { puts "Line" }, Set['a','c'] ],
43
+ Time.now,
44
+ ENV,
45
+ ARGV
46
+ ]
47
+ NonBasicObjects = HashKeyObjects + NonHashKeyObjects
48
+ AllObjects = NonBasicObjects + BasicObjects + DelegatorObjects
49
+ end
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: observable_object
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - moonfly (Andrey Pronin)
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-10-05 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.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.1'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: coveralls
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Thin delegator that wraps objects and triggers events on modification
70
+ (useful for storage accessor objects).
71
+ email:
72
+ - moonfly.msk@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files:
76
+ - README.md
77
+ - CONTRIBUTORS.md
78
+ - LICENSE.md
79
+ files:
80
+ - ".gitignore"
81
+ - ".hound.yml"
82
+ - ".travis.yml"
83
+ - CHANGELOG.md
84
+ - CONTRIBUTORS.md
85
+ - Gemfile
86
+ - LICENSE.md
87
+ - README.md
88
+ - Rakefile
89
+ - lib/observable_object.rb
90
+ - lib/observable_object/version.rb
91
+ - observable_object.gemspec
92
+ - spec/observable_object/deep_wrap_spec.rb
93
+ - spec/observable_object/notifier_spec.rb
94
+ - spec/observable_object/watcher/watcher_compare_spec.rb
95
+ - spec/observable_object/watcher/watcher_detect_spec.rb
96
+ - spec/observable_object/watcher/watcher_methods_spec.rb
97
+ - spec/observable_object/watcher_spec.rb
98
+ - spec/observable_object/wrapper_spec.rb
99
+ - spec/observable_object_spec.rb
100
+ - spec/spec_helper.rb
101
+ homepage: https://github.com/moonfly/observable_object
102
+ licenses:
103
+ - MIT
104
+ metadata: {}
105
+ post_install_message:
106
+ rdoc_options:
107
+ - "--charset=UTF-8"
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: 2.1.0
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubyforge_project:
122
+ rubygems_version: 2.2.2
123
+ signing_key:
124
+ specification_version: 4
125
+ summary: Thin delegator that wraps objects and triggers events on modification (useful
126
+ for storage accessor objects)
127
+ test_files:
128
+ - spec/observable_object/deep_wrap_spec.rb
129
+ - spec/observable_object/notifier_spec.rb
130
+ - spec/observable_object/watcher/watcher_compare_spec.rb
131
+ - spec/observable_object/watcher/watcher_detect_spec.rb
132
+ - spec/observable_object/watcher/watcher_methods_spec.rb
133
+ - spec/observable_object/watcher_spec.rb
134
+ - spec/observable_object/wrapper_spec.rb
135
+ - spec/observable_object_spec.rb
136
+ - spec/spec_helper.rb