caruby-core 1.4.1
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 +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
|