psychic 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/README +48 -0
  2. data/lib/psychic.rb +81 -0
  3. data/test/test_psychic.rb +168 -0
  4. 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
+
@@ -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
+