observable_object 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +25 -0
- data/.hound.yml +11 -0
- data/.travis.yml +15 -0
- data/CHANGELOG.md +0 -0
- data/CONTRIBUTORS.md +1 -0
- data/Gemfile +4 -0
- data/LICENSE.md +22 -0
- data/README.md +120 -0
- data/Rakefile +5 -0
- data/lib/observable_object.rb +131 -0
- data/lib/observable_object/version.rb +3 -0
- data/observable_object.gemspec +30 -0
- data/spec/observable_object/deep_wrap_spec.rb +108 -0
- data/spec/observable_object/notifier_spec.rb +14 -0
- data/spec/observable_object/watcher/watcher_compare_spec.rb +25 -0
- data/spec/observable_object/watcher/watcher_detect_spec.rb +32 -0
- data/spec/observable_object/watcher/watcher_methods_spec.rb +42 -0
- data/spec/observable_object/watcher_spec.rb +10 -0
- data/spec/observable_object/wrapper_spec.rb +61 -0
- data/spec/observable_object_spec.rb +121 -0
- data/spec/spec_helper.rb +49 -0
- metadata +136 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
@@ -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
|
data/.hound.yml
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
File without changes
|
data/CONTRIBUTORS.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
* Andrey Pronin <moonfly.msk@gmail.com> aka moonfly (https://github.com/moonfly)
|
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|