Dependency 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/dependency.rb +141 -0
- data/rakefile.rb +52 -0
- data/readme.txt +39 -0
- data/spec/dependency_spec.rb +48 -0
- data/spec/preamble.rb +53 -0
- metadata +51 -0
data/lib/dependency.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
# Just a namespace.
|
2
|
+
module Dependency
|
3
|
+
# This is the module you need to include in your classes (or Object).
|
4
|
+
module Methods
|
5
|
+
# Dependents can be any type of object, but must
|
6
|
+
# have the <tt>update</tt> method defined as follows:
|
7
|
+
#
|
8
|
+
# def update(aspect)
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# or:
|
12
|
+
#
|
13
|
+
# def update(aspect, message)
|
14
|
+
# # message[:from] always defined
|
15
|
+
# # message[:with] usually defined
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# Adding a dependent:
|
19
|
+
#
|
20
|
+
# object.dependents.add dependent
|
21
|
+
# object.dependents << dependent
|
22
|
+
#
|
23
|
+
# Removing a dependent:
|
24
|
+
#
|
25
|
+
# object.dependents.remove dependent
|
26
|
+
#
|
27
|
+
def dependents
|
28
|
+
@dependents ||= Proxy.new
|
29
|
+
end
|
30
|
+
# Call this method to notify of a change, e.g.
|
31
|
+
#
|
32
|
+
# def name=(new_name)
|
33
|
+
# old_name = self.name
|
34
|
+
# self.name = new_name
|
35
|
+
# changed :name, :with => old_name
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
def changed(aspect = nil, message = nil)
|
39
|
+
args = [:update, aspect]
|
40
|
+
unless message.nil?
|
41
|
+
message[:from] = self
|
42
|
+
args << message
|
43
|
+
end
|
44
|
+
flexi = FlexiCaller.new(*args)
|
45
|
+
dependents.each { |dependent| flexi.call_on dependent }
|
46
|
+
end
|
47
|
+
# Register a dependent that's only interested in a particular aspect.
|
48
|
+
#
|
49
|
+
# object.express_interest_in :name, :for => dependent
|
50
|
+
#
|
51
|
+
# Use the :send_back option to specify what message (method call) should
|
52
|
+
# be sent back (defaults to :update).
|
53
|
+
#
|
54
|
+
# object.express_interest_in :name, :for => dependent, :send_back => :name_changed
|
55
|
+
#
|
56
|
+
# Note: the method called on the dependents has a different (simpler!)
|
57
|
+
# signature to that when just using `dependents.add`:
|
58
|
+
#
|
59
|
+
# def update(old_name)
|
60
|
+
# def update(old_name, object)
|
61
|
+
#
|
62
|
+
def express_interest_in(aspect, options)
|
63
|
+
dependents.add Transformer.new(self, aspect, options[:for], options[:send_back] || :update)
|
64
|
+
end
|
65
|
+
# Remove a dependency:
|
66
|
+
#
|
67
|
+
# object.retract_interest_in :name, :for => dependent
|
68
|
+
#
|
69
|
+
def retract_interest_in(aspect, options)
|
70
|
+
dependents.delete_if do |dependent|
|
71
|
+
dependent.respond_to?(:for?) && dependent.for?(aspect, options[:for])
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Include this module in your classes (or Object),
|
77
|
+
# if you want to use the dependency_attr class method.
|
78
|
+
module ClassMethod
|
79
|
+
# Shorthand for the following pattern:
|
80
|
+
#
|
81
|
+
# def name=(new_name)
|
82
|
+
# old_name = self.name
|
83
|
+
# self.name = new_name
|
84
|
+
# changed :name, :with => old_name
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
# Use it as you would `attr`, e.g.
|
88
|
+
#
|
89
|
+
# dependency_attr :name, :age
|
90
|
+
#
|
91
|
+
def dependency_attr(*names)
|
92
|
+
for attribute in names
|
93
|
+
attr_reader attribute
|
94
|
+
class_eval %{
|
95
|
+
def #{attribute}=(new_value)
|
96
|
+
old_value = self.#{attribute}
|
97
|
+
instance_variable_set '@#{attribute}', new_value
|
98
|
+
changed :#{attribute}, :with => old_value
|
99
|
+
end
|
100
|
+
}
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
private # keep oot!
|
106
|
+
|
107
|
+
class Proxy < Array # :nodoc:
|
108
|
+
def remove(dependent)
|
109
|
+
delete_if { |d| d === dependent }
|
110
|
+
end
|
111
|
+
alias :add :<<
|
112
|
+
end
|
113
|
+
|
114
|
+
class FlexiCaller # :nodoc:
|
115
|
+
def initialize(method_name, *args)
|
116
|
+
@method_name, @args = method_name, args
|
117
|
+
end
|
118
|
+
def call_on(dependent)
|
119
|
+
case arity = dependent.method(@method_name).arity
|
120
|
+
when 0
|
121
|
+
dependent.send @method_name
|
122
|
+
when -1
|
123
|
+
dependent.send @method_name, *@args
|
124
|
+
else
|
125
|
+
dependent.send @method_name, *@args[0, arity]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class Transformer # :nodoc:
|
131
|
+
def initialize(parent, aspect, dependent, method_name)
|
132
|
+
@parent, @aspect, @dependent, @method_name = parent, aspect, dependent, method_name
|
133
|
+
end
|
134
|
+
def update(aspect = nil, message = {})
|
135
|
+
FlexiCaller.new(@method_name, message[:with], @parent).call_on(@dependent) if @aspect == aspect
|
136
|
+
end
|
137
|
+
def for?(aspect, dependent)
|
138
|
+
@aspect == aspect && @dependent.equal?(dependent)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
data/rakefile.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/packagetask'
|
5
|
+
require 'rake/gempackagetask'
|
6
|
+
require 'rcov/rcovtask'
|
7
|
+
require 'rake/rdoctask'
|
8
|
+
|
9
|
+
module Dependency
|
10
|
+
VERSION = '1.0.0'
|
11
|
+
PACK = ['readme.txt', 'rakefile.rb', '{lib,spec}/*.rb']
|
12
|
+
SPEC = ['spec/*_spec.rb']
|
13
|
+
RDOC = ['readme.txt', 'lib/*.rb']
|
14
|
+
end
|
15
|
+
|
16
|
+
gemspec = Gem::Specification.new do |s|
|
17
|
+
s.name = 'Dependency'
|
18
|
+
s.summary ='Smalltalk-inspired dependency mechanism.'
|
19
|
+
s.version = Dependency::VERSION
|
20
|
+
s.files = FileList[*Dependency::PACK]
|
21
|
+
s.require_path = 'lib'
|
22
|
+
s.autorequire = 'dependency'
|
23
|
+
s.has_rdoc = true
|
24
|
+
s.extra_rdoc_files << Dependency::RDOC.first
|
25
|
+
s.rdoc_options << '--main' << Dependency::RDOC.first
|
26
|
+
s.rubyforge_project = s.name.downcase
|
27
|
+
s.homepage = "http://#{s.rubyforge_project}.rubyforge.org/"
|
28
|
+
s.author = 'Tim Fletcher'
|
29
|
+
s.email = 'twoggle@gmail.com'
|
30
|
+
end
|
31
|
+
|
32
|
+
Rake::GemPackageTask.new(gemspec) { |t| t.package_dir = 'pkg' }
|
33
|
+
|
34
|
+
Rake::PackageTask.new(gemspec.name, Dependency::VERSION) do |p|
|
35
|
+
p.need_tar_gz = true
|
36
|
+
p.package_files.include *Dependency::PACK
|
37
|
+
end
|
38
|
+
|
39
|
+
Rake::TestTask.new :spec do |t|
|
40
|
+
t.test_files = Dependency::SPEC
|
41
|
+
end
|
42
|
+
|
43
|
+
Rcov::RcovTask.new do |t|
|
44
|
+
t.test_files = Dependency::SPEC
|
45
|
+
t.output_dir = 'coverage'
|
46
|
+
t.rcov_opts = []
|
47
|
+
end
|
48
|
+
|
49
|
+
Rake::RDocTask.new do |t|
|
50
|
+
t.main = Dependency::RDOC.first
|
51
|
+
t.rdoc_files.include *Dependency::RDOC
|
52
|
+
end
|
data/readme.txt
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
= Dependency
|
2
|
+
|
3
|
+
Smalltalk-inspired dependency mechanism.
|
4
|
+
|
5
|
+
|
6
|
+
== Basic HOWTO
|
7
|
+
|
8
|
+
In your classes (or Object):
|
9
|
+
|
10
|
+
include Dependency::Methods
|
11
|
+
|
12
|
+
Register dependencies:
|
13
|
+
|
14
|
+
object.dependents.add dependent
|
15
|
+
|
16
|
+
Notify of changes like so:
|
17
|
+
|
18
|
+
changed :attr_name, :with => old_value
|
19
|
+
|
20
|
+
The update method will then be called on all the object's dependents.
|
21
|
+
|
22
|
+
Remove dependencies:
|
23
|
+
|
24
|
+
object.dependents.remove dependent
|
25
|
+
|
26
|
+
For more insight, take a look at the source, the spec, or Chapter 19 of
|
27
|
+
Smalltalk by Example, upon which this code was based.
|
28
|
+
|
29
|
+
Smalltalk by Example: http://www.iam.unibe.ch/~ducasse/FreeBooks/ByExample
|
30
|
+
|
31
|
+
|
32
|
+
== License
|
33
|
+
|
34
|
+
Same as Ruby.
|
35
|
+
|
36
|
+
|
37
|
+
== Contact
|
38
|
+
|
39
|
+
Tim Fletcher <mailto:twoggle@gmail.com>
|
@@ -0,0 +1,48 @@
|
|
1
|
+
#
|
2
|
+
# Needs [test/spec](http://chneukirchen.org/repos/testspec)
|
3
|
+
|
4
|
+
$:.unshift File.dirname(__FILE__)
|
5
|
+
|
6
|
+
require 'test/spec'
|
7
|
+
require 'preamble'
|
8
|
+
|
9
|
+
$stock = Stock.new('DEPCO')
|
10
|
+
$simple_watcher = SimpleStockWatcher.new($stock)
|
11
|
+
$selective_watcher = SelectiveStockWatcher.new($stock)
|
12
|
+
$selective_watcher_with_send_back = SelectiveStockWatcherWithSendBack.new($stock)
|
13
|
+
|
14
|
+
$stock.traded 200, :price => 20
|
15
|
+
$stock.traded 100, :price => 30
|
16
|
+
$stock.name = 'NEWCO'
|
17
|
+
$stock.dependents.remove $simple_watcher
|
18
|
+
$stock.retract_interest_in :price, :for => $selective_watcher
|
19
|
+
$stock.retract_interest_in :price, :for => $selective_watcher_with_send_back
|
20
|
+
$stock.traded 150, :price => 25
|
21
|
+
|
22
|
+
|
23
|
+
context 'Simple Stock Watcher' do
|
24
|
+
specify 'should receive 3 calls to #update' do
|
25
|
+
$simple_watcher.method_calls.should.equal :update => 3
|
26
|
+
end
|
27
|
+
specify 'should receive correct price changes' do
|
28
|
+
$simple_watcher.price_changes.should.equal [[0, 20], [20, 30]]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'Selective Stock Watcher' do
|
33
|
+
specify 'should receive 2 calls to #update' do
|
34
|
+
$selective_watcher.method_calls.should.equal :update => 2
|
35
|
+
end
|
36
|
+
specify 'should receive correct price changes' do
|
37
|
+
$selective_watcher.price_changes.should.equal [[0, 20], [20, 30]]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'Selective Stock Watcher w/ Send Back' do
|
42
|
+
specify 'should receive 2 calls to #price_changed' do
|
43
|
+
$selective_watcher_with_send_back.method_calls.should.equal :price_changed => 2
|
44
|
+
end
|
45
|
+
specify 'should receive correct price changes' do
|
46
|
+
$selective_watcher.price_changes.should.equal [[0, 20], [20, 30]]
|
47
|
+
end
|
48
|
+
end
|
data/spec/preamble.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'dependency')
|
2
|
+
|
3
|
+
Object.send :include, Dependency::Methods
|
4
|
+
Class.send :include, Dependency::ClassMethod
|
5
|
+
|
6
|
+
class Stock
|
7
|
+
dependency_attr :price, :name
|
8
|
+
def initialize(name, price = 0)
|
9
|
+
@name, @price = name, price
|
10
|
+
end
|
11
|
+
def traded(count, args)
|
12
|
+
self.price = args[:price]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class StockWatcher
|
17
|
+
def method_calls
|
18
|
+
@method_calls ||= Hash.new { |calls, meth| calls[meth] = 0 }
|
19
|
+
end
|
20
|
+
def price_changes
|
21
|
+
@price_changes ||= []
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class SimpleStockWatcher < StockWatcher
|
26
|
+
def initialize(stock)
|
27
|
+
stock.dependents.add self
|
28
|
+
end
|
29
|
+
def update(aspect, message)
|
30
|
+
method_calls[:update] += 1
|
31
|
+
price_changes << [message[:with], message[:from].price] if :price == aspect
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class SelectiveStockWatcher < StockWatcher
|
36
|
+
def initialize(stock)
|
37
|
+
stock.express_interest_in :price, :for => self
|
38
|
+
end
|
39
|
+
def update(old_price, stock)
|
40
|
+
method_calls[:update] += 1
|
41
|
+
price_changes << [old_price, stock.price]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class SelectiveStockWatcherWithSendBack < StockWatcher
|
46
|
+
def initialize(stock)
|
47
|
+
stock.express_interest_in :price, :for => self, :send_back => :price_changed
|
48
|
+
end
|
49
|
+
def price_changed(old_price, stock)
|
50
|
+
method_calls[:price_changed] += 1
|
51
|
+
price_changes << [old_price, stock.price]
|
52
|
+
end
|
53
|
+
end
|
metadata
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.0
|
3
|
+
specification_version: 1
|
4
|
+
name: Dependency
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 1.0.0
|
7
|
+
date: 2006-10-20 00:00:00 +01:00
|
8
|
+
summary: Smalltalk-inspired dependency mechanism.
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: twoggle@gmail.com
|
12
|
+
homepage: http://dependency.rubyforge.org/
|
13
|
+
rubyforge_project: dependency
|
14
|
+
description:
|
15
|
+
autorequire: dependency
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Tim Fletcher
|
31
|
+
files:
|
32
|
+
- readme.txt
|
33
|
+
- rakefile.rb
|
34
|
+
- lib/dependency.rb
|
35
|
+
- spec/dependency_spec.rb
|
36
|
+
- spec/preamble.rb
|
37
|
+
test_files: []
|
38
|
+
|
39
|
+
rdoc_options:
|
40
|
+
- --main
|
41
|
+
- readme.txt
|
42
|
+
extra_rdoc_files:
|
43
|
+
- readme.txt
|
44
|
+
executables: []
|
45
|
+
|
46
|
+
extensions: []
|
47
|
+
|
48
|
+
requirements: []
|
49
|
+
|
50
|
+
dependencies: []
|
51
|
+
|