Skip to content

Commit 0e2a04b

Browse files
allow to call transform's method from the given block
1 parent f61daf0 commit 0e2a04b

File tree

4 files changed

+146
-90
lines changed

4 files changed

+146
-90
lines changed

lib/parslet/context.rb

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,46 @@
11
# Provides a context for tree transformations to run in. The context allows
22
# accessing each of the bindings in the bindings hash as local method.
33
#
4-
# Example:
4+
# Example:
55
#
66
# ctx = Context.new(:a => :b)
7-
# ctx.instance_eval do
7+
# ctx.instance_eval do
88
# a # => :b
99
# end
1010
#
1111
# @api private
1212
class Parslet::Context
1313
include Parslet
1414

15-
def initialize(bindings)
15+
def initialize(bindings, transform = nil)
16+
@__transform = transform if transform
1617
bindings.each do |key, value|
1718
singleton_class.send(:define_method, key) { value }
1819
instance_variable_set("@#{key}", value)
1920
end
2021
end
21-
end
22+
23+
def respond_to_missing?(method, include_private)
24+
@__transform&.respond_to?(method, include_private) || super
25+
end
26+
27+
if RUBY_VERSION >= '3'
28+
def method_missing(method, *args, **kwargs, &block)
29+
if @__transform&.respond_to?(method)
30+
@__transform.__send__(method, *args, **kwargs, &block)
31+
else
32+
super
33+
end
34+
end
35+
else
36+
def method_missing(method, *args, &block)
37+
if @__transform&.respond_to?(method)
38+
@__transform.__send__(method, *args, &block)
39+
else
40+
super
41+
end
42+
end
43+
44+
ruby2_keywords :method_missing
45+
end
46+
end

lib/parslet/transform.rb

Lines changed: 62 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@
88
# as is into the result tree.
99
#
1010
# This is almost what you would generally do with a tree visitor, except that
11-
# you can match several levels of the tree at once.
11+
# you can match several levels of the tree at once.
1212
#
1313
# As a consequence of this, the resulting tree will contain pieces of the
1414
# original tree and new pieces. Most likely, you will want to transform the
1515
# original tree wholly, so this isn't a problem.
1616
#
1717
# You will not be able to create a loop, given that each node will be replaced
1818
# only once and then left alone. This means that the results of a replacement
19-
# will not be acted upon.
19+
# will not be acted upon.
2020
#
21-
# Example:
21+
# Example:
2222
#
2323
# class Example < Parslet::Transform
2424
# rule(:string => simple(:x)) { # (1)
@@ -30,35 +30,35 @@
3030
# rule can be defined by calling #rule with the pattern as argument. The block
3131
# given will be called every time the rule matches somewhere in the tree given
3232
# to #apply. It is passed a Hash containing all the variable bindings of this
33-
# pattern match.
34-
#
35-
# In the above example, (1) illustrates a simple matching rule.
33+
# pattern match.
34+
#
35+
# In the above example, (1) illustrates a simple matching rule.
3636
#
3737
# Let's say you want to parse matching parentheses and distill a maximum nest
3838
# depth. You would probably write a parser like the one in example/parens.rb;
39-
# here's the relevant part:
39+
# here's the relevant part:
4040
#
4141
# rule(:balanced) {
4242
# str('(').as(:l) >> balanced.maybe.as(:m) >> str(')').as(:r)
4343
# }
4444
#
4545
# If you now apply this to a string like '(())', you get a intermediate parse
46-
# tree that looks like this:
46+
# tree that looks like this:
4747
#
4848
# {
49-
# l: '(',
49+
# l: '(',
5050
# m: {
51-
# l: '(',
52-
# m: nil,
53-
# r: ')'
54-
# },
55-
# r: ')'
51+
# l: '(',
52+
# m: nil,
53+
# r: ')'
54+
# },
55+
# r: ')'
5656
# }
5757
#
5858
# This parse tree is good for debugging, but what we would really like to have
59-
# is just the nesting depth. This transformation rule will produce that:
59+
# is just the nesting depth. This transformation rule will produce that:
6060
#
61-
# rule(:l => '(', :m => simple(:x), :r => ')') {
61+
# rule(:l => '(', :m => simple(:x), :r => ')') {
6262
# # innermost :m will contain nil
6363
# x.nil? ? 1 : x+1
6464
# }
@@ -67,9 +67,9 @@
6767
#
6868
# There are four ways of using this class. The first one is very much
6969
# recommended, followed by the second one for generality. The other ones are
70-
# omitted here.
70+
# omitted here.
7171
#
72-
# Recommended usage is as follows:
72+
# Recommended usage is as follows:
7373
#
7474
# class MyTransformator < Parslet::Transform
7575
# rule(...) { ... }
@@ -78,7 +78,7 @@
7878
# end
7979
# MyTransformator.new.apply(tree)
8080
#
81-
# Alternatively, you can use the Transform class as follows:
81+
# Alternatively, you can use the Transform class as follows:
8282
#
8383
# transform = Parslet::Transform.new do
8484
# rule(...) { ... }
@@ -87,12 +87,12 @@
8787
#
8888
# = Execution context
8989
#
90-
# The execution context of action blocks differs depending on the arity of
91-
# said blocks. This can be confusing. It is however somewhat intentional. You
92-
# should not create fat Transform descendants containing a lot of helper methods,
90+
# The execution context of action blocks differs depending on the arity of
91+
# said blocks. This can be confusing. It is however somewhat intentional. You
92+
# should not create fat Transform descendants containing a lot of helper methods,
9393
# instead keep your AST class construction in global scope or make it available
9494
# through a factory. The following piece of code illustrates usage of global
95-
# scope:
95+
# scope:
9696
#
9797
# transform = Parslet::Transform.new do
9898
# rule(...) { AstNode.new(a_variable) }
@@ -109,28 +109,28 @@
109109
# transform.apply(tree, :builder => Builder.new)
110110
#
111111
# As you can see, Transform allows you to inject local context for your rule
112-
# action blocks to use.
112+
# action blocks to use.
113113
#
114114
class Parslet::Transform
115115
# FIXME: Maybe only part of it? Or maybe only include into constructor
116116
# context?
117-
include Parslet
118-
117+
include Parslet
118+
119119
class << self
120120
# FIXME: Only do this for subclasses?
121121
include Parslet
122-
123-
# Define a rule for the transform subclass.
122+
123+
# Define a rule for the transform subclass.
124124
#
125125
def rule(expression, &block)
126126
@__transform_rules ||= []
127127
# Prepend new rules so they have higher precedence than older rules
128128
@__transform_rules.unshift([Parslet::Pattern.new(expression), block])
129129
end
130-
130+
131131
# Allows accessing the class' rules
132132
#
133-
def rules
133+
def rules
134134
@__transform_rules ||= []
135135
end
136136

