caruby-core 1.4.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/LEGAL +5 -0
- data/LICENSE +22 -0
- data/README.md +51 -0
- data/doc/website/css/site.css +1 -5
- data/doc/website/images/avatar.png +0 -0
- data/doc/website/images/favicon.ico +0 -0
- data/doc/website/images/logo.png +0 -0
- data/doc/website/index.html +82 -0
- data/doc/website/install.html +87 -0
- data/doc/website/quick_start.html +87 -0
- data/doc/website/tissue.html +85 -0
- data/doc/website/uom.html +10 -0
- data/lib/caruby.rb +3 -0
- data/lib/caruby/active_support/README.txt +2 -0
- data/lib/caruby/active_support/core_ext/string.rb +7 -0
- data/lib/caruby/active_support/core_ext/string/inflections.rb +167 -0
- data/lib/caruby/active_support/inflections.rb +55 -0
- data/lib/caruby/active_support/inflector.rb +398 -0
- data/lib/caruby/cli/application.rb +36 -0
- data/lib/caruby/cli/command.rb +169 -0
- data/lib/caruby/csv/csv_mapper.rb +157 -0
- data/lib/caruby/csv/csvio.rb +185 -0
- data/lib/caruby/database.rb +252 -0
- data/lib/caruby/database/fetched_matcher.rb +66 -0
- data/lib/caruby/database/persistable.rb +432 -0
- data/lib/caruby/database/persistence_service.rb +162 -0
- data/lib/caruby/database/reader.rb +599 -0
- data/lib/caruby/database/saved_merger.rb +131 -0
- data/lib/caruby/database/search_template_builder.rb +59 -0
- data/lib/caruby/database/sql_executor.rb +75 -0
- data/lib/caruby/database/store_template_builder.rb +200 -0
- data/lib/caruby/database/writer.rb +469 -0
- data/lib/caruby/domain/annotatable.rb +25 -0
- data/lib/caruby/domain/annotation.rb +23 -0
- data/lib/caruby/domain/attribute_metadata.rb +447 -0
- data/lib/caruby/domain/java_attribute_metadata.rb +160 -0
- data/lib/caruby/domain/merge.rb +91 -0
- data/lib/caruby/domain/properties.rb +95 -0
- data/lib/caruby/domain/reference_visitor.rb +289 -0
- data/lib/caruby/domain/resource_attributes.rb +528 -0
- data/lib/caruby/domain/resource_dependency.rb +205 -0
- data/lib/caruby/domain/resource_introspection.rb +159 -0
- data/lib/caruby/domain/resource_metadata.rb +117 -0
- data/lib/caruby/domain/resource_module.rb +285 -0
- data/lib/caruby/domain/uniquify.rb +38 -0
- data/lib/caruby/import/annotatable_class.rb +28 -0
- data/lib/caruby/import/annotation_class.rb +27 -0
- data/lib/caruby/import/annotation_module.rb +67 -0
- data/lib/caruby/import/java.rb +338 -0
- data/lib/caruby/migration/migratable.rb +167 -0
- data/lib/caruby/migration/migrator.rb +533 -0
- data/lib/caruby/migration/resource.rb +8 -0
- data/lib/caruby/migration/resource_module.rb +11 -0
- data/lib/caruby/migration/uniquify.rb +20 -0
- data/lib/caruby/resource.rb +969 -0
- data/lib/caruby/util/attribute_path.rb +46 -0
- data/lib/caruby/util/cache.rb +53 -0
- data/lib/caruby/util/class.rb +99 -0
- data/lib/caruby/util/collection.rb +1053 -0
- data/lib/caruby/util/controlled_value.rb +35 -0
- data/lib/caruby/util/coordinate.rb +75 -0
- data/lib/caruby/util/domain_extent.rb +49 -0
- data/lib/caruby/util/file_separator.rb +65 -0
- data/lib/caruby/util/inflector.rb +20 -0
- data/lib/caruby/util/log.rb +95 -0
- data/lib/caruby/util/math.rb +12 -0
- data/lib/caruby/util/merge.rb +59 -0
- data/lib/caruby/util/module.rb +34 -0
- data/lib/caruby/util/options.rb +92 -0
- data/lib/caruby/util/partial_order.rb +36 -0
- data/lib/caruby/util/person.rb +119 -0
- data/lib/caruby/util/pretty_print.rb +184 -0
- data/lib/caruby/util/properties.rb +112 -0
- data/lib/caruby/util/stopwatch.rb +66 -0
- data/lib/caruby/util/topological_sync_enumerator.rb +53 -0
- data/lib/caruby/util/transitive_closure.rb +45 -0
- data/lib/caruby/util/tree.rb +48 -0
- data/lib/caruby/util/trie.rb +37 -0
- data/lib/caruby/util/uniquifier.rb +30 -0
- data/lib/caruby/util/validation.rb +48 -0
- data/lib/caruby/util/version.rb +56 -0
- data/lib/caruby/util/visitor.rb +351 -0
- data/lib/caruby/util/weak_hash.rb +36 -0
- data/lib/caruby/version.rb +3 -0
- metadata +186 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
|
3
|
+
# Stopwatch is a simple execution time accumulator.
|
4
|
+
class Stopwatch
|
5
|
+
# Time accumulates elapsed real time and total CPU time.
|
6
|
+
class Time
|
7
|
+
# The Benchmark::Tms wrapped by this Time.
|
8
|
+
attr_reader :tms
|
9
|
+
|
10
|
+
def initialize(tms=nil)
|
11
|
+
@tms = tms || Benchmark::Tms.new
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns the cumulative elapsed real clock time.
|
15
|
+
def elapsed
|
16
|
+
@tms.real
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns the cumulative CPU total time.
|
20
|
+
def cpu
|
21
|
+
@tms.total
|
22
|
+
end
|
23
|
+
|
24
|
+
# Adds the time to execute the given block to this time. Returns the split execution Time.
|
25
|
+
def split(&block)
|
26
|
+
stms = Benchmark.measure(&block)
|
27
|
+
@tms += stms
|
28
|
+
Time.new(stms)
|
29
|
+
end
|
30
|
+
|
31
|
+
def reset
|
32
|
+
@tms = Benchmark::Tms.new
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Executes the given block. Returns the execution Time.
|
37
|
+
def self.measure(&block)
|
38
|
+
new.run(&block)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Creates a new idle Stopwatch.
|
42
|
+
def initialize
|
43
|
+
@time = Time.new
|
44
|
+
end
|
45
|
+
|
46
|
+
# Executes the given block. Accumulates the execution time in this Stopwatch.
|
47
|
+
# Returns the execution Time.
|
48
|
+
def run(&block)
|
49
|
+
@time.split(&block)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns the cumulative elapsed real clock time spent in {#run} executions.
|
53
|
+
def elapsed
|
54
|
+
@time.elapsed
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns the cumulative CPU total time spent in {#run} executions for the current process and its children.
|
58
|
+
def cpu
|
59
|
+
@time.cpu
|
60
|
+
end
|
61
|
+
|
62
|
+
# Resets this Stopwatch's cumulative time to zero.
|
63
|
+
def reset
|
64
|
+
@time.reset
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'caruby/util/collection'
|
2
|
+
|
3
|
+
class TopologicalSyncEnumerator
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize(targets, sources, symbol, &matcher)
|
7
|
+
@tgts = targets
|
8
|
+
@srcs = sources
|
9
|
+
@mthd = symbol
|
10
|
+
@matcher = matcher || lambda { |tgt, srcs| srcs.first }
|
11
|
+
end
|
12
|
+
|
13
|
+
# Calls the given block on each matching target and source.
|
14
|
+
# Returns the matching target => source hash.
|
15
|
+
def each # :yields: target, source
|
16
|
+
# the parent hashes for targets and sources
|
17
|
+
pt = @tgts.to_compact_hash { |tgt| tgt.send(@mthd) }
|
18
|
+
ps = @srcs.to_compact_hash { |src| src.send(@mthd) }
|
19
|
+
|
20
|
+
# the child hashes
|
21
|
+
ct = LazyHash.new { Array.new }
|
22
|
+
cs = LazyHash.new { Array.new }
|
23
|
+
|
24
|
+
# collect the chidren and roots
|
25
|
+
rt = @tgts.reject { |tgt| p = pt[tgt]; ct[p] << tgt if p }
|
26
|
+
rs = @srcs.reject { |src| p = ps[src]; cs[p] << src if p }
|
27
|
+
|
28
|
+
# the match hash
|
29
|
+
matches = {}
|
30
|
+
# match recursively
|
31
|
+
each_match_recursive(rt, rs, ct, cs) do |tgt, src|
|
32
|
+
yield(tgt, src)
|
33
|
+
matches[tgt] = src
|
34
|
+
end
|
35
|
+
|
36
|
+
matches
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def each_match_recursive(targets, sources, ct, cs, &block)
|
42
|
+
# copy the sources
|
43
|
+
srcs = sources.dup
|
44
|
+
# match each target, removing the matched source for the
|
45
|
+
# next iteration
|
46
|
+
targets.each do |tgt|
|
47
|
+
src = @matcher.call(tgt, srcs) || next
|
48
|
+
yield(tgt, src)
|
49
|
+
srcs.delete(src)
|
50
|
+
each_match_recursive(ct[tgt], cs[src], ct, cs, &block)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'caruby/util/visitor'
|
2
|
+
|
3
|
+
class Object
|
4
|
+
# Returns the transitive closure over a method or block, e.g.:
|
5
|
+
# class Node
|
6
|
+
# attr_reader :parent, :children
|
7
|
+
# def initialize(name, parent=nil)
|
8
|
+
# super()
|
9
|
+
# @name = name
|
10
|
+
# @parent = parent
|
11
|
+
# @children = []
|
12
|
+
# parent.children << self if parent
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# def to_s;
|
16
|
+
# end
|
17
|
+
# a = Node.new('a'); b = Node.new('b', a), c = Node.new('c', a); d = Node.new('d', c)
|
18
|
+
# a.transitive_closure { |node| node.children }.to_a.join(", ") #=> a, b, c, d
|
19
|
+
# a.transitive_closure(:children).to_a.join(", ") #=> a, b, c, d
|
20
|
+
# This method returns an array partially ordered by the closure method, i.e.
|
21
|
+
# each node occurs before all other nodes referenced directly or indirectly by the closure method.
|
22
|
+
#
|
23
|
+
# If a method symbol or name is provided, then that method is called. Otherwise, the block is called.
|
24
|
+
# In either case, the call is expected to return an object or Enumerable of objects which also respond
|
25
|
+
# to the method or block.
|
26
|
+
def transitive_closure(method=nil)
|
27
|
+
raise ArgumentError.new("Missing both a method argument and a block") if method.nil? and not block_given?
|
28
|
+
return transitive_closure() { |node| node.send(method) } if method
|
29
|
+
Visitor.new(:depth_first) { |node| yield node }.to_enum(self).to_a.reverse
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module Enumerable
|
34
|
+
# Returns the transitive closure over all items in this Enumerable.
|
35
|
+
#
|
36
|
+
# @see Object#transitive_closure
|
37
|
+
def transitive_closure(method=nil, &navigator)
|
38
|
+
# delegate to Object if there is a method argument
|
39
|
+
return super(method, &navigator) if method
|
40
|
+
# this Enumerable's children are this Enumerable's contents
|
41
|
+
closure = super() { |node| node.equal?(self) ? self : yield(node) }
|
42
|
+
# remove this collection from the closure
|
43
|
+
closure[1..-1]
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# A Tree consists of a root node and subtree children.
|
2
|
+
# A Tree can be decorated with an optional value.
|
3
|
+
class Tree
|
4
|
+
attr_reader :root, :children
|
5
|
+
|
6
|
+
attr_accessor :value
|
7
|
+
|
8
|
+
# Creates a Trie with the given root node.
|
9
|
+
def initialize(root=nil)
|
10
|
+
@root = root
|
11
|
+
@children = []
|
12
|
+
end
|
13
|
+
|
14
|
+
# Adds a subtree rooted at the given node as a child of this tree.
|
15
|
+
def <<(node)
|
16
|
+
@children << self.class.new(node)
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the subtree at the given node path.
|
21
|
+
def subtree(*path)
|
22
|
+
return self if path.empty?
|
23
|
+
first = path.shift
|
24
|
+
tree = @children.detect { |child| child.root == first }
|
25
|
+
tree.subtree(*path) if tree
|
26
|
+
end
|
27
|
+
|
28
|
+
alias :[] :subtree
|
29
|
+
|
30
|
+
# Creates the given node path if it does not yet exist.
|
31
|
+
# Returns the subtree at the path.
|
32
|
+
def fill(*path)
|
33
|
+
return self if path.empty?
|
34
|
+
first = path.shift
|
35
|
+
tree = subtree(first)
|
36
|
+
if tree.nil? then
|
37
|
+
self << first
|
38
|
+
tree = @children.last
|
39
|
+
end
|
40
|
+
tree.fill(*path)
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_s
|
44
|
+
root_s = @root.nil? || Symbol === root ? root.inspect : root.to_s
|
45
|
+
return "[#{root_s}]" if @children.empty?
|
46
|
+
"[#{root_s} -> #{@children.join(', ')}]"
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'tree')
|
2
|
+
|
3
|
+
# A Trie[http://en.wikipedia.org/wiki/Trie] is an associative access tree structure.
|
4
|
+
#
|
5
|
+
# @example
|
6
|
+
# trie = Trie.new
|
7
|
+
# trie[:a, :b] = 1
|
8
|
+
# trie[:a, :b] #=> 1
|
9
|
+
# trie[:a, :c] #=> nil
|
10
|
+
class Trie
|
11
|
+
# Creates a new empty Trie.
|
12
|
+
def initialize
|
13
|
+
@tree = Tree.new
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return the value at the given trie path
|
17
|
+
def [](*path)
|
18
|
+
tree = @tree[nil, *path]
|
19
|
+
tree.value if tree
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return the top_level Tree for this trie
|
23
|
+
def to_tree
|
24
|
+
@tree
|
25
|
+
end
|
26
|
+
|
27
|
+
# Sets the value for a node path.
|
28
|
+
#
|
29
|
+
# @example
|
30
|
+
# trie = Trie.new
|
31
|
+
# trie[:a, :b] = 1
|
32
|
+
# trie[:a, :b] #=> 1
|
33
|
+
def []=(*path_and_value)
|
34
|
+
value = path_and_value.pop
|
35
|
+
@tree.fill(nil, *path_and_value).value = value
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# A test utility class to generate id qualifiers.
|
2
|
+
class Uniquifier
|
3
|
+
# Returns a relatively unique integral qualifier. Successive calls to this method
|
4
|
+
# within the same time zone spaced more than a millisecond apart return different
|
5
|
+
# integers. Each generated qualifier is greater than the previous by an unspecified
|
6
|
+
# amount.
|
7
|
+
def self.qualifier
|
8
|
+
# the first date that this method could be called
|
9
|
+
@first ||= Date.new(2000, 01, 01)
|
10
|
+
# days as integer + milliseconds as fraction since the first date
|
11
|
+
diff = DateTime.now - @first
|
12
|
+
# shift a tenth of a milli up into the integer portion
|
13
|
+
decimillis = diff * 24 * 60 * 60 * 10000
|
14
|
+
# truncate the fraction
|
15
|
+
decimillis.truncate
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class String
|
20
|
+
# Returns a relatively unique value obtained from the specified base value.
|
21
|
+
# Spaces are removed, e.g.:
|
22
|
+
# Uniquifier.uniquify('Test Name')
|
23
|
+
# might produce:
|
24
|
+
# Test_Name_3309388006
|
25
|
+
#
|
26
|
+
# The suffix is generated by {Uniquifier.qualifier}.
|
27
|
+
def uniquify
|
28
|
+
gsub(' ', '_') + "_#{Uniquifier.qualifier}"
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# Raised when an object fails a validation test.
|
2
|
+
class ValidationError < RuntimeError; end
|
3
|
+
|
4
|
+
class Object
|
5
|
+
# Returns whether this object is nil, false, empty, or a whitespace string.
|
6
|
+
# For example, "", " ", +nil+, [], and {} are blank.
|
7
|
+
#
|
8
|
+
# This method is borrowed from Rails ActiveSupport.
|
9
|
+
def blank?
|
10
|
+
respond_to?(:empty?) ? empty? : !self
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns whether this object is nil, empty, or a whitespace string.
|
14
|
+
# For example, "", " ", +nil+, [], and {} return +true+.
|
15
|
+
#
|
16
|
+
# This method differs from blank? in that +false+ is an allowed value.
|
17
|
+
def nil_or_empty?
|
18
|
+
nil? or (respond_to?(:empty?) and empty?)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module Validation
|
23
|
+
# A Validator is a procedure which responds to the +validate(value)+ method.
|
24
|
+
class Validator < Proc
|
25
|
+
alias :validate :call
|
26
|
+
end
|
27
|
+
|
28
|
+
# Validates that each key value in the value_type_assns value => type hash is an instance of the associated class.
|
29
|
+
#
|
30
|
+
# Raises ValidationError if the value is missing.
|
31
|
+
# Raises TypeError if the value is not the specified type.
|
32
|
+
def validate_type(value_type_assns)
|
33
|
+
TYPE_VALIDATOR.validate(value_type_assns)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def self.create_type_validator
|
39
|
+
Validator.new do |value_type_assns|
|
40
|
+
value_type_assns.each do |value, type|
|
41
|
+
raise ArgumentError.new("Missing #{type.name} argument") if value.nil?
|
42
|
+
raise TypeError.new("Unsupported argument type; expected: #{type.name} found: #{value.class.name}") unless type === value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
TYPE_VALIDATOR = create_type_validator
|
48
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# A Version is an Array of version major and minor components that is comparable to
|
2
|
+
# another version identifier based on a precedence relationship.
|
3
|
+
class Version < Array
|
4
|
+
include Comparable
|
5
|
+
|
6
|
+
attr_reader :predecessor
|
7
|
+
|
8
|
+
# Creates a new Version from the given version components and optional predecessor.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# alpha = Version.new(1, '1alpha')
|
12
|
+
# Version.new(1, 1, alpha) > alpha #=> true
|
13
|
+
def initialize(*params)
|
14
|
+
@predecessor = params.pop if self.class === params.last
|
15
|
+
super(params)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns the comparison of this version identifier to the other version identifier as follows:
|
19
|
+
# * if this version can be compared to other via the predecessor graph, then return that comparison result
|
20
|
+
# * otherwise, return a component-wise comparison
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# beta = Version.new(1, '1beta')
|
24
|
+
# Version.new(1) < beta > #=> true
|
25
|
+
# Version.new(1, 1) < beta #=> true
|
26
|
+
# Version.new(1, 1, beta) > beta #=> true
|
27
|
+
def <=>(other)
|
28
|
+
return 0 if equal?(other)
|
29
|
+
raise ArgumentError.new("Comparand is not a #{self.class}: #{other}") unless self.class === other
|
30
|
+
return -1 if other.predecessor == self
|
31
|
+
return 1 unless predecessor.nil? or predecessor < other
|
32
|
+
each_with_index do |component, index|
|
33
|
+
return 1 unless index < other.length
|
34
|
+
other_component = other[index]
|
35
|
+
if String === other_component then
|
36
|
+
component = component.to_s
|
37
|
+
elsif String === component
|
38
|
+
other_component = other_component.to_s
|
39
|
+
end
|
40
|
+
cmp = (component <=> other_component)
|
41
|
+
return cmp unless cmp.zero?
|
42
|
+
end
|
43
|
+
length < other.length ? -1 : 0
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class String
|
48
|
+
# Returns this String as a Version.
|
49
|
+
#
|
50
|
+
# @example
|
51
|
+
# "1.2.1alpha".to_version #=> [1, 2, "1alpha"]
|
52
|
+
def to_version
|
53
|
+
components = split('.').map { |component| component =~ /[\D]/ ? component : component.to_i }
|
54
|
+
Version.new(*components)
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,351 @@
|
|
1
|
+
require 'caruby/util/collection'
|
2
|
+
require 'caruby/util/options'
|
3
|
+
|
4
|
+
# Enumerator overwrites to_enum, so include it first
|
5
|
+
require 'enumerator'
|
6
|
+
require 'generator'
|
7
|
+
|
8
|
+
# Error raised on a visit failure.
|
9
|
+
class VisitError < RuntimeError; end
|
10
|
+
|
11
|
+
# Visitor traverses items and applies an operation, e.g.:
|
12
|
+
# class Node
|
13
|
+
# attr_accessor :children, :value
|
14
|
+
# def initialize(value, parent=nil)
|
15
|
+
# @value = value
|
16
|
+
# @children = []
|
17
|
+
# @parent = parent
|
18
|
+
# @parent.children << self if @parent
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
# parent = Node.new(1)
|
22
|
+
# child = Node.new(2, parent)
|
23
|
+
# multiplier = 2
|
24
|
+
# Visitor.new { |node| node.children }.visit(parent) { |node| node.value *= multiplier } #=> 2
|
25
|
+
# parent.value #=> 2
|
26
|
+
# child.value #=> 4
|
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.
|
108
|
+
#
|
109
|
+
# @param node the root object to visit
|
110
|
+
# @yield [visited] an operator applied to each visited object
|
111
|
+
# @yieldparam visited the object currently being visited
|
112
|
+
# @return the result of the yield block on node, or node itself if no block is given
|
113
|
+
def visit(node, &operator)
|
114
|
+
visit_root(node, &operator)
|
115
|
+
end
|
116
|
+
|
117
|
+
# @param node the node to check
|
118
|
+
# @return whether the node was visited
|
119
|
+
def visited?(node)
|
120
|
+
@visited.has_key?(node)
|
121
|
+
end
|
122
|
+
|
123
|
+
# @return the top node visited
|
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
|
+
# ]
|
158
|
+
#
|
159
|
+
# By default, the children are grouped in enumeration order. If a block is given to this
|
160
|
+
# method, then the block is called to match child nodes, e.g. using the above example:
|
161
|
+
# visitor = Visitor.new { |node| node.children }
|
162
|
+
# synced = visitor.sync { |node, others| others.detect { |other| node.value == other.value }
|
163
|
+
# synced.enum.to_a #=> [
|
164
|
+
# [parent1, parent2],
|
165
|
+
# [child11, nil],
|
166
|
+
# [child12, child21]
|
167
|
+
# ]
|
168
|
+
#
|
169
|
+
# @yield [node, others] matches node in others (optional)
|
170
|
+
# @yieldparam [Resource] node the visited node to match
|
171
|
+
# @yieldparam [<Resource>] the candidates for matching the node
|
172
|
+
def sync(&matcher) # :yields: node, others
|
173
|
+
SyncVisitor.new(self, &matcher)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Returns a new Visitor which determines which nodes to visit by applying the given block
|
177
|
+
# to this visitor, e.g.:
|
178
|
+
# Visitor.new { |node| node.children }.filter { |parent, children| children.first if parent.age >= 18 }
|
179
|
+
# navigates to the first child of parents 18 or older.
|
180
|
+
#
|
181
|
+
# The filter block arguments consist of a parent node and an array of children nodes for the parent.
|
182
|
+
# The block can return nil, a single node to visit or a collection of nodes to visit.
|
183
|
+
#
|
184
|
+
# @return [Visitor] the filter visitor
|
185
|
+
# @yield [parent, children] the filter to select which of the children to visit next
|
186
|
+
# @yieldparam parent the currently visited node
|
187
|
+
# @yieldparam children the nodes slated by this Visitor to visit next
|
188
|
+
# @raise [ArgumentError] if a block is not given to this method
|
189
|
+
def filter
|
190
|
+
raise ArgumentError.new("Filter block not given to visitor filter method") unless block_given?
|
191
|
+
Visitor.new(@options) { |node| yield(node, node_children(node)) }
|
192
|
+
end
|
193
|
+
|
194
|
+
protected
|
195
|
+
|
196
|
+
# Resets this visitor's state in preparation for a new visit.
|
197
|
+
def clear
|
198
|
+
# clear the lineage
|
199
|
+
@lineage.clear
|
200
|
+
# if the visited hash is not shared, then clear it
|
201
|
+
@visited.clear unless @options.has_key?(:visited)
|
202
|
+
# clear the cycles
|
203
|
+
@cycles.clear
|
204
|
+
end
|
205
|
+
|
206
|
+
# Sets the visited hash.
|
207
|
+
def visited=(hash)
|
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]
|
256
|
+
end
|
257
|
+
# return nil if the node has not been visited but has been navigated in a depth-first visit
|
258
|
+
return if @lineage.include?(node)
|
259
|
+
visit_node_and_children(node, &operator)
|
260
|
+
end
|
261
|
+
|
262
|
+
def visit_node_and_children(node, &operator)
|
263
|
+
# set the current node
|
264
|
+
@lineage.push(node)
|
265
|
+
# if depth-first, then visit the children before the current node
|
266
|
+
visit_children(node, &operator) if @depth_first_flag
|
267
|
+
# visit the current node
|
268
|
+
result = visit_node(node, &operator)
|
269
|
+
# if not depth-first, then visit the children after the current node
|
270
|
+
visit_children(node, &operator) unless @depth_first_flag
|
271
|
+
@lineage.pop
|
272
|
+
# return the visit result
|
273
|
+
result
|
274
|
+
end
|
275
|
+
|
276
|
+
def visit_children(node, &operator)
|
277
|
+
children = node_children(node)
|
278
|
+
children.each { |child| visit_recursive(child, &operator) }
|
279
|
+
end
|
280
|
+
|
281
|
+
class VisitorEnumerator
|
282
|
+
include Enumerable
|
283
|
+
|
284
|
+
def initialize(visitor, node)
|
285
|
+
@visitor = visitor
|
286
|
+
@root = node
|
287
|
+
end
|
288
|
+
|
289
|
+
def each
|
290
|
+
@visitor.visit(@root) { |node| yield(node) }
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
class SyncVisitor < Visitor
|
295
|
+
# @param [Visitor] visitor the Visitor which will visit synchronized input
|
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) }
|
300
|
+
end
|
301
|
+
|
302
|
+
# Visits the given pair of nodes.
|
303
|
+
#
|
304
|
+
# Raises ArgumentError if nodes does not consist of either two node arguments or one two-item Array
|
305
|
+
# argument.
|
306
|
+
def visit(*nodes)
|
307
|
+
if nodes.size == 1 then
|
308
|
+
nodes = nodes.first
|
309
|
+
raise ArgumentError.new("Sync visitor requires a pair of entry nodes.") unless nodes.size == 2
|
310
|
+
end
|
311
|
+
super(nodes)
|
312
|
+
end
|
313
|
+
|
314
|
+
# Returns an Enumerable which applies the given block to each matched node starting at the given nodes.
|
315
|
+
#
|
316
|
+
# Raises ArgumentError if nodes does not consist of either two node arguments or one two-item Array
|
317
|
+
# argument.
|
318
|
+
def to_enum(*nodes)
|
319
|
+
if nodes.size == 1 then
|
320
|
+
nodes = nodes.first
|
321
|
+
raise ArgumentError.new("Sync visitor requires a pair of entry nodes.") unless nodes.size == 2
|
322
|
+
end
|
323
|
+
super(nodes)
|
324
|
+
end
|
325
|
+
|
326
|
+
private
|
327
|
+
|
328
|
+
# Returns an array of arrays of matched children from the given parent nodes. The children are matched
|
329
|
+
# using the block given to this method, if supplied, or by index otherwise.
|
330
|
+
#
|
331
|
+
# @see #sync a usage example
|
332
|
+
def match_children(visitor, nodes) # :yields: child, others
|
333
|
+
# the parent nodes
|
334
|
+
p1, p2 = nodes
|
335
|
+
# this visitor's children
|
336
|
+
c1 = visitor.node_children(p1)
|
337
|
+
c2 = p2 ? visitor.node_children(p2) : []
|
338
|
+
|
339
|
+
# apply the matcher block on each of this visitor's children and the other children.
|
340
|
+
# if no block, then group the children by index, which is the transpose of the array of children arrays.
|
341
|
+
if block_given? then
|
342
|
+
c1.map { |c| [c, yield(c, c2)] }
|
343
|
+
else
|
344
|
+
# ensure that both children arrays are the same size
|
345
|
+
others = c2.size <= c1.size ? c2.fill(nil, c2.size...c1.size) : c2[0, c1.size]
|
346
|
+
# the children grouped by index is the transpose of the array of children arrays
|
347
|
+
[c1, others].transpose
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|