jinx 2.1.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/.gitignore +14 -0
- data/.rspec +3 -0
- data/.yardopts +1 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +27 -0
- data/History.md +6 -0
- data/LEGAL +5 -0
- data/LICENSE +22 -0
- data/README.md +44 -0
- data/Rakefile +41 -0
- data/examples/family/README.md +10 -0
- data/examples/family/ext/build.xml +35 -0
- data/examples/family/ext/src/family/Address.java +68 -0
- data/examples/family/ext/src/family/Child.java +24 -0
- data/examples/family/ext/src/family/DomainObject.java +26 -0
- data/examples/family/ext/src/family/Household.java +36 -0
- data/examples/family/ext/src/family/Parent.java +48 -0
- data/examples/family/ext/src/family/Person.java +42 -0
- data/examples/family/lib/family.rb +15 -0
- data/examples/family/lib/family/address.rb +6 -0
- data/examples/family/lib/family/domain_object.rb +6 -0
- data/examples/family/lib/family/household.rb +6 -0
- data/examples/family/lib/family/parent.rb +16 -0
- data/examples/family/lib/family/person.rb +6 -0
- data/examples/model/README.md +25 -0
- data/examples/model/ext/build.xml +35 -0
- data/examples/model/ext/src/domain/Child.java +192 -0
- data/examples/model/ext/src/domain/Dependent.java +29 -0
- data/examples/model/ext/src/domain/DomainObject.java +26 -0
- data/examples/model/ext/src/domain/Independent.java +83 -0
- data/examples/model/ext/src/domain/Parent.java +129 -0
- data/examples/model/ext/src/domain/Person.java +14 -0
- data/examples/model/lib/model.rb +13 -0
- data/examples/model/lib/model/child.rb +13 -0
- data/examples/model/lib/model/domain_object.rb +6 -0
- data/examples/model/lib/model/independent.rb +11 -0
- data/examples/model/lib/model/parent.rb +17 -0
- data/jinx.gemspec +22 -0
- data/lib/jinx.rb +3 -0
- data/lib/jinx/active_support/README.txt +2 -0
- data/lib/jinx/active_support/core_ext/string.rb +7 -0
- data/lib/jinx/active_support/core_ext/string/inflections.rb +167 -0
- data/lib/jinx/active_support/inflections.rb +55 -0
- data/lib/jinx/active_support/inflector.rb +398 -0
- data/lib/jinx/cli/application.rb +36 -0
- data/lib/jinx/cli/command.rb +214 -0
- data/lib/jinx/helpers/array.rb +108 -0
- data/lib/jinx/helpers/boolean.rb +42 -0
- data/lib/jinx/helpers/case_insensitive_hash.rb +39 -0
- data/lib/jinx/helpers/class.rb +149 -0
- data/lib/jinx/helpers/collection.rb +33 -0
- data/lib/jinx/helpers/collections.rb +11 -0
- data/lib/jinx/helpers/collector.rb +20 -0
- data/lib/jinx/helpers/conditional_enumerator.rb +21 -0
- data/lib/jinx/helpers/enumerable.rb +242 -0
- data/lib/jinx/helpers/enumerate.rb +35 -0
- data/lib/jinx/helpers/error.rb +15 -0
- data/lib/jinx/helpers/file_separator.rb +65 -0
- data/lib/jinx/helpers/filter.rb +52 -0
- data/lib/jinx/helpers/flattener.rb +38 -0
- data/lib/jinx/helpers/hash.rb +12 -0
- data/lib/jinx/helpers/hashable.rb +502 -0
- data/lib/jinx/helpers/inflector.rb +36 -0
- data/lib/jinx/helpers/key_transformer_hash.rb +43 -0
- data/lib/jinx/helpers/lazy_hash.rb +44 -0
- data/lib/jinx/helpers/log.rb +106 -0
- data/lib/jinx/helpers/math.rb +12 -0
- data/lib/jinx/helpers/merge.rb +60 -0
- data/lib/jinx/helpers/module.rb +18 -0
- data/lib/jinx/helpers/multi_enumerator.rb +31 -0
- data/lib/jinx/helpers/options.rb +92 -0
- data/lib/jinx/helpers/os.rb +19 -0
- data/lib/jinx/helpers/partial_order.rb +37 -0
- data/lib/jinx/helpers/pretty_print.rb +207 -0
- data/lib/jinx/helpers/set.rb +8 -0
- data/lib/jinx/helpers/stopwatch.rb +76 -0
- data/lib/jinx/helpers/transformer.rb +24 -0
- data/lib/jinx/helpers/transitive_closure.rb +55 -0
- data/lib/jinx/helpers/uniquifier.rb +50 -0
- data/lib/jinx/helpers/validation.rb +33 -0
- data/lib/jinx/helpers/visitor.rb +370 -0
- data/lib/jinx/import/class_path_modifier.rb +77 -0
- data/lib/jinx/import/java.rb +337 -0
- data/lib/jinx/importer.rb +240 -0
- data/lib/jinx/metadata.rb +155 -0
- data/lib/jinx/metadata/attribute_enumerator.rb +73 -0
- data/lib/jinx/metadata/dependency.rb +244 -0
- data/lib/jinx/metadata/id_alias.rb +23 -0
- data/lib/jinx/metadata/introspector.rb +179 -0
- data/lib/jinx/metadata/inverse.rb +170 -0
- data/lib/jinx/metadata/java_property.rb +169 -0
- data/lib/jinx/metadata/propertied.rb +500 -0
- data/lib/jinx/metadata/property.rb +401 -0
- data/lib/jinx/metadata/property_characteristics.rb +114 -0
- data/lib/jinx/resource.rb +862 -0
- data/lib/jinx/resource/copy_visitor.rb +36 -0
- data/lib/jinx/resource/inversible.rb +90 -0
- data/lib/jinx/resource/match_visitor.rb +180 -0
- data/lib/jinx/resource/matcher.rb +20 -0
- data/lib/jinx/resource/merge_visitor.rb +73 -0
- data/lib/jinx/resource/mergeable.rb +185 -0
- data/lib/jinx/resource/reference_enumerator.rb +49 -0
- data/lib/jinx/resource/reference_path_visitor.rb +38 -0
- data/lib/jinx/resource/reference_visitor.rb +55 -0
- data/lib/jinx/resource/unique.rb +35 -0
- data/lib/jinx/version.rb +3 -0
- data/spec/defaults_spec.rb +30 -0
- data/spec/definitions/model/alias/child.rb +5 -0
- data/spec/definitions/model/base/child.rb +5 -0
- data/spec/definitions/model/base/domain_object.rb +5 -0
- data/spec/definitions/model/base/independent.rb +5 -0
- data/spec/definitions/model/defaults/child.rb +5 -0
- data/spec/definitions/model/dependency/child.rb +5 -0
- data/spec/definitions/model/dependency/parent.rb +6 -0
- data/spec/definitions/model/inverse/child.rb +5 -0
- data/spec/definitions/model/inverse/independent.rb +5 -0
- data/spec/definitions/model/inverse/parent.rb +5 -0
- data/spec/definitions/model/mandatory/child.rb +6 -0
- data/spec/dependency_spec.rb +47 -0
- data/spec/family_spec.rb +64 -0
- data/spec/inverse_spec.rb +53 -0
- data/spec/mandatory_spec.rb +43 -0
- data/spec/metadata_spec.rb +68 -0
- data/spec/resource_spec.rb +30 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/support/model.rb +19 -0
- data/test/fixtures/line_separator/cr_line_sep.txt +1 -0
- data/test/fixtures/line_separator/crlf_line_sep.txt +3 -0
- data/test/fixtures/line_separator/lf_line_sep.txt +3 -0
- data/test/fixtures/mixed/ext/build.xml +35 -0
- data/test/fixtures/mixed/ext/src/mixed/Case/Example.java +5 -0
- data/test/helper.rb +7 -0
- data/test/lib/jinx/command_test.rb +41 -0
- data/test/lib/jinx/helpers/boolean_test.rb +27 -0
- data/test/lib/jinx/helpers/class_test.rb +60 -0
- data/test/lib/jinx/helpers/collections_test.rb +402 -0
- data/test/lib/jinx/helpers/file_separator_test.rb +29 -0
- data/test/lib/jinx/helpers/inflector_test.rb +11 -0
- data/test/lib/jinx/helpers/lazy_hash_test.rb +32 -0
- data/test/lib/jinx/helpers/module_test.rb +24 -0
- data/test/lib/jinx/helpers/options_test.rb +66 -0
- data/test/lib/jinx/helpers/partial_order_test.rb +41 -0
- data/test/lib/jinx/helpers/pretty_print_test.rb +83 -0
- data/test/lib/jinx/helpers/stopwatch_test.rb +16 -0
- data/test/lib/jinx/helpers/transitive_closure_test.rb +80 -0
- data/test/lib/jinx/helpers/visitor_test.rb +288 -0
- data/test/lib/jinx/import/java_test.rb +78 -0
- data/test/lib/jinx/import/mixed_case_test.rb +16 -0
- metadata +272 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require 'jinx/resource/merge_visitor'
|
|
2
|
+
|
|
3
|
+
module Jinx
|
|
4
|
+
# A CopyVisitor copies a domain object's visitable attributes transitive closure.
|
|
5
|
+
class CopyVisitor < MergeVisitor
|
|
6
|
+
# Creates a new CopyVisitor with the options described in {MergeVisitor#initialize}.
|
|
7
|
+
# The default :copier option is {Resource#copy}.
|
|
8
|
+
#
|
|
9
|
+
# @param (see MergeVisitor#initialize)
|
|
10
|
+
# @option opts [Proc] :mergeable the mergeable domain attribute selector
|
|
11
|
+
# @option opts [Proc] :matcher the match block
|
|
12
|
+
# @option opts [Proc] :copier the unmatched source copy block
|
|
13
|
+
# @yield (see MergeVisitor#initialize)
|
|
14
|
+
# @yieldparam (see MergeVisitor#initialize)
|
|
15
|
+
def initialize(opts=nil)
|
|
16
|
+
opts = Options.to_hash(opts)
|
|
17
|
+
opts[:copier] ||= Proc.new { |src| src.copy }
|
|
18
|
+
# no match forces a copy
|
|
19
|
+
opts[:matcher] = self
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Copies the given source domain object's reference graph.
|
|
24
|
+
#
|
|
25
|
+
# @param (see MergeVisitor#visit)
|
|
26
|
+
# @return [Resource] the source copy
|
|
27
|
+
def visit(source)
|
|
28
|
+
target = @copier.call(source)
|
|
29
|
+
super(source, target)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def match(sources, targets, from=nil, property=nil)
|
|
33
|
+
Hash::EMPTY_HASH
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module Jinx
|
|
2
|
+
# {Resource} inverse integrity aspect mix-in. The {Inversible} methods are intended for the
|
|
3
|
+
# sole use of the {Inverse} class mix-in.
|
|
4
|
+
module Inversible
|
|
5
|
+
# Sets an attribute inverse by calling the attribute writer method with the other argument.
|
|
6
|
+
# If other is non-nil, then the inverse writer method is called on self.
|
|
7
|
+
#
|
|
8
|
+
# @param other [Resource] the attribute value to set
|
|
9
|
+
# @param [Symbol] writer the attribute writer method
|
|
10
|
+
# @param [Symbol] inv_writer the attribute inverse writer method defined for the other object
|
|
11
|
+
# @private
|
|
12
|
+
def set_inverse(other, writer, inv_writer)
|
|
13
|
+
other.send(inv_writer, self) if other
|
|
14
|
+
send(writer, other)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Sets a non-collection attribute value in a way which enforces inverse integrity.
|
|
18
|
+
#
|
|
19
|
+
# @param [Object] newval the value to set
|
|
20
|
+
# @param [(Symbol, Symbol)] accessors the reader and writer methods to use in setting the
|
|
21
|
+
# attribute
|
|
22
|
+
# @param [Symbol] inverse_writer the inverse attribute writer method
|
|
23
|
+
# @private
|
|
24
|
+
def set_inversible_noncollection_attribute(newval, accessors, inverse_writer)
|
|
25
|
+
rdr, wtr = accessors
|
|
26
|
+
# the previous value
|
|
27
|
+
oldval = send(rdr)
|
|
28
|
+
# bail if no change
|
|
29
|
+
return newval if newval.equal?(oldval)
|
|
30
|
+
|
|
31
|
+
# clear the previous inverse
|
|
32
|
+
logger.debug { "Moving #{qp} from #{oldval.qp} to #{newval.qp}..." } if oldval and newval
|
|
33
|
+
if oldval then
|
|
34
|
+
clr_wtr = self.class === oldval && oldval.send(rdr).equal?(self) ? wtr : inverse_writer
|
|
35
|
+
oldval.send(clr_wtr, nil)
|
|
36
|
+
end
|
|
37
|
+
# call the writer
|
|
38
|
+
send(wtr, newval)
|
|
39
|
+
# call the inverse writer on self
|
|
40
|
+
if newval then
|
|
41
|
+
newval.send(inverse_writer, self)
|
|
42
|
+
logger.debug { "Moved #{qp} from #{oldval.qp} to #{newval.qp}." } if oldval
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
newval
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Sets a collection attribute value in a way which enforces inverse integrity.
|
|
49
|
+
# The inverse of the attribute is a collection accessed by calling inverse on newval.
|
|
50
|
+
#
|
|
51
|
+
# @param [Resource] newval the new attribute reference value
|
|
52
|
+
# @param [(Symbol, Symbol)] accessors the reader and writer to use in setting
|
|
53
|
+
# the attribute
|
|
54
|
+
# @param [Symbol] inverse the inverse collection attribute to which
|
|
55
|
+
# this domain object will be added
|
|
56
|
+
# @yield a factory to create a new collection on demand (default is an Array)
|
|
57
|
+
# @private
|
|
58
|
+
def add_to_inverse_collection(newval, accessors, inverse)
|
|
59
|
+
rdr, wtr = accessors
|
|
60
|
+
# the current inverse
|
|
61
|
+
oldval = send(rdr)
|
|
62
|
+
# no-op if no change
|
|
63
|
+
return newval if newval == oldval
|
|
64
|
+
|
|
65
|
+
# delete self from the current inverse reference collection
|
|
66
|
+
if oldval then
|
|
67
|
+
coll = oldval.send(inverse)
|
|
68
|
+
coll.delete(self) if coll
|
|
69
|
+
end
|
|
70
|
+
# call the writer on this object
|
|
71
|
+
send(wtr, newval)
|
|
72
|
+
# add self to the inverse collection
|
|
73
|
+
if newval then
|
|
74
|
+
coll = newval.send(inverse)
|
|
75
|
+
if coll.nil? then
|
|
76
|
+
coll = block_given? ? yield : Array.new
|
|
77
|
+
newval.set_property_value(inverse, coll)
|
|
78
|
+
end
|
|
79
|
+
coll << self
|
|
80
|
+
if oldval then
|
|
81
|
+
logger.debug { "Moved #{qp} from #{rdr} #{oldval.qp} #{inverse} to #{newval.qp}." }
|
|
82
|
+
else
|
|
83
|
+
logger.debug { "Added #{qp} to #{rdr} #{newval.qp} #{inverse}." }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
newval
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
require 'jinx/helpers/lazy_hash'
|
|
2
|
+
require 'jinx/resource/reference_visitor'
|
|
3
|
+
|
|
4
|
+
module Jinx
|
|
5
|
+
# A MatchVisitor visits two domain objects' visitable attributes transitive closure in lock-step.
|
|
6
|
+
class MatchVisitor < ReferenceVisitor
|
|
7
|
+
# @return [{Resource => Resource}] the domain object matches
|
|
8
|
+
attr_reader :matches
|
|
9
|
+
|
|
10
|
+
# Creates a new visitor which matches source and target domain object references.
|
|
11
|
+
# The domain attributes to visit are determined by calling the selector block given to
|
|
12
|
+
# this initializer. The selector arguments consist of the match source and target.
|
|
13
|
+
#
|
|
14
|
+
# @param (see ReferenceVisitor#initialize)
|
|
15
|
+
# @option opts [Proc] :mergeable the block which determines which attributes are merged
|
|
16
|
+
# @option opts [Proc] :matchable the block which determines which attributes to match
|
|
17
|
+
# (default is the visit selector)
|
|
18
|
+
# @option opts [:match] :matcher an object which matches sources to targets
|
|
19
|
+
# @option opts [Proc] :copier the block which copies an unmatched source
|
|
20
|
+
# @yield (see ReferenceVisitor#initialize)
|
|
21
|
+
# @yieldparam [Resource] source the matched source object
|
|
22
|
+
def initialize(opts=nil)
|
|
23
|
+
Jinx.fail(ArgumentError, "Reference visitor missing domain reference selector") unless block_given?
|
|
24
|
+
opts = Options.to_hash(opts)
|
|
25
|
+
@matcher = opts.delete(:matcher) || DEF_MATCHER
|
|
26
|
+
@matchable = opts.delete(:matchable)
|
|
27
|
+
@copier = opts.delete(:copier)
|
|
28
|
+
# the source => target matches
|
|
29
|
+
@matches = {}
|
|
30
|
+
# Apply a filter to the selected references so that only a matched reference is visited.
|
|
31
|
+
opts[:filter] = Proc.new { |src| @matches[src] }
|
|
32
|
+
# the class => {id => target} hash
|
|
33
|
+
@id_mtchs = LazyHash.new { Hash.new }
|
|
34
|
+
# Match the source references before navigating from the source to its references, since
|
|
35
|
+
# only a matched reference is visited.
|
|
36
|
+
super do |src|
|
|
37
|
+
tgt = @matches[src]
|
|
38
|
+
attrs = yield(src)
|
|
39
|
+
match_references(src, tgt, attrs)
|
|
40
|
+
attrs
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Visits the source and target.
|
|
45
|
+
#
|
|
46
|
+
# If a block is given to this method, then this method returns the evaluation of the block on the visited
|
|
47
|
+
# source reference and its matching copy, if any. The default return value is the target which matches
|
|
48
|
+
# source.
|
|
49
|
+
#
|
|
50
|
+
# @param [Resource] source the match visit source
|
|
51
|
+
# @param [Resource] target the match visit target
|
|
52
|
+
# @yield [target, source] the optional block to call on the matched source and target
|
|
53
|
+
# @yieldparam [Resource] source the visited source domain object
|
|
54
|
+
# @yieldparam [Resource] target the domain object which matches the visited source
|
|
55
|
+
# @yieldparam [Resource] from the visiting domain object
|
|
56
|
+
# @yieldparam [Property] property the visiting property
|
|
57
|
+
def visit(source, target, &block)
|
|
58
|
+
# clear the match hashes
|
|
59
|
+
@matches.clear
|
|
60
|
+
@id_mtchs.clear
|
|
61
|
+
# seed the matches with the top-level source => target
|
|
62
|
+
add_match(source, target)
|
|
63
|
+
# Visit the source reference.
|
|
64
|
+
super(source) { |src| visit_matched(src, &block) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Matches sources to targets using {Resource#match_all}
|
|
70
|
+
class DefaultMatcher
|
|
71
|
+
def match(sources, targets, from, attribute)
|
|
72
|
+
Resource.match_all(sources, targets)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
DEF_MATCHER = DefaultMatcher.new
|
|
77
|
+
|
|
78
|
+
# Visits the given source domain object.
|
|
79
|
+
#
|
|
80
|
+
# @param [Resource] source the match visit source
|
|
81
|
+
# @yield [target, source] the optional block to call on the matched source and target
|
|
82
|
+
# @yieldparam [Resource] source the visited source domain object
|
|
83
|
+
# @yieldparam [Resource] target the domain object which matches the visited source
|
|
84
|
+
# @yieldparam [Resource] from the visiting domain object
|
|
85
|
+
# @yieldparam [Property] property the visiting property
|
|
86
|
+
def visit_matched(source)
|
|
87
|
+
tgt = @matches[source] || return
|
|
88
|
+
# Match the unvisited matchable references, if any.
|
|
89
|
+
if @matchable then
|
|
90
|
+
mas = @matchable.call(source) - attributes_to_visit(source)
|
|
91
|
+
mas.each { |ma| match_reference(source, tgt, ma) }
|
|
92
|
+
end
|
|
93
|
+
block_given? ? yield(source, tgt) : tgt
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @param source (see #match_visited)
|
|
97
|
+
# @return [<Resource>] the source match
|
|
98
|
+
# @raise [ValidationError] if there is no match
|
|
99
|
+
def match_for_visited(source)
|
|
100
|
+
target = @matches[source]
|
|
101
|
+
if target.nil? then Jinx.fail(ValidationError, "Match visitor target not found for #{source}") end
|
|
102
|
+
target
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @param [Resource] source (see #match_visited)
|
|
106
|
+
# @param [Resource] target the source match
|
|
107
|
+
# @param [<Symbol>] attributes the attributes to match on
|
|
108
|
+
# @return [{Resource => Resource}] the referenced attribute matches
|
|
109
|
+
def match_references(source, target, attributes)
|
|
110
|
+
# collect the references to visit
|
|
111
|
+
matches = {}
|
|
112
|
+
attributes.each do |ma|
|
|
113
|
+
matches.merge!(match_reference(source, target, ma))
|
|
114
|
+
end
|
|
115
|
+
matches
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Matches the given source and target attribute references.
|
|
119
|
+
# The match is performed by this visitor's matcher.
|
|
120
|
+
#
|
|
121
|
+
# @param source (see #visit)
|
|
122
|
+
# @param target (see #visit)
|
|
123
|
+
# @param [Symbol] attribute the parent reference attribute
|
|
124
|
+
# @return [{Resource => Resource}] the referenced source => target matches
|
|
125
|
+
def match_reference(source, target, attribute)
|
|
126
|
+
srcs = source.send(attribute).to_enum
|
|
127
|
+
tgts = target.send(attribute).to_enum
|
|
128
|
+
|
|
129
|
+
# the match targets
|
|
130
|
+
mtchd_tgts = Set.new
|
|
131
|
+
# capture the matched targets and the the unmatched sources
|
|
132
|
+
unmtchd_srcs = srcs.reject do |src|
|
|
133
|
+
# the prior match, if any
|
|
134
|
+
tgt = match_for(src)
|
|
135
|
+
mtchd_tgts << tgt if tgt
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# the unmatched targets
|
|
139
|
+
unmtchd_tgts = tgts.difference(mtchd_tgts)
|
|
140
|
+
logger.debug { "#{qp} matching #{unmtchd_tgts.qp}..." } if @verbose and not unmtchd_tgts.empty?
|
|
141
|
+
# match the residual targets and sources
|
|
142
|
+
rsd_mtchs = @matcher.match(unmtchd_srcs, unmtchd_tgts, source, attribute)
|
|
143
|
+
# add residual matches
|
|
144
|
+
rsd_mtchs.each { |src, tgt| add_match(src, tgt) }
|
|
145
|
+
logger.debug { "#{qp} matched #{rsd_mtchs.qp}..." } if @verbose and not rsd_mtchs.empty?
|
|
146
|
+
|
|
147
|
+
# The source => target match hash.
|
|
148
|
+
# If there is a copier, then copy each unmatched source.
|
|
149
|
+
matches = srcs.to_compact_hash { |src| match_for(src) or copy_unmatched(src) }
|
|
150
|
+
|
|
151
|
+
matches
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# @return the target matching the given source
|
|
155
|
+
def match_for(source)
|
|
156
|
+
@matches[source] or identifier_match(source)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def add_match(source, target)
|
|
160
|
+
@matches[source] = target
|
|
161
|
+
@id_mtchs[source.class][source.identifier] = target if source.identifier
|
|
162
|
+
target
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# @return the target matching the given source on the identifier, if any
|
|
166
|
+
def identifier_match(source)
|
|
167
|
+
tgt = @id_mtchs[source.class][source.identifier] if source.identifier
|
|
168
|
+
@matches[source] = tgt if tgt
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# @return [Resource, nil] a copy of the given source if this ReferenceVisitor has a copier,
|
|
172
|
+
# nil otherwise
|
|
173
|
+
def copy_unmatched(source)
|
|
174
|
+
return unless @copier
|
|
175
|
+
copy = @copier.call(source)
|
|
176
|
+
logger.debug { "#{qp} copied unmatched #{source} to #{copy}." } if @verbose
|
|
177
|
+
add_match(source, copy)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Jinx
|
|
2
|
+
module Resource
|
|
3
|
+
# Matches the given targets to sources using {Resource#match_in}.
|
|
4
|
+
# @private
|
|
5
|
+
class Matcher
|
|
6
|
+
def match(sources, targets)
|
|
7
|
+
unmatched = Set === sources ? sources.dup : sources.to_set
|
|
8
|
+
matches = {}
|
|
9
|
+
targets.each do |tgt|
|
|
10
|
+
src = tgt.match_in(unmatched)
|
|
11
|
+
if src then
|
|
12
|
+
unmatched.delete(src)
|
|
13
|
+
matches[src] = tgt
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
matches
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require 'jinx/resource/match_visitor'
|
|
2
|
+
|
|
3
|
+
module Jinx
|
|
4
|
+
# A MergeVisitor merges a domain object's visitable attributes transitive closure into a target.
|
|
5
|
+
class MergeVisitor < MatchVisitor
|
|
6
|
+
# Creates a new MergeVisitor on domain attributes.
|
|
7
|
+
# The domain attributes to visit are determined by calling the selector block given to
|
|
8
|
+
# this initializer as described in {ReferenceVisitor#initialize}.
|
|
9
|
+
#
|
|
10
|
+
# @param (see MatchVisitor#initialize)
|
|
11
|
+
# @option opts [Proc] :mergeable the block which determines which attributes are merged
|
|
12
|
+
# @option opts [Proc] :matcher the block which matches sources to targets
|
|
13
|
+
# @option opts [Proc] :copier the block which copies an unmatched source
|
|
14
|
+
# @yield (see MatchVisitor#initialize)
|
|
15
|
+
# @yieldparam (see MatchVisitor#initialize)
|
|
16
|
+
def initialize(opts=nil, &selector)
|
|
17
|
+
opts = Options.to_hash(opts)
|
|
18
|
+
# Merge is depth-first, since the source references must be matched, and created if necessary,
|
|
19
|
+
# before they can be merged into the target.
|
|
20
|
+
opts[:depth_first] = true
|
|
21
|
+
@mergeable = opts.delete(:mergeable) || selector
|
|
22
|
+
# each mergeable attribute is matchable
|
|
23
|
+
opts[:matchable] = @mergeable unless @mergeable == selector
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Visits the source and target reference graphs and recursively merges each matching source
|
|
28
|
+
# reference into its corresponding target reference.
|
|
29
|
+
#
|
|
30
|
+
# If a block is given to this method, then the block is called on each matched (source, target) pair.
|
|
31
|
+
#
|
|
32
|
+
# @param [Resource] source the domain object to merge from
|
|
33
|
+
# @param [Resource] target the domain object to merge into
|
|
34
|
+
# @yield [target, source] the optional block to call on the visited source domain object and its
|
|
35
|
+
# matching target
|
|
36
|
+
# @yieldparam [Resource] target the domain object which matches the visited source
|
|
37
|
+
# @yieldparam [Resource] source the visited source domain object
|
|
38
|
+
def visit(source, target)
|
|
39
|
+
super(source, target) do |src, tgt|
|
|
40
|
+
merge(src, tgt)
|
|
41
|
+
block_given? ? yield(src, tgt) : tgt
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# Merges the given source object into the target object.
|
|
48
|
+
#
|
|
49
|
+
# @param [Resource] source the domain object to merge from
|
|
50
|
+
# @param [Resource] target the domain object to merge into
|
|
51
|
+
# @return [Resource] the merged target
|
|
52
|
+
def merge(source, target)
|
|
53
|
+
# trivial case
|
|
54
|
+
return target if source.equal?(target)
|
|
55
|
+
# the domain attributes to merge
|
|
56
|
+
mas = @mergeable.call(source)
|
|
57
|
+
logger.debug { format_merge_log_message(source, target, mas) }
|
|
58
|
+
# merge the non-domain attributes
|
|
59
|
+
target.merge_attributes(source)
|
|
60
|
+
# merge the source domain attributes into the target
|
|
61
|
+
target.merge(source, mas, @matches)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param source (see #merge)
|
|
65
|
+
# @param target (see #merge)
|
|
66
|
+
# @param attributes (see Mergeable#merge)
|
|
67
|
+
# @return [String] the log message
|
|
68
|
+
def format_merge_log_message(source, target, attributes)
|
|
69
|
+
attr_clause = " including domain attributes #{attributes.to_series}" unless attributes.empty?
|
|
70
|
+
"Merging #{source.qp} into #{target.qp}#{attr_clause}..."
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
require 'jinx/helpers/validation'
|
|
2
|
+
|
|
3
|
+
module Jinx
|
|
4
|
+
# A Mergeable supports merging {Resource} attribute values.
|
|
5
|
+
module Mergeable
|
|
6
|
+
# Merges the values of the other attributes into this object and returns self.
|
|
7
|
+
# The other argument can be either a Hash or an object whose class responds to the
|
|
8
|
+
# +mergeable_attributes+ method.
|
|
9
|
+
# The optional attributes argument can be either a single attribute symbol or a
|
|
10
|
+
# collection of attribute symbols.
|
|
11
|
+
#
|
|
12
|
+
# A hash argument consists of attribute name => value associations.
|
|
13
|
+
# For example, given a Mergeable +person+ object with attributes +ssn+ and +children+, the call:
|
|
14
|
+
# person.merge_attributes(:ssn => '555-55-5555', :children => children)
|
|
15
|
+
# is equivalent to:
|
|
16
|
+
# person.ssn ||= '555-55-5555'
|
|
17
|
+
# person.children ||= []
|
|
18
|
+
# person.children.merge(children, :deep)
|
|
19
|
+
# An unrecognized attribute is ignored.
|
|
20
|
+
#
|
|
21
|
+
# If other is not a Hash, then the other object's attributes values are merged into
|
|
22
|
+
# this object. The default attributes is this mergeable's class
|
|
23
|
+
# {Propertied#mergeable_attributes}.
|
|
24
|
+
#
|
|
25
|
+
# The merge is performed by calling {#merge_attribute} on each attribute with the matches
|
|
26
|
+
# and merger block given to this method.
|
|
27
|
+
#
|
|
28
|
+
# @param [Mergeable, {Symbol => Object}] other the source domain object or value hash to merge from
|
|
29
|
+
# @param [<Symbol>, nil] attributes the attributes to merge (default {Propertied#nondomain_attributes})
|
|
30
|
+
# @param [{Resource => Resource}, nil] the optional merge source => target reference matches
|
|
31
|
+
# @yield [attribute, oldval, newval] the optional merger block
|
|
32
|
+
# @yieldparam [Symbol] attribute the merge target attribute
|
|
33
|
+
# @yieldparam oldval the current merge attribute value
|
|
34
|
+
# @yieldparam newval the new merge attribute value
|
|
35
|
+
# @return [Mergeable] self
|
|
36
|
+
# @raise [ArgumentError] if none of the following are true:
|
|
37
|
+
# * other is a Hash
|
|
38
|
+
# * attributes is non-nil
|
|
39
|
+
# * the other class responds to +mergeable_attributes+
|
|
40
|
+
def merge_attributes(other, attributes=nil, matches=nil, &merger)
|
|
41
|
+
return self if other.nil? or other.equal?(self)
|
|
42
|
+
attributes = [attributes] if Symbol === attributes
|
|
43
|
+
attributes ||= self.class.mergeable_attributes
|
|
44
|
+
|
|
45
|
+
# if the source object is not a hash, then convert it to an attribute => value hash
|
|
46
|
+
vh = Hashable === other ? other : other.value_hash(attributes)
|
|
47
|
+
# merge the value hash
|
|
48
|
+
vh.each { |pa, value| merge_attribute(pa, value, matches, &merger) }
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
alias :merge :merge_attributes
|
|
53
|
+
|
|
54
|
+
alias :merge! :merge
|
|
55
|
+
|
|
56
|
+
# Merges the value newval into the attribute as follows:
|
|
57
|
+
# * If the value is nil, empty or equal to the current attribute value, then no merge
|
|
58
|
+
# is performed.
|
|
59
|
+
# * Otherwise, if a merger block is given to this method, then that block is called
|
|
60
|
+
# to perform the merge.
|
|
61
|
+
# * Otherwise, if the attribute is a non-domain attribute and the current value is non-nil,
|
|
62
|
+
# then no merge is performed.
|
|
63
|
+
# * Otherwise, if the attribute is a non-domain attribute and the current value is nil,
|
|
64
|
+
# then set the attribute to the newval.
|
|
65
|
+
# * Otherwise, if the attribute is a domain non-collection attribute, then newval is recursively
|
|
66
|
+
# merged into the current referenced domain object.
|
|
67
|
+
# * Otherwise, attribute is a domain collection attribute and matching newval members are
|
|
68
|
+
# merged into the corresponding current collection members and non-matching newval members
|
|
69
|
+
# are added to the current collection.
|
|
70
|
+
#
|
|
71
|
+
# @param [Symbol] attribute the merge attribute
|
|
72
|
+
# @param newval the value to merge
|
|
73
|
+
# @param [{Resource => Resource}, nil] the optional merge source => target reference matches
|
|
74
|
+
# @yield (see #merge_attributes)
|
|
75
|
+
# @yieldparam (see #merge_attributes)
|
|
76
|
+
# @return the merged attribute value
|
|
77
|
+
def merge_attribute(attribute, newval, matches=nil)
|
|
78
|
+
# the previous value
|
|
79
|
+
oldval = send(attribute)
|
|
80
|
+
# If nothing to merge or a block can take over, then bail.
|
|
81
|
+
if newval.nil? or mergeable__equal?(oldval, newval) then
|
|
82
|
+
return oldval
|
|
83
|
+
elsif block_given? then
|
|
84
|
+
return yield(attribute, oldval, value)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Discriminate between a domain and non-domain attribute.
|
|
88
|
+
prop = self.class.property(attribute)
|
|
89
|
+
if prop.domain? then
|
|
90
|
+
merge_domain_attribute_value(prop, oldval, newval, matches)
|
|
91
|
+
else
|
|
92
|
+
merge_nondomain_attribute_value(prop, oldval, newval)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# @see #merge_attribute
|
|
99
|
+
def merge_nondomain_attribute_value(prop, oldval, newval)
|
|
100
|
+
if oldval.nil? then
|
|
101
|
+
send(prop.writer, newval)
|
|
102
|
+
elsif prop.collection? then
|
|
103
|
+
oldval.merge(newval)
|
|
104
|
+
else
|
|
105
|
+
oldval
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @see #merge_attribute
|
|
110
|
+
def merge_domain_attribute_value(prop, oldval, newval, matches)
|
|
111
|
+
# the dependent owner writer method, if any
|
|
112
|
+
if prop.dependent? then
|
|
113
|
+
val = prop.collection? ? newval.first : newval
|
|
114
|
+
klass = val.class if val
|
|
115
|
+
inv_prop = self.class.inverse_property(prop, klass)
|
|
116
|
+
if inv_prop and not inv_prop.collection? then
|
|
117
|
+
owtr = inv_prop.writer
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# If the attribute is a collection, then merge the matches into the current attribute
|
|
122
|
+
# collection value and add each unmatched source to the collection.
|
|
123
|
+
# Otherwise, if the attribute is not yet set and there is a new value, then set it
|
|
124
|
+
# to the new value match or the new value itself if unmatched.
|
|
125
|
+
if prop.collection? then
|
|
126
|
+
# TODO - refactor into method
|
|
127
|
+
if oldval.nil? then
|
|
128
|
+
Jinx.fail(ValidationError, "Merge into #{qp} #{prop} with nil collection value is not supported")
|
|
129
|
+
end
|
|
130
|
+
# the references to add
|
|
131
|
+
adds = []
|
|
132
|
+
logger.debug { "Merging #{newval.qp} into #{qp} #{prop} #{oldval.qp}..." } unless newval.nil_or_empty?
|
|
133
|
+
newval.enumerate do |src|
|
|
134
|
+
# If the match target is in the current collection, then update the matched
|
|
135
|
+
# target from the source.
|
|
136
|
+
# Otherwise, if there is no match or the match is a new reference created
|
|
137
|
+
# from the match, then add the match to the oldval collection.
|
|
138
|
+
if matches && matches.has_key?(src) then
|
|
139
|
+
# the source match
|
|
140
|
+
tgt = matches[src]
|
|
141
|
+
if tgt then
|
|
142
|
+
if oldval.include?(tgt) then
|
|
143
|
+
tgt.merge_attributes(src)
|
|
144
|
+
else
|
|
145
|
+
adds << tgt
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
else
|
|
149
|
+
adds << src
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
# add the unmatched sources
|
|
153
|
+
logger.debug { "Adding #{qp} #{prop} unmatched #{adds.qp}..." } unless adds.empty?
|
|
154
|
+
adds.each do |ref|
|
|
155
|
+
# If there is an owner writer attribute, then add the ref to the attribute collection by
|
|
156
|
+
# delegating to the owner writer. Otherwise, add the ref to the attribute collection directly.
|
|
157
|
+
owtr ? delegate_to_inverse_setter(prop, ref, owtr) : oldval << ref
|
|
158
|
+
end
|
|
159
|
+
oldval
|
|
160
|
+
elsif newval.nil? then
|
|
161
|
+
# no merge source
|
|
162
|
+
oldval
|
|
163
|
+
elsif oldval then
|
|
164
|
+
# merge the source into the target
|
|
165
|
+
oldval.merge(newval)
|
|
166
|
+
else
|
|
167
|
+
# No target; set the attribute to the source.
|
|
168
|
+
# The target is either a source match or the source itself.
|
|
169
|
+
ref = (matches[newval] if matches) || newval
|
|
170
|
+
logger.debug { "Setting #{qp} #{prop} reference #{ref.qp}..." }
|
|
171
|
+
# If the target is a dependent, then set the dependent owner, which will in turn
|
|
172
|
+
# set the attribute to the dependent. Otherwise, set the attribute to the target.
|
|
173
|
+
owtr ? delegate_to_inverse_setter(prop, ref, owtr) : send(prop.writer, ref)
|
|
174
|
+
end
|
|
175
|
+
newval
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# @quirk Java Java TreeSet comparison uses the TreeSet comparator rather than an
|
|
179
|
+
# element-wise comparator. Work around this rare aberration by converting the TreeSet
|
|
180
|
+
# to a Ruby Set.
|
|
181
|
+
def mergeable__equal?(v1, v2)
|
|
182
|
+
Java::JavaUtil::TreeSet === v1 && Java::JavaUtil::TreeSet === v2 ? v1.to_set == v2.to_set : v1 == v2
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|