caruby-core 1.4.2 → 1.4.3
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/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
|