virtual_keywords 0.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.
@@ -0,0 +1,181 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'instance_methods_of' do
4
+ it 'retrieves the instance methods of a class' do
5
+ method_names = VirtualKeywords::ClassReflection.instance_methods_of(
6
+ Fizzbuzzer).keys
7
+ method_names.should include 'fizzbuzz'
8
+ end
9
+ end
10
+
11
+ describe 'subclasses_of_classes' do
12
+ it 'finds the subclasses of classes and flattens the result' do
13
+ rails_classes = [ActiveRecord::Base, ApplicationController]
14
+ subclasses = VirtualKeywords::ClassReflection.
15
+ subclasses_of_classes rails_classes
16
+
17
+ subclasses.should include Fizzbuzzer
18
+ subclasses.should include Greeter
19
+ end
20
+ end
21
+
22
+ describe 'install_method_on_class' do
23
+ before :each do
24
+ class MyClass
25
+ def foo
26
+ :hello
27
+ end
28
+ end
29
+ @object = MyClass.new
30
+ end
31
+
32
+ it 'installs methods on classes' do
33
+ VirtualKeywords::ClassReflection.install_method_on_class(
34
+ MyClass, 'def foo; :goodbye; end')
35
+ @object.foo.should eql :goodbye
36
+ end
37
+
38
+ it 'installs methods that change instance fields' do
39
+ class MyClass
40
+ def foo
41
+ :hello
42
+ end
43
+ end
44
+ VirtualKeywords::ClassReflection.install_method_on_class(
45
+ MyClass, 'def foo; @bar = :bar; :goodbye; end')
46
+ VirtualKeywords::ClassReflection.install_method_on_class(
47
+ MyClass, 'def bar; @bar; end')
48
+
49
+ @object.foo.should eql :goodbye
50
+ @object.bar.should eql :bar
51
+ end
52
+
53
+ it 'installs methods that mutate globals' do
54
+ $thing = :old
55
+ VirtualKeywords::ClassReflection.install_method_on_class(
56
+ MyClass, 'def foo; $thing = :new; end')
57
+
58
+ @object.foo()
59
+ $thing.should eql :new
60
+ end
61
+ end
62
+
63
+ describe 'install_method_on_instance' do
64
+ before :each do
65
+ class MyClass
66
+ def foo
67
+ :hello
68
+ end
69
+ end
70
+ @object1 = MyClass.new
71
+ @object2 = MyClass.new
72
+ end
73
+
74
+ it 'installs methods on specific instances' do
75
+ VirtualKeywords::ClassReflection.install_method_on_instance(
76
+ @object1, 'def foo; :goodbye; end')
77
+ @object1.foo.should eql :goodbye
78
+ @object2.foo.should eql :hello
79
+ end
80
+ end
81
+
82
+ describe 'Virtualizer' do
83
+ before :each do
84
+ @greeter = Greeter.new false
85
+
86
+ class MyClass
87
+ def foo
88
+ if (2 + 2) == 4 and false
89
+ :tampered_and
90
+ else
91
+ :original
92
+ end
93
+ end
94
+
95
+ def bar
96
+ if (2 + 2) == 4 or false
97
+ :original
98
+ else
99
+ :tampered_or
100
+ end
101
+ end
102
+ end
103
+
104
+ class AnotherClass
105
+ def quux
106
+ if true then :right else :if_modified end
107
+ end
108
+ end
109
+
110
+ class YetAnotherClass < AnotherClass
111
+ def quux
112
+ if false then :if_modified else :right end
113
+ end
114
+ end
115
+
116
+ @my_class = MyClass.new
117
+ @another_class = AnotherClass.new
118
+ @yet_another_class = YetAnotherClass.new
119
+ @operator_user = OperatorUser.new false
120
+ @virtualizer = VirtualKeywords::Virtualizer.new(
121
+ :for_instances => [@greeter, @my_class]
122
+ )
123
+ @class_virtualizer = VirtualKeywords::Virtualizer.new(
124
+ :for_classes => [AnotherClass]
125
+ )
126
+ @subclass_virtualizer = VirtualKeywords::Virtualizer.new(
127
+ :for_subclasses_of => [AnotherClass]
128
+ )
129
+ @rails_subclass_virtualizer = VirtualKeywords::Virtualizer.new(
130
+ :for_subclasses_of => [ActiveRecord::Base, ApplicationController]
131
+ )
132
+ end
133
+
134
+ it 'virtualizes "if" on instances' do
135
+ @virtualizer.virtual_if do |condition, then_do, else_do|
136
+ :clobbered_if
137
+ end
138
+ result = @greeter.greet_if_else
139
+ result.should eql :clobbered_if
140
+ end
141
+
142
+ it 'virtualizes "and" on instances' do
143
+ @virtualizer.virtual_and do |first, second|
144
+ first.call or second.call
145
+ end
146
+ @my_class.foo.should eql :tampered_and
147
+ end
148
+
149
+ it 'virtualizes "or" on instances' do
150
+ @virtualizer.virtual_or do |first, second|
151
+ first.call and second.call
152
+ end
153
+ @my_class.bar.should eql :tampered_or
154
+ end
155
+
156
+ it 'virtualizes "if" on classes' do
157
+ @class_virtualizer.virtual_if do |condition, then_do, else_do|
158
+ if not condition.call
159
+ then_do.call
160
+ else
161
+ else_do.call
162
+ end
163
+ end
164
+
165
+ @another_class.quux.should eql :if_modified
166
+ end
167
+
168
+ it 'virtualizes "if" on subclasses of given classes' do
169
+ @subclass_virtualizer.virtual_if do |condition, then_do, else_do|
170
+ if not condition.call
171
+ then_do.call
172
+ else
173
+ else_do.call
174
+ end
175
+ end
176
+
177
+ # AnotherClass shouldn't be modified, it's not a subclass of itself
178
+ @another_class.quux.should eql :right
179
+ @yet_another_class.quux.should eql :if_modified
180
+ end
181
+ end
@@ -0,0 +1,39 @@
1
+ module VirtualKeywords
2
+ # Simple data object holding a Class and the name of one of its methods
3
+ ClassAndMethodName = Struct.new(:klass, :method_name)
4
+
5
+ # Class that takes classes and "mirrors" them, by parsing their methods
6
+ # and storing the results.
7
+ class ClassMirrorer
8
+ # Initialize a ClassMirrorer
9
+ #
10
+ # Arguments:
11
+ # parser: (Class) an object with a method translate, that takes a class
12
+ # and method name, and returns a syntax tree that can be
13
+ # sexpified (optional, uses ParseTree by default).
14
+ def initialize(parser = ParseTree)
15
+ @parser = parser
16
+ end
17
+
18
+ # Map ClassAndMethodNames to outputs of parser.translate
19
+ #
20
+ # Arguments:
21
+ # klasses: (Array[Class]) the classes to mirror.
22
+ #
23
+ # Returns:
24
+ # (Hash[ClassAndMethodName, Array]) a hash mapping every method of every
25
+ # class to parsed output.
26
+ def mirror(klasses)
27
+ methods = {}
28
+ klasses.each do |klass|
29
+ klass.instance_methods.each do |method_name|
30
+ key = ClassAndMethodName.new(klass, method_name)
31
+ translated = @parser.translate(klass, method_name)
32
+ methods[key] = translated
33
+ end
34
+ end
35
+
36
+ methods
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,108 @@
1
+ module VirtualKeywords
2
+ class IfRewriter < SexpProcessor
3
+ # Initialize an IfRewriter (self.strict is false)
4
+ def initialize
5
+ super
6
+ self.strict = false
7
+ end
8
+
9
+ # Rewrite an :if sexp. SexpProcessor#process is a template method that will
10
+ # call this method every time it encounters an :if.
11
+ #
12
+ # Arguments:
13
+ # expression: (Sexp) the :if sexp to be rewritten.
14
+ #
15
+ # Returns:
16
+ # (Sexp) A rewritten sexp that calls
17
+ # VirtualKeywords::REWRITTEN_KEYWORDS.call_if with the condition,
18
+ # then clause, and else clause as arguments, all wrapped in lambdas.
19
+ # It must also pass self to call_if, so REWRITTEN_KEYWORDS can decide
20
+ # which of the lambdas registered with it should be called.
21
+ def rewrite_if(expression)
22
+ # The sexp for the condition passed to if is inside expression[1]
23
+ # We can further process this sexp if it has and/or in it.
24
+ condition = expression[1]
25
+ then_do = expression[2]
26
+ else_do = expression[3]
27
+
28
+ s(:call,
29
+ s(:colon2,
30
+ s(:const, :VirtualKeywords),
31
+ :REWRITTEN_KEYWORDS
32
+ ), :call_if,
33
+ s(:array,
34
+ s(:self),
35
+ s(:iter, s(:fcall, :lambda), nil, condition),
36
+ s(:iter, s(:fcall, :lambda), nil, then_do),
37
+ s(:iter, s(:fcall, :lambda), nil, else_do)
38
+ )
39
+ )
40
+ end
41
+ end
42
+
43
+ # Helper method. Call a 2-argument function used to replace an operator
44
+ # (like "and" or "or")
45
+ #
46
+ # Arguments:
47
+ # method_name: (Symbol) the name of the REWRITTEN_KEYWORDS method that
48
+ # should be called in the sexp.
49
+ # first: (Sexp) the first argument to the method, which should be
50
+ # wrapped in a lambda then passed to REWRITTEN_KEYWORDS.
51
+ # second: (Sexp) the second argument to the method, which should be
52
+ # wrapped in a lambda then passed to REWRITTEN_KEYWORDS.
53
+ def self.call_operator_replacement(function_name, first, second)
54
+ s(:call,
55
+ s(:colon2,
56
+ s(:const, :VirtualKeywords),
57
+ :REWRITTEN_KEYWORDS
58
+ ), function_name,
59
+ s(:array,
60
+ s(:self),
61
+ s(:iter, s(:fcall, :lambda), nil, first),
62
+ s(:iter, s(:fcall, :lambda), nil, second)
63
+ )
64
+ )
65
+ end
66
+
67
+ class AndRewriter < SexpProcessor
68
+ def initialize
69
+ super
70
+ self.strict = false
71
+ end
72
+
73
+ # Rewrite "and" expressions (automatically called by SexpProcessor#process)
74
+ #
75
+ # Arguments:
76
+ # expression: (Sexp) the :and sexp to rewrite.
77
+ #
78
+ # Returns:
79
+ # (Sexp): a sexp that instead calls REWRITTEN_KEYWORDS.call_and
80
+ def rewrite_and(expression)
81
+ first = expression[1]
82
+ second = expression[2]
83
+
84
+ VirtualKeywords.call_operator_replacement(:call_and, first, second)
85
+ end
86
+ end
87
+
88
+ class OrRewriter < SexpProcessor
89
+ def initialize
90
+ super
91
+ self.strict = false
92
+ end
93
+
94
+ # Rewrite "or" expressions (automatically called by SexpProcessor#process)
95
+ #
96
+ # Arguments:
97
+ # expression: (Sexp) the :or sexp to rewrite.
98
+ #
99
+ # Returns:
100
+ # (Sexp): a sexp that instead calls REWRITTEN_KEYWORDS.call_or
101
+ def rewrite_or(expression)
102
+ first = expression[1]
103
+ second = expression[2]
104
+
105
+ VirtualKeywords.call_operator_replacement(:call_or, first, second)
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,141 @@
1
+ module VirtualKeywords
2
+
3
+ # Simple data object holding an object and a Ruby keyword (as a symbol)
4
+ ObjectAndKeyword = Struct.new(:object, :keyword)
5
+
6
+ # Exception raised when a client tries to call the rewritten version of a
7
+ # keyword, but no lambda was provided for the given object and keyword.
8
+ class RewriteLambdaNotProvided < StandardError
9
+ end
10
+
11
+ # Class holding the lambdas to call in place of keywords.
12
+ # Different classes can have their own set of "virtualized keywords".
13
+ class RewrittenKeywords
14
+
15
+ # Initialize a RewrittenKeywords
16
+ #
17
+ # Arguments:
18
+ # A Hash with the following key:
19
+ # predicates_to_blocks: (Hash[Proc, Proc]) a hash mapping predicates that
20
+ # take ObjectAndKeywords and return true for matches
21
+ # to the lambdas that should be called in place of
22
+ # the keyword in the object's methods
23
+ # (optional, an empty Hash is the default).
24
+ def initialize(input)
25
+ @predicates_to_blocks = input[:predicates_to_blocks] || {}
26
+ end
27
+
28
+ # Register (save) a lambda to be called for a specific object.
29
+ #
30
+ # Arguments:
31
+ # object: (Object) the object whose methods will have their keyword
32
+ # virtualized.
33
+ # keyword: (Symbol) the keyword that will be virtualized
34
+ # a_lambda: (Proc) The lambda to be called in place of the keyword.
35
+ def register_lambda_for_object(object, keyword, a_lambda)
36
+ predicate = lambda { |input|
37
+ input.object == object and input.keyword == keyword
38
+ }
39
+ @predicates_to_blocks[predicate] = a_lambda
40
+ end
41
+
42
+ # Register a lambda to be called for all objects created from a class.
43
+ # The predicate will match for all objects that are initialized with
44
+ # the class (but not if they are from subclasses)
45
+ #
46
+ # Arguments:
47
+ # klass: (Class) the class whose objects will have their methods
48
+ # virtualized.
49
+ # keyword: (Symbol) the keyword that will be virtualized
50
+ # a_lambda: (Proc) The lambda to be called in place of the keyword.
51
+ def register_lambda_for_class(klass, keyword, a_lambda)
52
+ predicate = lambda { |input|
53
+ input.object.instance_of?(klass) and input.keyword == keyword
54
+ }
55
+ @predicates_to_blocks[predicate] = a_lambda
56
+ end
57
+
58
+ # Get the virtual lambda to call for the given input, or raise an
59
+ # exception if it's not there.
60
+ #
61
+ # Arguments:
62
+ # caller_object: (Object) the object part of the ObjectAndKeyword
63
+ # keyword: (Symbol) they keyword part of the ObjectAndKeyword
64
+ #
65
+ # Returns:
66
+ # The lambda to call for that object's keyword, if the object and keyword
67
+ # matched any of the predicates.
68
+ #
69
+ # Raises:
70
+ # RewriteLambdaNotProvided if no predicate returns true for
71
+ # ObjectAndKeyword.
72
+ def lambda_or_raise(caller_object, keyword)
73
+ object_and_keyword = ObjectAndKeyword.new(caller_object, keyword)
74
+ matching = @predicates_to_blocks.keys.find { |predicate|
75
+ predicate.call(object_and_keyword)
76
+ }
77
+
78
+ if matching.nil?
79
+ raise RewriteLambdaNotProvided, 'A rewrite was requested for ' +
80
+ "#{caller_object}'s #{keyword} expressions, but there's no" +
81
+ 'lambda for it.'
82
+ end
83
+
84
+ @predicates_to_blocks[matching]
85
+ end
86
+
87
+ # Call an if virtual block in place of an actual if expression.
88
+ # This function locates the lambda registered with the given object.
89
+ #
90
+ # Arguments:
91
+ # caller_object: (Object) the object whose method this is being called in.
92
+ # condition: (Proc) The condition of the if statement, wrapped in a
93
+ # lambda.
94
+ # then_do: (Proc) the lambda to execute if the condition is true (but
95
+ # the user-supplied block may do something else)
96
+ # else_do: (Proc) the lambda to execute if the condition is false (but
97
+ # the user-supplied block may do something else)
98
+ #
99
+ # Raises:
100
+ # RewriteLambdaNotProvided if no "if" lambda is available.
101
+ def call_if(caller_object, condition, then_do, else_do)
102
+ if_lambda = lambda_or_raise(caller_object, :if)
103
+ if_lambda.call(condition, then_do, else_do)
104
+ end
105
+
106
+ # Call an "and" virtual block in place of an "and" expression.
107
+ #
108
+ # Arguments:
109
+ # caller_object: (Object) the object whose method this is being called in.
110
+ # first: (Proc) The first operand of the "and", wrapped in a lambda.
111
+ # second: (Proc) The second operand of the "and", wrapped in a lambda.
112
+ #
113
+ # Raises:
114
+ # RewriteLambdaNotProvided if no "and" lambda is available.
115
+ def call_and(caller_object, first, second)
116
+ and_lambda = lambda_or_raise(caller_object, :and)
117
+ and_lambda.call(first, second)
118
+ end
119
+
120
+ # Call an "or" virtual block in place of an "or" expression.
121
+ #
122
+ # Arguments:
123
+ # caller_object: (Object) the object whose method this is being called in.
124
+ # first: (Proc) The first operand of the "or", wrapped in a lambda.
125
+ # second: (Proc) The second operand of the "or", wrapped in a lambda.
126
+ #
127
+ # Raises:
128
+ # RewriteLambdaNotProvided if no "or" lambda is available.
129
+ def call_or(caller_object, first, second)
130
+ or_lambda = lambda_or_raise(caller_object, :or)
131
+ or_lambda.call(first, second)
132
+ end
133
+ end
134
+
135
+
136
+ # The global instance of RewrittenKeywords that will be used.
137
+ # I don't normally like using global variables, but in this case
138
+ # we need a global point of access, because we can't always control the
139
+ # scope in which methods are executed.
140
+ REWRITTEN_KEYWORDS = RewrittenKeywords.new({})
141
+ end
@@ -0,0 +1,30 @@
1
+ require 'parse_tree'
2
+ require 'ruby2ruby'
3
+
4
+ module VirtualKeywords
5
+
6
+ # Class that turns a sexp back into a string of Ruby code.
7
+ class SexpStringifier
8
+ # Initialize the SexpStringifier
9
+ #
10
+ # Arguments:
11
+ # unifier: (Unifier) a Unifier, used by ParseTree/ruby2ruby (optional)
12
+ # ruby2ruby: (Ruby2Ruby) a Ruby2Ruby, used by ParseTree/ruby2ruby
13
+ # (optional)
14
+ def initialize(unifier = Unifier.new, ruby2ruby = Ruby2Ruby.new)
15
+ @unifier = unifier
16
+ @ruby2ruby = ruby2ruby
17
+ end
18
+
19
+ # Turn a sexp into a string of Ruby code.
20
+ #
21
+ # Arguments:
22
+ # sexp: (Sexp) the sexp to be stringified.
23
+ #
24
+ # Returns:
25
+ # (String) Ruby code equivalent to the sexp.
26
+ def stringify(sexp)
27
+ @ruby2ruby.process(@unifier.process(sexp))
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ module VirtualKeywords
2
+ VERSION = '0.0.0'
3
+ end