@@ -139,47 +139,47 @@ def inherited(subclass)
139139
subclass.instance_variable_set(:@__transform_rules, rules.dup)
140140
end
141141
end
142-
143-
def initialize(raise_on_unmatch=false, &block)
142+
143+
def initialize(raise_on_unmatch=false, &block)
144144
@raise_on_unmatch = raise_on_unmatch
145145
@rules = []
146-
146+
147147
if block
148148
instance_eval(&block)
149149
end
150150
end
151-
151+
152152
# Defines a rule to be applied whenever apply is called on a tree. A rule
153-
# is composed of two parts:
154-
#
153+
# is composed of two parts:
154+
#
155155
# * an *expression pattern*
156156
# * a *transformation block*
157157
#
158158
def rule(expression, &block)
159159
# Prepend new rules so they have higher precedence than older rules
160160
@rules.unshift([Parslet::Pattern.new(expression), block])
161161
end
162-
162+
163163
# Applies the transformation to a tree that is generated by Parslet::Parser
164164
# or a simple parslet. Transformation will proceed down the tree, replacing
165-
# parts/all of it with new objects. The resulting object will be returned.
165+
# parts/all of it with new objects. The resulting object will be returned.
166166
#
167167
# Using the context parameter, you can inject bindings for the transformation.
168168
# This can be used to allow access to the outside world from transform blocks,
169169
# like so:
170-
#
170+
#
171171
# document = # some class that you act on
172172
# transform.apply(tree, document: document)
173-
#
174-
# The above will make document available to all your action blocks:
173+
#
174+
# The above will make document available to all your action blocks:
175175
#
176176
# # Variant A
177177
# rule(...) { document.foo(bar) }
178178
# # Variant B
179179
# rule(...) { |d| d[:document].foo(d[:bar]) }
180180
#
181181
# @param obj PORO ast to transform
182-
# @param context start context to inject into the bindings.
182+
# @param context start context to inject into the bindings.
183183
#
184184
def apply(obj, context=nil)
185185
transform_elt(
@@ -190,52 +190,52 @@ def apply(obj, context=nil)
190190
recurse_array(obj, context)
191191
else
192192
obj
193-
end,
193+
end,
194194
context
195195
)
196196
end
197-
197+
198198
# Executes the block on the bindings obtained by Pattern#match, if such a match
199-
# can be made. Depending on the arity of the given block, it is called in
199+
# can be made. Depending on the arity of the given block, it is called in
200200
# one of two environments: the current one or a clean toplevel environment.
201201
#
202-
# If you would like the current environment preserved, please use the
202+
# If you would like the current environment preserved, please use the
203203
# arity 1 variant of the block. Alternatively, you can inject a context object
204204
# and call methods on it (think :ctx => self).
205205
#
206206
# # the local variable a is simulated
207-
# t.call_on_match(:a => :b) { a }
207+
# t.call_on_match(:a => :b) { a }
208208
# # no change of environment here
209209
# t.call_on_match(:a => :b) { |d| d[:a] }
210210
#
211211
def call_on_match(bindings, block)
212212
if block
213213
if block.arity == 1
214-
return block.call(bindings)
214+
return instance_exec(bindings, &block)
215215
else
216-
context = Context.new(bindings)
216+
context = Context.new(bindings, self)
217217
return context.instance_eval(&block)
218218
end
219219
end
220220
end
221-
222-
# Allow easy access to all rules, the ones defined in the instance and the
223-
# ones predefined in a subclass definition.
221+
222+
# Allow easy access to all rules, the ones defined in the instance and the
223+
# ones predefined in a subclass definition.
224224
#
225-
def rules
225+
def rules
226226
self.class.rules + @rules
227227
end
228-
229-
# @api private
228+
229+
# @api private
230230
#
231-
def transform_elt(elt, context)
231+
def transform_elt(elt, context)
232232
rules.each do |pattern, block|
233233
if bindings=pattern.match(elt, context)
234234
# Produces transformed value
235235
return call_on_match(bindings, block)
236236
end
237237
end
238-
238+
239239
# No rule matched - element is not transformed
240240
if @raise_on_unmatch && elt.is_a?(Hash)
241241
elt_types = elt.map do |key, value|
@@ -247,19 +247,19 @@ def transform_elt(elt, context)
247247
end
248248
end
249249

