jinx 2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|