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.
- data/lib/Rakefile +5 -0
- data/lib/sexps/count_to_ten_sexp.txt +12 -0
- data/lib/sexps/sexps_and.txt +29 -0
- data/lib/sexps/sexps_greet.txt +63 -0
- data/lib/sexps/sexps_rewritten_keywords.txt +36 -0
- data/lib/sexps/sexps_symbolic_and.txt +25 -0
- data/lib/spec/class_mirrorer_spec.rb +18 -0
- data/lib/spec/keyword_rewriter_spec.rb +247 -0
- data/lib/spec/rewritten_keywords_spec.rb +28 -0
- data/lib/spec/spec_helper.rb +218 -0
- data/lib/spec/virtualizer_spec.rb +181 -0
- data/lib/virtual_keywords/class_mirrorer.rb +39 -0
- data/lib/virtual_keywords/keyword_rewriter.rb +108 -0
- data/lib/virtual_keywords/rewritten_keywords.rb +141 -0
- data/lib/virtual_keywords/sexp_stringifier.rb +30 -0
- data/lib/virtual_keywords/version.rb +3 -0
- data/lib/virtual_keywords/virtualizer.rb +241 -0
- data/lib/virtual_keywords.rb +33 -0
- metadata +137 -0
@@ -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
|