caruby-core 1.4.2 → 1.4.3
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +10 -0
- data/lib/caruby/cli/command.rb +10 -8
- data/lib/caruby/database/fetched_matcher.rb +28 -39
- data/lib/caruby/database/lazy_loader.rb +101 -0
- data/lib/caruby/database/persistable.rb +190 -167
- data/lib/caruby/database/persistence_service.rb +21 -7
- data/lib/caruby/database/persistifier.rb +185 -0
- data/lib/caruby/database/reader.rb +106 -176
- data/lib/caruby/database/saved_matcher.rb +56 -0
- data/lib/caruby/database/search_template_builder.rb +1 -1
- data/lib/caruby/database/sql_executor.rb +8 -7
- data/lib/caruby/database/store_template_builder.rb +134 -61
- data/lib/caruby/database/writer.rb +252 -52
- data/lib/caruby/database.rb +88 -67
- data/lib/caruby/domain/attribute_initializer.rb +16 -0
- data/lib/caruby/domain/attribute_metadata.rb +161 -72
- data/lib/caruby/domain/id_alias.rb +22 -0
- data/lib/caruby/domain/inversible.rb +91 -0
- data/lib/caruby/domain/merge.rb +116 -35
- data/lib/caruby/domain/properties.rb +1 -1
- data/lib/caruby/domain/reference_visitor.rb +207 -71
- data/lib/caruby/domain/resource_attributes.rb +93 -80
- data/lib/caruby/domain/resource_dependency.rb +22 -97
- data/lib/caruby/domain/resource_introspection.rb +21 -28
- data/lib/caruby/domain/resource_inverse.rb +134 -0
- data/lib/caruby/domain/resource_metadata.rb +41 -19
- data/lib/caruby/domain/resource_module.rb +42 -33
- data/lib/caruby/import/java.rb +8 -9
- data/lib/caruby/migration/migrator.rb +20 -7
- data/lib/caruby/migration/resource_module.rb +0 -2
- data/lib/caruby/resource.rb +132 -351
- data/lib/caruby/util/cache.rb +4 -1
- data/lib/caruby/util/class.rb +48 -1
- data/lib/caruby/util/collection.rb +54 -18
- data/lib/caruby/util/inflector.rb +7 -0
- data/lib/caruby/util/options.rb +35 -31
- data/lib/caruby/util/partial_order.rb +1 -1
- data/lib/caruby/util/properties.rb +2 -2
- data/lib/caruby/util/stopwatch.rb +16 -8
- data/lib/caruby/util/transitive_closure.rb +1 -1
- data/lib/caruby/util/visitor.rb +342 -328
- data/lib/caruby/version.rb +1 -1
- data/lib/caruby/yard/resource_metadata_handler.rb +8 -0
- data/lib/caruby.rb +2 -0
- metadata +10 -9
- data/lib/caruby/database/saved_merger.rb +0 -131
- data/lib/caruby/domain/annotatable.rb +0 -25
- data/lib/caruby/domain/annotation.rb +0 -23
- data/lib/caruby/import/annotatable_class.rb +0 -28
- data/lib/caruby/import/annotation_class.rb +0 -27
- data/lib/caruby/import/annotation_module.rb +0 -67
- data/lib/caruby/migration/resource.rb +0 -8
data/lib/caruby/util/cache.rb
CHANGED
@@ -8,13 +8,16 @@ module CaRuby
|
|
8
8
|
attr_reader :sticky
|
9
9
|
|
10
10
|
# Returns a new Cache whose value key is determined by calling the given
|
11
|
-
# extractor block on the cached value.
|
11
|
+
# extractor block on the cached value. The key must uniquely identify the
|
12
|
+
# cached value within the scope of the value class.
|
12
13
|
#
|
13
14
|
# If the value is not cached and there is a factory Proc, then the result of
|
14
15
|
# calling the factory on the missing value is cached with the value key.
|
15
16
|
#
|
16
17
|
# @param [Proc] optional factory Proc called with a missing value as argument
|
17
18
|
# to create a cached object
|
19
|
+
# @yield [value] returns the value key
|
20
|
+
# @yieldparam value the value to cach
|
18
21
|
def initialize(factory=nil, &extractor)
|
19
22
|
@factory = factory
|
20
23
|
# Make the class => { key => value } hash.
|
data/lib/caruby/util/class.rb
CHANGED
@@ -3,7 +3,7 @@ require 'enumerator'
|
|
3
3
|
class Class
|
4
4
|
# Returns an Enumerable on this class and its ancestors.
|
5
5
|
def class_hierarchy
|
6
|
-
@
|
6
|
+
@class__hierarchy ||= Enumerable::Enumerator.new(self, :each_class_in_hierarchy)
|
7
7
|
end
|
8
8
|
|
9
9
|
# Returns this class's superclass, thereby enabling class ranges, e.g.
|
@@ -58,6 +58,53 @@ class Class
|
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
61
|
+
# Defines an instance variable accessor attribute whose reader calls the block given
|
62
|
+
# to this method to create a new instance variable on demand, if necessary.
|
63
|
+
#
|
64
|
+
# For example, the declaration
|
65
|
+
# class AlertHandler
|
66
|
+
# attr_create_on_demand_accessor(:pings) { Array.new }
|
67
|
+
# end
|
68
|
+
# is equivalent to:
|
69
|
+
# class AlertHandler
|
70
|
+
# attr_writer :pings
|
71
|
+
# def pings
|
72
|
+
# instance_variable_defined?(@pings) ? @pings : Array.new
|
73
|
+
# end
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# This method is useful either as a short-hand for the create-on-demand idiom
|
77
|
+
# as shown in the example above, or when it is desirable to dynamically add a
|
78
|
+
# mix-in attribute to a class at runtime whose name is not known when the class
|
79
|
+
# is defined.
|
80
|
+
#
|
81
|
+
# @example
|
82
|
+
# class AlertHandler
|
83
|
+
# def self.handle(alert)
|
84
|
+
# attr_create_on_demand_accessor(alert) { AlertQueue.new }
|
85
|
+
# end
|
86
|
+
# end
|
87
|
+
# ...
|
88
|
+
# AlertHandler.handle(:pings)
|
89
|
+
# AlertHandler.new.pings #=> empty AlertQueue
|
90
|
+
#
|
91
|
+
# @param [Symbol] symbol the attribute to define
|
92
|
+
# @yield [obj] factory to create the new attribute value for the given instance
|
93
|
+
# @yieldparam obj the class instance for which the attribute will be set
|
94
|
+
def attr_create_on_demand_accessor(symbol)
|
95
|
+
attr_writer(symbol)
|
96
|
+
wtr = "#{symbol}=".to_sym
|
97
|
+
iv = "@#{symbol}".to_sym
|
98
|
+
# the attribute reader creates a new proxy on demand
|
99
|
+
define_method(symbol) do
|
100
|
+
instance_variable_defined?(iv) ? instance_variable_get(iv) : send(wtr, yield(self))
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Enumerates each class in the hierarchy.
|
105
|
+
#
|
106
|
+
# @yield [klass] the enumeration block
|
107
|
+
# @yieldparam [Class] klass the class in the hierarchy
|
61
108
|
def each_class_in_hierarchy
|
62
109
|
current = self
|
63
110
|
until current.nil?
|
@@ -151,6 +151,8 @@ module Enumerable
|
|
151
151
|
# Note, however, that unlike select, filter does not return an Array.
|
152
152
|
# The default filter block returns the passed item.
|
153
153
|
#
|
154
|
+
# @yield [item] filter the selection filter
|
155
|
+
# @yieldparam item the collection member to filter
|
154
156
|
# @return [Enumerable] the filtered result
|
155
157
|
# @example
|
156
158
|
# [1, nil, 3].filter.to_a #=> [1, 3]
|
@@ -174,7 +176,7 @@ module Enumerable
|
|
174
176
|
# Returns an Enumerable which iterates over items in this Enumerable and the other Enumerable in sequence, e.g.:
|
175
177
|
# [1, 2, 3] + [3, 4] #=> [1, 2, 3, 3, 4]
|
176
178
|
#
|
177
|
-
# Unlike Array
|
179
|
+
# Unlike the Array plus (+) operator, {#union} reflects changes to the underlying enumerators.
|
178
180
|
#
|
179
181
|
# @example
|
180
182
|
# a = [1, 2]
|
@@ -183,7 +185,8 @@ module Enumerable
|
|
183
185
|
# ab #=> [1, 2, 4, 5]
|
184
186
|
# a << 3
|
185
187
|
# ab #=> [1, 2, 3, 4, 5]
|
186
|
-
# @
|
188
|
+
# @param [Enumerable] other the Enumerable to compose with this Enumerable
|
189
|
+
# @return [Enumerable] an enumerator over self followed by other
|
187
190
|
def union(other)
|
188
191
|
MultiEnumerator.new(self, other)
|
189
192
|
end
|
@@ -207,16 +210,31 @@ module Enumerable
|
|
207
210
|
# Returns a new Enumerable that iterates over the base Enumerable applying the transformer block to each item, e.g.:
|
208
211
|
# [1, 2, 3].transform { |n| n * 2 }.to_a #=> [2, 4, 6]
|
209
212
|
#
|
210
|
-
# Unlike
|
213
|
+
# Unlike Array.map, {#wrap} reflects changes to the base Enumerable, e.g.:
|
211
214
|
# a = [2, 4, 6]
|
212
215
|
``# transformed = a.wrap { |n| n * 2 }
|
213
216
|
# a << 4
|
214
217
|
# transformed.to_a #=> [2, 4, 6, 8]
|
215
218
|
#
|
216
219
|
# In addition, transform has a small, fixed storage requirement, making it preferable to select for large collections.
|
217
|
-
# Note, however, that unlike
|
218
|
-
|
219
|
-
|
220
|
+
# Note, however, that unlike map, transform does not return an Array.
|
221
|
+
#
|
222
|
+
# @yield [item] the transformer on the enumerated items
|
223
|
+
# @yieldparam item an enumerated item
|
224
|
+
# @return [Enumerable] an enumeration on the transformed values
|
225
|
+
def wrap(&mapper)
|
226
|
+
Transformer.new(self, &mapper)
|
227
|
+
end
|
228
|
+
|
229
|
+
def join(other)
|
230
|
+
Joiner.new(self, other)
|
231
|
+
end
|
232
|
+
|
233
|
+
# @yield [item] the transformer on the enumerated items
|
234
|
+
# @yieldparam item an enumerated item
|
235
|
+
# @return [Enumerable] the mapped values excluding null values
|
236
|
+
def compact_map(&mapper)
|
237
|
+
wrap(&mapper).compact
|
220
238
|
end
|
221
239
|
|
222
240
|
private
|
@@ -229,30 +247,45 @@ module Enumerable
|
|
229
247
|
@filter = filter
|
230
248
|
end
|
231
249
|
|
232
|
-
# Calls block on each item which passes this Filter's filter test.
|
233
|
-
|
250
|
+
# Calls the given block on each item which passes this Filter's filter test.
|
251
|
+
#
|
252
|
+
# @yield [item] the block called on each item
|
253
|
+
# @yieldparam item the enumerated item
|
254
|
+
def each
|
234
255
|
@base.each { |item| yield(item) if @filter ? @filter.call(item) : item }
|
235
256
|
end
|
236
257
|
|
237
258
|
# Optimized for a Set base.
|
259
|
+
#
|
260
|
+
# @param [item] the item to check
|
261
|
+
# @return [Boolean] whether the item is a member of this Enumerable
|
238
262
|
def include?(item)
|
239
263
|
return false if Set === @base and not @base.include?(item)
|
240
264
|
super
|
241
265
|
end
|
242
266
|
|
243
|
-
# Adds
|
244
|
-
|
267
|
+
# Adds an item to the base Enumerable, if thif Filter's base supports it.
|
268
|
+
#
|
269
|
+
# @param item the item to add
|
270
|
+
# @return [Filter] self
|
271
|
+
def <<(item)
|
245
272
|
@base << value
|
273
|
+
self
|
246
274
|
end
|
247
275
|
|
248
|
-
# @
|
276
|
+
# @param [Enumerable] other the Enumerable to merge
|
277
|
+
# @return [Array] this Filter's filtered content merged with the other Enumerable
|
249
278
|
def merge(other)
|
250
|
-
to_a.merge(other)
|
279
|
+
to_a.merge!(other)
|
251
280
|
end
|
252
281
|
|
253
282
|
# Merges the other Enumerable into the base Enumerable, if the base supports it.
|
283
|
+
#
|
284
|
+
# @param other (see #merge)
|
285
|
+
# @return [Filter, nil] this Filter's filtered content merged with the other Enumerable
|
254
286
|
def merge!(other)
|
255
287
|
@base.merge!(other)
|
288
|
+
self
|
256
289
|
end
|
257
290
|
end
|
258
291
|
|
@@ -272,7 +305,7 @@ module Enumerable
|
|
272
305
|
self
|
273
306
|
end
|
274
307
|
|
275
|
-
# Calls block on each item after this Transformer's transformer block is applied.
|
308
|
+
# Calls the block on each item after this Transformer's transformer block is applied.
|
276
309
|
def each
|
277
310
|
@base.each { |item| yield(item.nil? ? nil : @xfm.call(item)) }
|
278
311
|
end
|
@@ -400,7 +433,7 @@ end
|
|
400
433
|
module Hashable
|
401
434
|
include Enumerable
|
402
435
|
|
403
|
-
# @see Hash#
|
436
|
+
# @see Hash#each_pair
|
404
437
|
def each_pair(&block)
|
405
438
|
each(&block)
|
406
439
|
end
|
@@ -992,15 +1025,18 @@ class Array
|
|
992
1025
|
base__flatten
|
993
1026
|
end
|
994
1027
|
|
995
|
-
#
|
1028
|
+
# Concatenates the other Enumerable to this array.
|
996
1029
|
#
|
997
|
-
#
|
1030
|
+
# @param [#to_a] other the other Enumerable
|
1031
|
+
# @raise [ArgumentError] if other does not respond to the +to_a+ method
|
998
1032
|
def add_all(other)
|
999
1033
|
return concat(other) if Array === other
|
1000
1034
|
begin
|
1001
|
-
|
1035
|
+
add_all(other.to_a)
|
1036
|
+
rescue NoMethodError
|
1037
|
+
raise
|
1002
1038
|
rescue
|
1003
|
-
raise ArgumentError.new("Can't convert #{other.class.name} to array")
|
1039
|
+
raise ArgumentError.new("Can't convert #{other.class.name} to array")
|
1004
1040
|
end
|
1005
1041
|
end
|
1006
1042
|
|
@@ -17,4 +17,11 @@ class String
|
|
17
17
|
def capitalize_first
|
18
18
|
sub(/(?:^)(.)/) { $1.upcase }
|
19
19
|
end
|
20
|
+
|
21
|
+
# @return this String with the first letter decapitalized and other letters preserved.
|
22
|
+
# @example
|
23
|
+
# "RosesAreRed".decapitalize #=> "rosesAreRed"
|
24
|
+
def decapitalize
|
25
|
+
sub(/(?:^)(.)/) { $1.downcase }
|
26
|
+
end
|
20
27
|
end
|
data/lib/caruby/util/options.rb
CHANGED
@@ -42,42 +42,46 @@ class Options
|
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
45
|
-
#
|
45
|
+
# Returns the given argument list as a hash, determined as follows:
|
46
|
+
# * If the sole argument member is a hash, then that hash is the options.
|
47
|
+
# * An argument list of option symbols followed by zero, one or more non-option parameters is composed as the option hash.
|
48
|
+
# * An empty argument list is a new empty option hash.
|
46
49
|
#
|
47
50
|
# @example
|
48
|
-
# Options.
|
49
|
-
# Options.
|
50
|
-
# Options.
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
when Hash then
|
70
|
-
options
|
71
|
-
when Array then
|
72
|
-
options.to_hash { |item| Symbol === item or raise ArgumentError.new("Option is not supported; expected Symbol, found: #{options.class}") }
|
73
|
-
when Symbol then
|
74
|
-
{options => true}
|
75
|
-
else
|
76
|
-
raise ArgumentError.new("Options argument type is not supported; expected Hash or Symbol, found: #{options.class}")
|
51
|
+
# Options.to_hash() #=> {}
|
52
|
+
# Options.to_hash(nil) #=> {}
|
53
|
+
# Options.to_hash(:a => 1) #=> {:a => 1}
|
54
|
+
# Options.to_hash(:a) #=> {:a => true}
|
55
|
+
# Options.to_hash(:a, 1, :b, 2) #=> {:a => 1, :b => 2}
|
56
|
+
# Options.to_hash(:a, 1, :b, :c, 2, 3) #=> {:a => 1, :b => true, :c => [2, 3]}
|
57
|
+
# @param [Array] args the option list
|
58
|
+
# @return [Hash] the option hash
|
59
|
+
def self.to_hash(*args)
|
60
|
+
unless Enumerable === args then
|
61
|
+
raise ArgumentError.new("Expected Enumerable, found #{args.class.qp}")
|
62
|
+
end
|
63
|
+
oargs = {}
|
64
|
+
opt = args.first
|
65
|
+
return oargs if opt.nil?
|
66
|
+
return opt if oargs.empty? and Hash === opt
|
67
|
+
unless Symbol === opt then
|
68
|
+
raise ArgumentError.new("Expected Symbol as first argument, found #{args.first.class.qp}")
|
69
|
+
end
|
70
|
+
args.inject(nil) do |list, item|
|
71
|
+
Symbol === item ? oargs[item] = Array.new : list << item
|
77
72
|
end
|
73
|
+
# convert the value list to true, a single value or leave as an array
|
74
|
+
oargs.transform do |list|
|
75
|
+
case list.size
|
76
|
+
when 0 then true
|
77
|
+
when 1 then list.first
|
78
|
+
else list
|
79
|
+
end
|
80
|
+
end.to_hash
|
78
81
|
end
|
79
82
|
|
80
|
-
#
|
83
|
+
# @param [Hash, Symbol, nil] opts the options to validate
|
84
|
+
# @raise [ValidationError] if the given options are not in the given allowable choices
|
81
85
|
def self.validate(options, choices)
|
82
86
|
to_hash(options).each_key do |opt|
|
83
87
|
raise ValidationError.new("Option is not supported: #{opt}") unless choices.include?(opt)
|
@@ -9,7 +9,7 @@
|
|
9
9
|
# module Queued
|
10
10
|
# attr_reader :queue
|
11
11
|
# def <=>(other)
|
12
|
-
# raise TypeError.new("Comparison argument is not another Queued item") unless Queued
|
12
|
+
# raise TypeError.new("Comparison argument is not another Queued item") unless Queued === other
|
13
13
|
# queue.index(self) <=> queue.index(other) if queue.equal?(other.queue)
|
14
14
|
# end
|
15
15
|
# end
|
@@ -4,36 +4,42 @@ require 'benchmark'
|
|
4
4
|
class Stopwatch
|
5
5
|
# Time accumulates elapsed real time and total CPU time.
|
6
6
|
class Time
|
7
|
-
#
|
7
|
+
# @return [Benchmark::Tms] the Tms wrapped by this Time
|
8
8
|
attr_reader :tms
|
9
9
|
|
10
|
+
# @param [Benchmark::Tms, nil] the starting time (default is now)
|
10
11
|
def initialize(tms=nil)
|
11
12
|
@tms = tms || Benchmark::Tms.new
|
12
13
|
end
|
13
14
|
|
14
|
-
#
|
15
|
+
# @return [Numeric] the cumulative elapsed real clock time
|
15
16
|
def elapsed
|
16
17
|
@tms.real
|
17
18
|
end
|
18
19
|
|
19
|
-
#
|
20
|
+
# @return [Numeric] the cumulative CPU total time
|
20
21
|
def cpu
|
21
22
|
@tms.total
|
22
23
|
end
|
23
24
|
|
24
|
-
# Adds the time to execute the given block to this time.
|
25
|
+
# Adds the time to execute the given block to this time.
|
26
|
+
#
|
27
|
+
# @return [Numeric] the split execution Time
|
25
28
|
def split(&block)
|
26
29
|
stms = Benchmark.measure(&block)
|
27
30
|
@tms += stms
|
28
31
|
Time.new(stms)
|
29
32
|
end
|
30
33
|
|
34
|
+
# Sets this benchmark timer to zero.
|
31
35
|
def reset
|
32
36
|
@tms = Benchmark::Tms.new
|
33
37
|
end
|
34
38
|
end
|
35
39
|
|
36
|
-
# Executes the given block
|
40
|
+
# Executes the given block
|
41
|
+
#
|
42
|
+
# @return [Numeric] the execution Time
|
37
43
|
def self.measure(&block)
|
38
44
|
new.run(&block)
|
39
45
|
end
|
@@ -44,17 +50,19 @@ class Stopwatch
|
|
44
50
|
end
|
45
51
|
|
46
52
|
# Executes the given block. Accumulates the execution time in this Stopwatch.
|
47
|
-
#
|
53
|
+
#
|
54
|
+
# @return [Numeric] the execution run Time
|
48
55
|
def run(&block)
|
49
56
|
@time.split(&block)
|
50
57
|
end
|
51
58
|
|
52
|
-
#
|
59
|
+
# @return [Numeric] the cumulative elapsed real clock time spent in {#run} executions
|
53
60
|
def elapsed
|
54
61
|
@time.elapsed
|
55
62
|
end
|
56
63
|
|
57
|
-
#
|
64
|
+
# @return [Numeric] the cumulative CPU total time spent in {#run} executions for the
|
65
|
+
# current process and its children
|
58
66
|
def cpu
|
59
67
|
@time.cpu
|
60
68
|
end
|
@@ -26,7 +26,7 @@ class Object
|
|
26
26
|
def transitive_closure(method=nil)
|
27
27
|
raise ArgumentError.new("Missing both a method argument and a block") if method.nil? and not block_given?
|
28
28
|
return transitive_closure() { |node| node.send(method) } if method
|
29
|
-
Visitor.new(:depth_first) { |node| yield node }.to_enum(self).to_a.reverse
|
29
|
+
CaRuby::Visitor.new(:depth_first) { |node| yield node }.to_enum(self).to_a.reverse
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
data/lib/caruby/util/visitor.rb
CHANGED
@@ -5,346 +5,360 @@ require 'caruby/util/options'
|
|
5
5
|
require 'enumerator'
|
6
6
|
require 'generator'
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
# @
|
17
|
-
# @
|
18
|
-
# @parent
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
# parent.value #=> 2
|
26
|
-
#
|
27
|
-
#
|
28
|
-
# The visit result is the result of evaluating the operation block on the initial visited node.
|
29
|
-
# Visiting a collection returns an array of the result of visiting each member of the collection,
|
30
|
-
# e.g. augmenting the preceding example:
|
31
|
-
# parent2 = Node.new(3)
|
32
|
-
# child2 = Node.new(4, parent2)
|
33
|
-
# Visitor.new { |node| node.children }.visit([parent, parent2]) { |node| node.value *= multiplier } #=> [2, 6]
|
34
|
-
# Each visit captures the visit result in the +visited+ hash, e.g.:
|
35
|
-
# parent = Node.new(1)
|
36
|
-
# child = Node.new(2, parent)
|
37
|
-
# visitor = Visitor.new { |node| node.children }
|
38
|
-
# visitor.visit([parent]) { |node| node.value += 1 }
|
39
|
-
# parent.value #=> 2
|
40
|
-
# visitor.visited[parent] #=> 2
|
41
|
-
# child.value #=> 3
|
42
|
-
# visitor.visited[child] #=> 3
|
43
|
-
#
|
44
|
-
# A +return+ from the operation block terminates the visit and exits from the defining scope with the block return value,
|
45
|
-
# e.g. given the preceding example:
|
46
|
-
# def increment(parent, limit)
|
47
|
-
# Visitor.new { |node| node.children }.visit(parent) { |node| node.value < limit ? node.value += 1 : return }
|
48
|
-
# end
|
49
|
-
# increment(parent, 2) #=> nil
|
50
|
-
# parent.value #=> 2
|
51
|
-
# child.value #=> 2
|
52
|
-
#
|
53
|
-
# The to_enum method allows navigator iteration, e.g.:
|
54
|
-
# Visitor.new { |node| node.children }.to_enum(parent).detect { |node| node.value == 2 }
|
55
|
-
class Visitor
|
56
|
-
|
57
|
-
attr_reader :options, :visited, :lineage, :cycles
|
58
|
-
|
59
|
-
# Creates a new Visitor which traverses the child objects returned by the navigator block.
|
60
|
-
# The navigator block takes a parent argument and returns the children to visit. If the block
|
61
|
-
# return value is not nil and not a collection, then the returned object is visited. A nil or
|
62
|
-
# empty child is not visited.
|
63
|
-
#
|
64
|
-
# options is a symbol => value hash. A Symbol argument _symbol_ is the same as +{+_symbol_+=>true}+.
|
65
|
-
# Supported options include the follwing:
|
66
|
-
#
|
67
|
-
# The value of :depth_first can be +true+, +false+ or a Proc. If the value is a Proc, then
|
68
|
-
# value determines whether a child is visited depth-first. See the {#visit} method for more information.
|
69
|
-
#
|
70
|
-
# If the the :visited option is set, then the visited nodes are recorded in the :visited option hash.
|
71
|
-
# In that case, the {#visit} call does not clear the visited hash.
|
72
|
-
#
|
73
|
-
# If the :operator option is set, then the visit operator block is called when a node is visited.
|
74
|
-
# The operator block argument is the visited node.
|
75
|
-
#
|
76
|
-
# @param [Symbol, {Symbol => Object}] options the visit options. A symbol argument is the same
|
77
|
-
# as symbol => true
|
78
|
-
# @option options [String] :depth_first depth-first traversal
|
79
|
-
# @option options [Hash] :visited the hash to use when recording visited node => value associations
|
80
|
-
# @option options [Proc] :operator the visit operator block
|
81
|
-
# @option options [String] :prune_cycle flag indicating whether to exclude cycles to the root in a visit
|
82
|
-
# @yield [parent] the parent being visited
|
83
|
-
def initialize(options=nil, &navigator)
|
84
|
-
@navigator = navigator
|
85
|
-
@options = Options.to_hash(options)
|
86
|
-
@depth_first_flag = @options[:depth_first]
|
87
|
-
@visited = @options[:visited] || {}
|
88
|
-
@prune_cycle_flag = @options[:prune_cycle]
|
89
|
-
@lineage = []
|
90
|
-
@cycles = []
|
91
|
-
@exclude = Set.new
|
92
|
-
end
|
93
|
-
|
94
|
-
# Navigates to node and the children returned by this Visitor's navigator block.
|
95
|
-
# Applies the optional operator block to each child node if the block is given to this method.
|
96
|
-
# Returns the result of the operator block if given, or the node itself otherwise.
|
97
|
-
#
|
98
|
-
# The nodes to visit from a parent node are determined in the following sequence:
|
99
|
-
# * Return if the parent node has already been visited.
|
100
|
-
# * If depth_first, then call the navigator block defined in the initializer on
|
101
|
-
# the parent node and visit each child node.
|
102
|
-
# * Visit the parent node.
|
103
|
-
# * If not depth-first, then call the navigator block defined in the initializer
|
104
|
-
# on the parent node and visit each child node.
|
105
|
-
# The :depth option value constrains child traversal to that number of levels.
|
106
|
-
#
|
107
|
-
# This method first clears the _visited_ hash, unless the :visited option was set in the initializer.
|
8
|
+
module CaRuby
|
9
|
+
# Error raised on a visit failure.
|
10
|
+
class VisitError < RuntimeError; end
|
11
|
+
|
12
|
+
# Visitor traverses items and applies an operation, e.g.:
|
13
|
+
# class Node
|
14
|
+
# attr_accessor :children, :value
|
15
|
+
# def initialize(value, parent=nil)
|
16
|
+
# @value = value
|
17
|
+
# @children = []
|
18
|
+
# @parent = parent
|
19
|
+
# @parent.children << self if @parent
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
# parent = Node.new(1)
|
23
|
+
# child = Node.new(2, parent)
|
24
|
+
# multiplier = 2
|
25
|
+
# CaRuby::Visitor.new { |node| node.children }.visit(parent) { |node| node.value *= multiplier } #=> 2
|
26
|
+
# parent.value #=> 2
|
27
|
+
# child.value #=> 4
|
108
28
|
#
|
109
|
-
#
|
110
|
-
#
|
111
|
-
#
|
112
|
-
#
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
#
|
118
|
-
#
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
#
|
124
|
-
def root
|
125
|
-
@lineage.first
|
126
|
-
end
|
127
|
-
|
128
|
-
# @return the current node being visited
|
129
|
-
def current
|
130
|
-
@lineage.last
|
131
|
-
end
|
132
|
-
|
133
|
-
# @return the node most recently passed as an argument to this visitor's navigator block, or nil if
|
134
|
-
# visiting the first node
|
135
|
-
def parent
|
136
|
-
@lineage[-2]
|
137
|
-
end
|
138
|
-
|
139
|
-
# @return [Enumerable] iterator over each visited node
|
140
|
-
def to_enum(node)
|
141
|
-
# could use Generator, but that results in dire behavior on any error by crashing with an elided Java lineage trace
|
142
|
-
VisitorEnumerator.new(self, node)
|
143
|
-
end
|
144
|
-
|
145
|
-
# Returns a new visitor that traverses a collection of parent nodes in lock-step fashion using this visitor.
|
146
|
-
# The synced {#visit} method applies the visit operator block to an array of child nodes taken
|
147
|
-
# from each parent node, e.g. given the class documentation example:
|
148
|
-
# parent1 = Node.new(1)
|
149
|
-
# child11 = Node.new(2, parent1)
|
150
|
-
# child12 = Node.new(3, parent1)
|
151
|
-
# parent2 = Node.new(1)
|
152
|
-
# child21 = Node.new(3, parent2)
|
153
|
-
# Visitor.new { |node| node.children }.sync.enum.to_a #=> [
|
154
|
-
# [parent1, parent2],
|
155
|
-
# [child11, child21],
|
156
|
-
# [child12, nil]
|
157
|
-
# ]
|
29
|
+
# The visit result is the result of evaluating the operation block on the initial visited node.
|
30
|
+
# Visiting a collection returns an array of the result of visiting each member of the collection,
|
31
|
+
# e.g. augmenting the preceding example:
|
32
|
+
# parent2 = Node.new(3)
|
33
|
+
# child2 = Node.new(4, parent2)
|
34
|
+
# CaRuby::Visitor.new { |node| node.children }.visit([parent, parent2]) { |node| node.value *= multiplier } #=> [2, 6]
|
35
|
+
# Each visit captures the visit result in the +visited+ hash, e.g.:
|
36
|
+
# parent = Node.new(1)
|
37
|
+
# child = Node.new(2, parent)
|
38
|
+
# visitor = CaRuby::Visitor.new { |node| node.children }
|
39
|
+
# visitor.visit([parent]) { |node| node.value += 1 }
|
40
|
+
# parent.value #=> 2
|
41
|
+
# visitor.visited[parent] #=> 2
|
42
|
+
# child.value #=> 3
|
43
|
+
# visitor.visited[child] #=> 3
|
158
44
|
#
|
159
|
-
#
|
160
|
-
#
|
161
|
-
#
|
162
|
-
#
|
163
|
-
#
|
164
|
-
#
|
165
|
-
#
|
166
|
-
#
|
167
|
-
# ]
|
45
|
+
# A +return+ from the operation block terminates the visit and exits from the defining scope with the block return value,
|
46
|
+
# e.g. given the preceding example:
|
47
|
+
# def increment(parent, limit)
|
48
|
+
# CaRuby::Visitor.new { |node| node.children }.visit(parent) { |node| node.value < limit ? node.value += 1 : return }
|
49
|
+
# end
|
50
|
+
# increment(parent, 2) #=> nil
|
51
|
+
# parent.value #=> 2
|
52
|
+
# child.value #=> 2
|
168
53
|
#
|
169
|
-
#
|
170
|
-
#
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
#
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
@visited = hash ||= {}
|
209
|
-
end
|
210
|
-
|
211
|
-
# Visits the given node using the block given to this method.
|
212
|
-
# The default block returns node.
|
213
|
-
def visit_node(node)
|
214
|
-
@visited[node] = block_given? ? yield(node) : node
|
215
|
-
end
|
216
|
-
|
217
|
-
# Returns the children to visit for the given node.
|
218
|
-
def node_children(node)
|
219
|
-
children = @navigator.call(node)
|
220
|
-
return Array::EMPTY_ARRAY if children.nil?
|
221
|
-
Enumerable === children ? children.to_a.compact : [children]
|
222
|
-
end
|
223
|
-
|
224
|
-
private
|
225
|
-
|
226
|
-
# Visits the root node and all descendants.
|
227
|
-
def visit_root(node, &operator)
|
228
|
-
clear
|
229
|
-
prune_cycle_nodes(node) if @prune_cycle_flag
|
230
|
-
# visit the root node
|
231
|
-
visit_recursive(node, &operator)
|
232
|
-
end
|
233
|
-
|
234
|
-
# Excludes the internal nodes in cycles starting and ending at the given root.
|
235
|
-
def prune_cycle_nodes(root)
|
236
|
-
@exclude.clear
|
237
|
-
# visit the root, which will detect cycles, and remove the visited nodes afterwords
|
238
|
-
@prune_cycle_flag = false
|
239
|
-
to_enum(root).collect.each { |node| @visited.delete(node) }
|
240
|
-
@prune_cycle_flag = true
|
241
|
-
# add each cyclic internal node to the exclude list
|
242
|
-
@cycles.each { |cycle| cycle[1...-1].each { |node| @exclude << node } if cycle.first == root }
|
243
|
-
end
|
244
|
-
|
245
|
-
def visit_recursive(node, &operator)
|
246
|
-
return if node.nil? or @exclude.include?(node)
|
247
|
-
# return the visited value if the node has already been visited
|
248
|
-
if @visited.has_key?(node) then
|
249
|
-
#capture a cycle
|
250
|
-
index = @lineage.index(node)
|
251
|
-
if index then
|
252
|
-
cycle = @lineage[index..-1] << node
|
253
|
-
@cycles << cycle
|
254
|
-
end
|
255
|
-
return @visited[node]
|
54
|
+
# The to_enum method allows navigator iteration, e.g.:
|
55
|
+
# CaRuby::Visitor.new { |node| node.children }.to_enum(parent).detect { |node| node.value == 2 }
|
56
|
+
class Visitor
|
57
|
+
|
58
|
+
attr_reader :options, :visited, :lineage, :cycles
|
59
|
+
|
60
|
+
# Creates a new Visitor which traverses the child objects returned by the navigator block.
|
61
|
+
# The navigator block takes a parent argument and returns the children to visit. If the block
|
62
|
+
# return value is not nil and not a collection, then the returned object is visited. A nil or
|
63
|
+
# empty child is not visited.
|
64
|
+
#
|
65
|
+
# options is a symbol => value hash. A Symbol argument _symbol_ is the same as +{+_symbol_+=>true}+.
|
66
|
+
# Supported options include the follwing:
|
67
|
+
#
|
68
|
+
# The value of :depth_first can be +true+, +false+ or a Proc. If the value is a Proc, then
|
69
|
+
# value determines whether a child is visited depth-first. See the {#visit} method for more information.
|
70
|
+
#
|
71
|
+
# If the the :visited option is set, then the visited nodes are recorded in the :visited option hash.
|
72
|
+
# In that case, the {#visit} call does not clear the visited hash.
|
73
|
+
#
|
74
|
+
# If the :operator option is set, then the visit operator block is called when a node is visited.
|
75
|
+
# The operator block argument is the visited node.
|
76
|
+
#
|
77
|
+
# @param [Symbol, {Symbol => Object}] opts the visit options. A symbol argument is the same
|
78
|
+
# as symbol => true
|
79
|
+
# @option opts [String] :depth_first depth-first traversal
|
80
|
+
# @option opts [Hash] :visited the hash to use when recording visited node => value associations
|
81
|
+
# @option opts [Proc] :operator the visit operator block
|
82
|
+
# @option opts [String] :prune_cycle flag indicating whether to exclude cycles to the root in a visit
|
83
|
+
# @yield [parent] the parent being visited
|
84
|
+
def initialize(opts=nil, &navigator)
|
85
|
+
@navigator = navigator
|
86
|
+
@options = Options.to_hash(opts)
|
87
|
+
@depth_first_flag = @options[:depth_first]
|
88
|
+
@visited = @options[:visited] || {}
|
89
|
+
@prune_cycle_flag = @options[:prune_cycle]
|
90
|
+
@lineage = []
|
91
|
+
@cycles = []
|
92
|
+
@exclude = Set.new
|
256
93
|
end
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
#
|
264
|
-
|
265
|
-
#
|
266
|
-
|
267
|
-
#
|
268
|
-
|
269
|
-
#
|
270
|
-
|
271
|
-
|
272
|
-
#
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
end
|
280
|
-
|
281
|
-
class VisitorEnumerator
|
282
|
-
include Enumerable
|
283
|
-
|
284
|
-
def initialize(visitor, node)
|
285
|
-
@visitor = visitor
|
286
|
-
@root = node
|
94
|
+
|
95
|
+
# Navigates to node and the children returned by this Visitor's navigator block.
|
96
|
+
# Applies the optional operator block to each child node if the block is given to this method.
|
97
|
+
# Returns the result of the operator block if given, or the node itself otherwise.
|
98
|
+
#
|
99
|
+
# The nodes to visit from a parent node are determined in the following sequence:
|
100
|
+
# * Return if the parent node has already been visited.
|
101
|
+
# * If depth_first, then call the navigator block defined in the initializer on
|
102
|
+
# the parent node and visit each child node.
|
103
|
+
# * Visit the parent node.
|
104
|
+
# * If not depth-first, then call the navigator block defined in the initializer
|
105
|
+
# on the parent node and visit each child node.
|
106
|
+
# The :depth option value constrains child traversal to that number of levels.
|
107
|
+
#
|
108
|
+
# This method first clears the _visited_ hash, unless the :visited option was set in the initializer.
|
109
|
+
#
|
110
|
+
# @param node the root object to visit
|
111
|
+
# @yield [visited] an operator applied to each visited object
|
112
|
+
# @yieldparam visited the object currently being visited
|
113
|
+
# @return the result of the yield block on node, or node itself if no block is given
|
114
|
+
def visit(node, &operator)
|
115
|
+
visit_root(node, &operator)
|
287
116
|
end
|
288
|
-
|
289
|
-
|
290
|
-
|
117
|
+
|
118
|
+
# @param node the node to check
|
119
|
+
# @return whether the node was visited
|
120
|
+
def visited?(node)
|
121
|
+
@visited.has_key?(node)
|
291
122
|
end
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
# @yield (see Visitor#sync)
|
297
|
-
def initialize(visitor, &matcher)
|
298
|
-
# the next node to visit is an array of child node pairs matched by the given matcher block
|
299
|
-
super() { |nodes| match_children(visitor, nodes, &matcher) }
|
123
|
+
|
124
|
+
# @return the top node visited
|
125
|
+
def root
|
126
|
+
@lineage.first
|
300
127
|
end
|
301
|
-
|
302
|
-
#
|
128
|
+
|
129
|
+
# @return the current node being visited
|
130
|
+
def current
|
131
|
+
@lineage.last
|
132
|
+
end
|
133
|
+
|
134
|
+
# @return the node most recently passed as an argument to this visitor's navigator block,
|
135
|
+
# or nil if visiting the first node
|
136
|
+
def parent
|
137
|
+
@lineage[-2]
|
138
|
+
end
|
139
|
+
|
140
|
+
# @return [Enumerable] iterator over each visited node
|
141
|
+
def to_enum(node)
|
142
|
+
# JRuby alert - could use Generator instead, but that results in dire behavior on any error
|
143
|
+
# by crashing with an elided Java lineage trace.
|
144
|
+
VisitorEnumerator.new(self, node)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Returns a new visitor that traverses a collection of parent nodes in lock-step fashion using
|
148
|
+
# this visitor. The synced {#visit} method applies the visit operator block to an array of child
|
149
|
+
# nodes taken from each parent node, e.g.:
|
150
|
+
# parent1 = Node.new(1)
|
151
|
+
# child11 = Node.new(2, parent1)
|
152
|
+
# child12 = Node.new(3, parent1)
|
153
|
+
# parent2 = Node.new(1)
|
154
|
+
# child21 = Node.new(3, parent2)
|
155
|
+
# CaRuby::Visitor.new { |node| node.children }.sync.to_enum.to_a #=> [
|
156
|
+
# [parent1, parent2],
|
157
|
+
# [child11, child21],
|
158
|
+
# [child12, nil]
|
159
|
+
# ]
|
303
160
|
#
|
304
|
-
#
|
305
|
-
#
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
161
|
+
# By default, the children are grouped in enumeration order. If a block is given to this method,
|
162
|
+
# then the block is called to match child nodes, e.g. using the above example:
|
163
|
+
# visitor = CaRuby::Visitor.new { |node| node.children }
|
164
|
+
# synced = visitor.sync { |nodes, others| nodes.to_compact_hash { others.detect { |other| node.value == other.value } } }
|
165
|
+
# synced.to_enum.to_a #=> [
|
166
|
+
# [parent1, parent2],
|
167
|
+
# [child11, nil],
|
168
|
+
# [child12, child21]
|
169
|
+
# ]
|
170
|
+
#
|
171
|
+
# @yield [nodes, others] matches node in others (optional)
|
172
|
+
# @yieldparam [<Resource>] nodes the visited nodes to match
|
173
|
+
# @yieldparam [<Resource>] others the candidates for matching the node
|
174
|
+
def sync(&matcher)
|
175
|
+
SyncVisitor.new(self, &matcher)
|
312
176
|
end
|
313
|
-
|
314
|
-
# Returns
|
177
|
+
|
178
|
+
# Returns a new Visitor which determines which nodes to visit by applying the given block
|
179
|
+
# to this visitor, e.g.:
|
180
|
+
# CaRuby::Visitor.new { |node| node.children }.filter { |parent, children| children.first if parent.age >= 18 }
|
181
|
+
# navigates to the first child of parents 18 or older.
|
315
182
|
#
|
316
|
-
#
|
317
|
-
#
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
183
|
+
# The filter block arguments consist of a parent node and an array of children nodes for the parent.
|
184
|
+
# The block can return nil, a single node to visit or a collection of nodes to visit.
|
185
|
+
#
|
186
|
+
# @return [Visitor] the filter visitor
|
187
|
+
# @yield [parent, children] the filter to select which of the children to visit next
|
188
|
+
# @yieldparam parent the currently visited node
|
189
|
+
# @yieldparam children the nodes slated by this Visitor to visit next
|
190
|
+
# @raise [ArgumentError] if a block is not given to this method
|
191
|
+
def filter
|
192
|
+
raise ArgumentError.new("Filter block not given to visitor filter method") unless block_given?
|
193
|
+
Visitor.new(@options) { |node| yield(node, node_children(node)) }
|
324
194
|
end
|
325
|
-
|
195
|
+
|
196
|
+
protected
|
197
|
+
|
198
|
+
# Resets this visitor's state in preparation for a new visit.
|
199
|
+
def clear
|
200
|
+
# clear the lineage
|
201
|
+
@lineage.clear
|
202
|
+
# if the visited hash is not shared, then clear it
|
203
|
+
@visited.clear unless @options.has_key?(:visited)
|
204
|
+
# clear the cycles
|
205
|
+
@cycles.clear
|
206
|
+
end
|
207
|
+
|
208
|
+
# Sets the visited hash.
|
209
|
+
def visited=(hash)
|
210
|
+
@visited = hash ||= {}
|
211
|
+
end
|
212
|
+
|
213
|
+
# Visits the given node using the block given to this method.
|
214
|
+
# The default block returns node.
|
215
|
+
def visit_node(node)
|
216
|
+
@visited[node] = block_given? ? yield(node) : node
|
217
|
+
end
|
218
|
+
|
219
|
+
# Returns the children to visit for the given node.
|
220
|
+
def node_children(node)
|
221
|
+
children = @navigator.call(node)
|
222
|
+
return Array::EMPTY_ARRAY if children.nil?
|
223
|
+
Enumerable === children ? children.to_a.compact : [children]
|
224
|
+
end
|
225
|
+
|
326
226
|
private
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
227
|
+
|
228
|
+
def depth_first?
|
229
|
+
@depth_first_flag
|
230
|
+
end
|
231
|
+
|
232
|
+
# Visits the root node and all descendants.
|
233
|
+
def visit_root(node, &operator)
|
234
|
+
clear
|
235
|
+
prune_cycle_nodes(node) if @prune_cycle_flag
|
236
|
+
# visit the root node
|
237
|
+
visit_recursive(node, &operator)
|
238
|
+
end
|
239
|
+
|
240
|
+
# Excludes the internal nodes in cycles starting and ending at the given root.
|
241
|
+
def prune_cycle_nodes(root)
|
242
|
+
@exclude.clear
|
243
|
+
# visit the root, which will detect cycles, and remove the visited nodes afterwords
|
244
|
+
@prune_cycle_flag = false
|
245
|
+
to_enum(root).collect.each { |node| @visited.delete(node) }
|
246
|
+
@prune_cycle_flag = true
|
247
|
+
# add each cyclic internal node to the exclude list
|
248
|
+
@cycles.each { |cycle| cycle[1...-1].each { |node| @exclude << node } if cycle.first == root }
|
249
|
+
end
|
250
|
+
|
251
|
+
def visit_recursive(node, &operator)
|
252
|
+
# bail if no node or the node is specifically excluded
|
253
|
+
return if node.nil? or @exclude.include?(node)
|
254
|
+
# return the visited value if the node has already been visited
|
255
|
+
if @visited.has_key?(node) then
|
256
|
+
#capture a cycle
|
257
|
+
index = @lineage.index(node)
|
258
|
+
if index then
|
259
|
+
cycle = @lineage[index..-1] << node
|
260
|
+
@cycles << cycle
|
261
|
+
end
|
262
|
+
return @visited[node]
|
263
|
+
end
|
264
|
+
# return nil if the node has not been visited but has been navigated in a depth-first visit
|
265
|
+
return if @lineage.include?(node)
|
266
|
+
# all systems go: visit the node graph
|
267
|
+
visit_node_and_children(node, &operator)
|
268
|
+
end
|
269
|
+
|
270
|
+
def visit_node_and_children(node, &operator)
|
271
|
+
# set the current node
|
272
|
+
@lineage.push(node)
|
273
|
+
# if depth-first, then visit the children before the current node
|
274
|
+
visit_children(node, &operator) if depth_first?
|
275
|
+
# visit the current node
|
276
|
+
result = visit_node(node, &operator)
|
277
|
+
# if not depth-first, then visit the children after the current node
|
278
|
+
visit_children(node, &operator) unless depth_first?
|
279
|
+
@lineage.pop
|
280
|
+
# return the visit result
|
281
|
+
result
|
282
|
+
end
|
283
|
+
|
284
|
+
def visit_children(node, &operator)
|
285
|
+
children = node_children(node)
|
286
|
+
children.each { |child| visit_recursive(child, &operator) }
|
287
|
+
end
|
288
|
+
|
289
|
+
class VisitorEnumerator
|
290
|
+
include Enumerable
|
291
|
+
|
292
|
+
def initialize(visitor, node)
|
293
|
+
@visitor = visitor
|
294
|
+
@root = node
|
295
|
+
end
|
296
|
+
|
297
|
+
def each
|
298
|
+
@visitor.visit(@root) { |node| yield(node) }
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
class SyncVisitor < Visitor
|
303
|
+
# @param [Visitor] visitor the Visitor which will visit synchronized input
|
304
|
+
# @yield (see Visitor#sync)
|
305
|
+
def initialize(visitor, &matcher)
|
306
|
+
# the next node to visit is an array of child node pairs matched by the given matcher block
|
307
|
+
super() { |nodes| match_children(visitor, nodes, &matcher) }
|
308
|
+
end
|
309
|
+
|
310
|
+
# Visits the given pair of nodes.
|
311
|
+
#
|
312
|
+
# Raises ArgumentError if nodes does not consist of either two node arguments or one two-item Array
|
313
|
+
# argument.
|
314
|
+
def visit(*nodes)
|
315
|
+
if nodes.size == 1 then
|
316
|
+
nodes = nodes.first
|
317
|
+
raise ArgumentError.new("Sync visitor requires a pair of entry nodes") unless nodes.size == 2
|
318
|
+
end
|
319
|
+
super(nodes)
|
320
|
+
end
|
321
|
+
|
322
|
+
# Returns an Enumerable which applies the given block to each matched node starting at the given nodes.
|
323
|
+
#
|
324
|
+
# Raises ArgumentError if nodes does not consist of either two node arguments or one two-item Array
|
325
|
+
# argument.
|
326
|
+
def to_enum(*nodes)
|
327
|
+
if nodes.size == 1 then
|
328
|
+
nodes = nodes.first
|
329
|
+
raise ArgumentError.new("Sync visitor requires a pair of entry nodes") unless nodes.size == 2
|
330
|
+
end
|
331
|
+
super(nodes)
|
332
|
+
end
|
333
|
+
|
334
|
+
private
|
335
|
+
|
336
|
+
# Returns an array of arrays of matched children from the given parent nodes. The children are matched
|
337
|
+
# using the block given to this method, if supplied, or by index otherwise.
|
338
|
+
#
|
339
|
+
# @see #sync a usage example
|
340
|
+
# @yield (see Visitor#sync)
|
341
|
+
def match_children(visitor, nodes)
|
342
|
+
# the parent nodes
|
343
|
+
p1, p2 = nodes
|
344
|
+
# this visitor's children
|
345
|
+
c1 = visitor.node_children(p1)
|
346
|
+
c2 = p2 ? visitor.node_children(p2) : []
|
347
|
+
|
348
|
+
# Apply the matcher block on each of this visitor's children and the other children.
|
349
|
+
# If no block is given, then group the children by index, which is the transpose of the array of
|
350
|
+
# children arrays.
|
351
|
+
if block_given? then
|
352
|
+
# Match each item in the first children array to an item from the second children array using
|
353
|
+
# then given block.
|
354
|
+
matches = yield(c1, c2)
|
355
|
+
c1.map { |c| [c, matches[c]] }
|
356
|
+
else
|
357
|
+
# Ensure that both children arrays are the same size.
|
358
|
+
others = c2.size <= c1.size ? c2.fill(nil, c2.size...c1.size) : c2[0, c1.size]
|
359
|
+
# The children grouped by index is the transpose of the array of children arrays.
|
360
|
+
[c1, others].transpose
|
361
|
+
end
|
348
362
|
end
|
349
363
|
end
|
350
364
|
end
|