psychic 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +48 -0
- data/lib/psychic.rb +81 -0
- data/test/test_psychic.rb +168 -0
- metadata +57 -0
data/README
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
|
2
|
+
psychic is a metaprogramming module which inject a listener interface into any object.
|
3
|
+
it was first intented to be used to make ordinary arrays listenable, but of course may
|
4
|
+
be used for other purposes.
|
5
|
+
|
6
|
+
for example:
|
7
|
+
a = [:a,:b,:c]
|
8
|
+
Psychic.connect(a,:delete, {|obj,method,value,*args,&proc|
|
9
|
+
puts "#{method}->#{obj.inspect}"}
|
10
|
+
a.delete :a
|
11
|
+
#will call proc when delete is called.
|
12
|
+
=> delete->:a
|
13
|
+
|
14
|
+
#(the proc is called following the method)
|
15
|
+
#parameters to the proc are as follows:
|
16
|
+
#object -> the object which is being listened to.
|
17
|
+
#method -> name of the method which was called
|
18
|
+
#value -> value returned by method
|
19
|
+
#args -> arguements to method
|
20
|
+
#proc -> the proc passed to the method (if applicable)
|
21
|
+
|
22
|
+
Psychic can remove procs as well, but you need to retain the reference to the proc so you must call it in a different style:
|
23
|
+
|
24
|
+
p = proc {|obj,method,*args,&proc|}
|
25
|
+
Psychic.connect(object,method_names,.., &p)
|
26
|
+
#note that the proc can be injected into many methods at once.
|
27
|
+
|
28
|
+
#now, disconnect Psychic like this:
|
29
|
+
Psychic.disconnect(object,method_names,.., &p)
|
30
|
+
|
31
|
+
#now the proc p will no longer be called
|
32
|
+
|
33
|
+
multiple blocks can be injected into the same methods by repeated calls to Psychic.connect()
|
34
|
+
|
35
|
+
also, I have included a convience method which wraps all the methods which change the state of String,Array, and Hash
|
36
|
+
|
37
|
+
Psychic.connect_mutable(object,&proc)
|
38
|
+
|
39
|
+
this is a easy way to create a listener who is notified of changes of state of a Array, String or Hash.
|
40
|
+
|
41
|
+
this is my first gem. any questions/suggestions/encouragement/requests please email. dominic.tarr@gmail.com
|
42
|
+
|
43
|
+
cheers!
|
44
|
+
|
45
|
+
|
46
|
+
|
47
|
+
|
48
|
+
|
data/lib/psychic.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
|
2
|
+
module Psychic
|
3
|
+
def self.make_args (arity)
|
4
|
+
args = []
|
5
|
+
var = nil
|
6
|
+
if(arity == 0)
|
7
|
+
return "&block"
|
8
|
+
elsif (arity < 0)
|
9
|
+
var = "*args"
|
10
|
+
arity = (arity + 1) * -1
|
11
|
+
end
|
12
|
+
arity.times{|i| args << "arg#{i}"}
|
13
|
+
if var then args << var; end
|
14
|
+
args << "&block"
|
15
|
+
return args.join(",")
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.connect (object,*methods,&block)
|
19
|
+
prefix = "psychic_"
|
20
|
+
|
21
|
+
unless psyc_c = object.send(:instance_variable_get,:@psychic_connections) then
|
22
|
+
object.send(:instance_variable_set,:@psychic_connections,psyc_c = Hash.new)
|
23
|
+
end
|
24
|
+
if block.nil? then
|
25
|
+
raise "cannot make a psychic connection to #{object.inspect} without a block"
|
26
|
+
end
|
27
|
+
|
28
|
+
methods.each {|m|
|
29
|
+
old_m = "#{prefix}#{m}"
|
30
|
+
|
31
|
+
unless psyc_c[m] and !(psyc_c[m].empty?) then
|
32
|
+
|
33
|
+
object.instance_eval "alias :\"#{old_m}\" :\"#{m}\""
|
34
|
+
psyc_c[m] = [block]
|
35
|
+
def object.psychic_connection (method, value,*args,&block)
|
36
|
+
if psy = @psychic_connections[method] then
|
37
|
+
psy.each{|b|
|
38
|
+
# argh! i've found another bug in ruby!
|
39
|
+
# def f (a,b,&block);end
|
40
|
+
# method(:f).arity == 2
|
41
|
+
# proc {|a,b,&block|}.arity == 1
|
42
|
+
unless b.arity == -4 or (b.arity == 1 and b.is_a?(Proc)) then
|
43
|
+
raise "psychic connection needs arity of block to be -4, was=#{b.arity}
|
44
|
+
suggest these arguments: |object,method,value,*args,&block|"
|
45
|
+
end
|
46
|
+
b.call(self,method,value,*args,&block)
|
47
|
+
}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
args = make_args(object.method(m).arity)
|
52
|
+
object.instance_eval "def #{m}(#{args})
|
53
|
+
value = method(:\"#{old_m}\").call(#{args})
|
54
|
+
psychic_connection(:\"#{m}\",value,#{args})
|
55
|
+
return value
|
56
|
+
end"
|
57
|
+
else
|
58
|
+
psyc_c[m] << block
|
59
|
+
end
|
60
|
+
}
|
61
|
+
end
|
62
|
+
def self.disconnect (object,*methods,&block)
|
63
|
+
psyc_c = object.send(:instance_variable_get,:@psychic_connections)
|
64
|
+
methods.each {|m|
|
65
|
+
psyc_c[m].delete block
|
66
|
+
}
|
67
|
+
end
|
68
|
+
def self.connect_mutable(object,&block) # connect to all mutable methods on certain core types.
|
69
|
+
if object.is_a? Array then
|
70
|
+
methods = [:"[]=",:"<<",:delete,:delete_at,:delete_if,:"flatten!",:insert,:"map!",:"collect!",:"slice!",:"uniq!",:"reverse!",:"compact!"]
|
71
|
+
|
72
|
+
elsif object.is_a? Hash
|
73
|
+
methods = [:"[]=",:"default=",:"merge!",:"reject!"]
|
74
|
+
|
75
|
+
elsif object.is_a? String
|
76
|
+
methods = [:"[]=",:"<<",:"capitalize!",:"chomp!",:"downcase!",:"gsub!",:"upcase!"]
|
77
|
+
end
|
78
|
+
connect(object,*methods,&block)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'psychic/psychic'
|
3
|
+
|
4
|
+
class TestPsychic < Test::Unit::TestCase
|
5
|
+
include Test::Unit
|
6
|
+
#Psychic can make observe any object.
|
7
|
+
#it's powers from from metaprogramming the subject,
|
8
|
+
#aliasing mutable methods with methods which call's a closure
|
9
|
+
#and then calling the old method.
|
10
|
+
|
11
|
+
def test_simple
|
12
|
+
called = false
|
13
|
+
Psychic.connect(h = "Hello", :length) {|obj,method,value,*args|
|
14
|
+
assert_equal h,obj
|
15
|
+
assert_equal :length,method
|
16
|
+
assert_equal 5,value
|
17
|
+
assert_equal [],args #also test on something wit args
|
18
|
+
called = true
|
19
|
+
}
|
20
|
+
assert_equal 5,h.length
|
21
|
+
assert called, "expected psychic connection on \"#{h}\".length"
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_with_args
|
25
|
+
called = 0
|
26
|
+
deleted = []
|
27
|
+
Psychic.connect(h = [:a,:b,:c], :delete) {|obj,method,value,*args,&block|
|
28
|
+
assert_equal h,obj
|
29
|
+
assert_equal :delete,method
|
30
|
+
assert Symbol === value
|
31
|
+
assert_equal [value] ,args
|
32
|
+
deleted << value
|
33
|
+
called = called + 1
|
34
|
+
}
|
35
|
+
h.delete :a
|
36
|
+
h.delete :b
|
37
|
+
h.delete :c
|
38
|
+
|
39
|
+
assert_equal 3,called
|
40
|
+
assert_equal deleted,[:a,:b,:c]
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_multiple_connections
|
44
|
+
|
45
|
+
b_delete = b_called = called = 0
|
46
|
+
deleted = []
|
47
|
+
|
48
|
+
Psychic.connect(h = [:a,:b,:c], :delete) {|obj,method,value,*args|
|
49
|
+
assert_equal h,obj
|
50
|
+
assert_equal :delete,method
|
51
|
+
assert Symbol === value
|
52
|
+
assert_equal [value] ,args
|
53
|
+
deleted << value
|
54
|
+
called = called + 1
|
55
|
+
}
|
56
|
+
Psychic.connect(h,:"<<", :delete) {|obj,method,value,*args|
|
57
|
+
assert_equal h,obj
|
58
|
+
b_called = b_called + 1
|
59
|
+
if method == :delete then
|
60
|
+
b_delete = b_delete + 1
|
61
|
+
end
|
62
|
+
}
|
63
|
+
h.delete :a
|
64
|
+
h.delete :b
|
65
|
+
h.delete :c
|
66
|
+
h << :d
|
67
|
+
h << :e
|
68
|
+
h << :f
|
69
|
+
|
70
|
+
assert_equal 3,called
|
71
|
+
assert_equal 6,b_called
|
72
|
+
assert_equal 3,called
|
73
|
+
assert_equal deleted,[:a,:b,:c]
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_disconnect
|
77
|
+
h = "Hello"
|
78
|
+
called = false
|
79
|
+
p = proc {|obj,method,value,*args,&block|
|
80
|
+
assert_equal h,obj
|
81
|
+
assert_equal :length,method
|
82
|
+
assert_equal 5,value
|
83
|
+
assert_equal [],args #also test on something wit args
|
84
|
+
called = true
|
85
|
+
}
|
86
|
+
Psychic.connect(h, :length,&p)
|
87
|
+
|
88
|
+
assert_equal 5,h.length
|
89
|
+
assert called, "expected psychic connection on \"#{h}\".length"
|
90
|
+
Psychic.disconnect(h,:length,&p)
|
91
|
+
called = false
|
92
|
+
assert_equal 5,h.length
|
93
|
+
assert !called, "expected psychic connection disconnected on \"#{h}\".length"
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
def expect (state,code)
|
99
|
+
assert state, "closure was not called for opperation: \"#{code}\""
|
100
|
+
false
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_mutable(klass,code)#checks that result actually changes, and the block is called.
|
104
|
+
before = nil
|
105
|
+
a = klass.new
|
106
|
+
c = false
|
107
|
+
Psychic.connect_mutable(a) {|object,method,value,*args,&block|
|
108
|
+
puts "#{before.inspect}.#{method}(#{args.join(',')}) => #{object.inspect}"
|
109
|
+
c = true
|
110
|
+
}
|
111
|
+
b = binding
|
112
|
+
code.split("\n").each{|s|
|
113
|
+
before = a.dup
|
114
|
+
eval(s,b);
|
115
|
+
c = expect(c,s)
|
116
|
+
assert a != before, "expected opperation: #{s} to alter #{a.inspect} from #{before.inspect}"
|
117
|
+
}
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
def test_array
|
122
|
+
code = "a << :a
|
123
|
+
a.delete :a
|
124
|
+
a << :a
|
125
|
+
a << :b
|
126
|
+
a << :c
|
127
|
+
a.reverse!
|
128
|
+
a.reverse!
|
129
|
+
a[1] = 2
|
130
|
+
a[2] = 0
|
131
|
+
a << [1,2,3]
|
132
|
+
a << nil
|
133
|
+
a.compact!
|
134
|
+
a.flatten!
|
135
|
+
a.slice!(1,4)
|
136
|
+
a << a.dup
|
137
|
+
a.flatten!
|
138
|
+
a.map! {|x| x.to_s + '!'}
|
139
|
+
a.uniq!"
|
140
|
+
test_mutable(Array,code)
|
141
|
+
end
|
142
|
+
|
143
|
+
def test_hash
|
144
|
+
test_mutable(Hash,"a[1] = 123
|
145
|
+
a[2] = 1231
|
146
|
+
a[3] = 325235
|
147
|
+
a.reject! {|k,v| v > 2000}
|
148
|
+
a.merge! ({:a => :b})")
|
149
|
+
end
|
150
|
+
|
151
|
+
def test_string
|
152
|
+
test_mutable(String,"a << 'hello'
|
153
|
+
a.capitalize!
|
154
|
+
a.upcase!
|
155
|
+
a.downcase!
|
156
|
+
a['e'] = 'XXX'
|
157
|
+
a << 'XXX'
|
158
|
+
a.gsub!(/X/,'_')
|
159
|
+
a.chomp!('_')")
|
160
|
+
#what about genetic algorithm to figure out test for when a function will change a state?
|
161
|
+
#example, [:a].delete :a will change state, because [:a].include? :a
|
162
|
+
# << will always change the state
|
163
|
+
#also, we can discover what will produce errors.
|
164
|
+
#or inductively prove that a method can't make errors.
|
165
|
+
#it it's found an error, find most concise test which predicts error.
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
metadata
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: psychic
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dominic Tarr
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-06-16 00:00:00 +12:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: " can inject a block into any method on any object, which will be called back when that method is called.\n for example, may be used to listen to state changes on Array's, Hash's etc.\n"
|
17
|
+
email: dominic.tarr@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- lib/psychic.rb
|
26
|
+
- test/test_psychic.rb
|
27
|
+
- README
|
28
|
+
has_rdoc: true
|
29
|
+
homepage:
|
30
|
+
licenses: []
|
31
|
+
|
32
|
+
post_install_message:
|
33
|
+
rdoc_options: []
|
34
|
+
|
35
|
+
require_paths:
|
36
|
+
- lib
|
37
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: "0"
|
42
|
+
version:
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: "0"
|
48
|
+
version:
|
49
|
+
requirements: []
|
50
|
+
|
51
|
+
rubyforge_project:
|
52
|
+
rubygems_version: 1.3.5
|
53
|
+
signing_key:
|
54
|
+
specification_version: 3
|
55
|
+
summary: inject listener block into any methods on any object
|
56
|
+
test_files: []
|
57
|
+
|