250-
# @api private
250+
# @api private
251251
#
252-
def recurse_hash(hsh, ctx)
252+
def recurse_hash(hsh, ctx)
253253
hsh.inject({}) do |new_hsh, (k,v)|
254254
new_hsh[k] = apply(v, ctx)
255255
new_hsh
256256
end
257257
end
258-
# @api private
258+
# @api private
259259
#
260-
def recurse_array(ary, ctx)
260+
def recurse_array(ary, ctx)
261261
ary.map { |elt| apply(elt, ctx) }
262262
end
263263
end
264264

265-
require 'parslet/context'
265+
require 'parslet/context'

spec/parslet/transform/context_spec.rb

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
11
require 'spec_helper'
22

33
describe Parslet::Context do
4-
def context(*args)
5-
described_class.new(*args)
4+
let(:transform) do
5+
flexmock('transform')
66
end
7-
7+
8+
def context(bindings)
9+
described_class.new(bindings, transform)
10+
end
11+
812
it "binds hash keys as variable like things" do
913
context(:a => 'value').instance_eval { a }.
1014
should == 'value'
1115
end
16+
it "responds transform's methods" do
17+
transform.should_receive(:foo).and_return { :foo }
18+
transform.should_receive(:bar).and_return { :bar }
19+
20+
c = context(:a => 'value')
21+
assert c.respond_to?(:foo)
22+
c.foo.should == :foo
23+
assert c.respond_to?(:bar)
24+
c.bar.should == :bar
25+
end
1226
it "one contexts variables aren't the next ones" do
1327
ca = context(:a => 'b')
1428
cb = context(:b => 'c')
@@ -53,4 +67,4 @@ def foo
5367
end
5468
end
5569
end
56-
end
70+
end

0 commit comments

Comments
 (0)