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.
Files changed (149) hide show
  1. data/.gitignore +14 -0
  2. data/.rspec +3 -0
  3. data/.yardopts +1 -0
  4. data/Gemfile +6 -0
  5. data/Gemfile.lock +27 -0
  6. data/History.md +6 -0
  7. data/LEGAL +5 -0
  8. data/LICENSE +22 -0
  9. data/README.md +44 -0
  10. data/Rakefile +41 -0
  11. data/examples/family/README.md +10 -0
  12. data/examples/family/ext/build.xml +35 -0
  13. data/examples/family/ext/src/family/Address.java +68 -0
  14. data/examples/family/ext/src/family/Child.java +24 -0
  15. data/examples/family/ext/src/family/DomainObject.java +26 -0
  16. data/examples/family/ext/src/family/Household.java +36 -0
  17. data/examples/family/ext/src/family/Parent.java +48 -0
  18. data/examples/family/ext/src/family/Person.java +42 -0
  19. data/examples/family/lib/family.rb +15 -0
  20. data/examples/family/lib/family/address.rb +6 -0
  21. data/examples/family/lib/family/domain_object.rb +6 -0
  22. data/examples/family/lib/family/household.rb +6 -0
  23. data/examples/family/lib/family/parent.rb +16 -0
  24. data/examples/family/lib/family/person.rb +6 -0
  25. data/examples/model/README.md +25 -0
  26. data/examples/model/ext/build.xml +35 -0
  27. data/examples/model/ext/src/domain/Child.java +192 -0
  28. data/examples/model/ext/src/domain/Dependent.java +29 -0
  29. data/examples/model/ext/src/domain/DomainObject.java +26 -0
  30. data/examples/model/ext/src/domain/Independent.java +83 -0
  31. data/examples/model/ext/src/domain/Parent.java +129 -0
  32. data/examples/model/ext/src/domain/Person.java +14 -0
  33. data/examples/model/lib/model.rb +13 -0
  34. data/examples/model/lib/model/child.rb +13 -0
  35. data/examples/model/lib/model/domain_object.rb +6 -0
  36. data/examples/model/lib/model/independent.rb +11 -0
  37. data/examples/model/lib/model/parent.rb +17 -0
  38. data/jinx.gemspec +22 -0
  39. data/lib/jinx.rb +3 -0
  40. data/lib/jinx/active_support/README.txt +2 -0
  41. data/lib/jinx/active_support/core_ext/string.rb +7 -0
  42. data/lib/jinx/active_support/core_ext/string/inflections.rb +167 -0
  43. data/lib/jinx/active_support/inflections.rb +55 -0
  44. data/lib/jinx/active_support/inflector.rb +398 -0
  45. data/lib/jinx/cli/application.rb +36 -0
  46. data/lib/jinx/cli/command.rb +214 -0
  47. data/lib/jinx/helpers/array.rb +108 -0
  48. data/lib/jinx/helpers/boolean.rb +42 -0
  49. data/lib/jinx/helpers/case_insensitive_hash.rb +39 -0
  50. data/lib/jinx/helpers/class.rb +149 -0
  51. data/lib/jinx/helpers/collection.rb +33 -0
  52. data/lib/jinx/helpers/collections.rb +11 -0
  53. data/lib/jinx/helpers/collector.rb +20 -0
  54. data/lib/jinx/helpers/conditional_enumerator.rb +21 -0
  55. data/lib/jinx/helpers/enumerable.rb +242 -0
  56. data/lib/jinx/helpers/enumerate.rb +35 -0
  57. data/lib/jinx/helpers/error.rb +15 -0
  58. data/lib/jinx/helpers/file_separator.rb +65 -0
  59. data/lib/jinx/helpers/filter.rb +52 -0
  60. data/lib/jinx/helpers/flattener.rb +38 -0
  61. data/lib/jinx/helpers/hash.rb +12 -0
  62. data/lib/jinx/helpers/hashable.rb +502 -0
  63. data/lib/jinx/helpers/inflector.rb +36 -0
  64. data/lib/jinx/helpers/key_transformer_hash.rb +43 -0
  65. data/lib/jinx/helpers/lazy_hash.rb +44 -0
  66. data/lib/jinx/helpers/log.rb +106 -0
  67. data/lib/jinx/helpers/math.rb +12 -0
  68. data/lib/jinx/helpers/merge.rb +60 -0
  69. data/lib/jinx/helpers/module.rb +18 -0
  70. data/lib/jinx/helpers/multi_enumerator.rb +31 -0
  71. data/lib/jinx/helpers/options.rb +92 -0
  72. data/lib/jinx/helpers/os.rb +19 -0
  73. data/lib/jinx/helpers/partial_order.rb +37 -0
  74. data/lib/jinx/helpers/pretty_print.rb +207 -0
  75. data/lib/jinx/helpers/set.rb +8 -0
  76. data/lib/jinx/helpers/stopwatch.rb +76 -0
  77. data/lib/jinx/helpers/transformer.rb +24 -0
  78. data/lib/jinx/helpers/transitive_closure.rb +55 -0
  79. data/lib/jinx/helpers/uniquifier.rb +50 -0
  80. data/lib/jinx/helpers/validation.rb +33 -0
  81. data/lib/jinx/helpers/visitor.rb +370 -0
  82. data/lib/jinx/import/class_path_modifier.rb +77 -0
  83. data/lib/jinx/import/java.rb +337 -0
  84. data/lib/jinx/importer.rb +240 -0
  85. data/lib/jinx/metadata.rb +155 -0
  86. data/lib/jinx/metadata/attribute_enumerator.rb +73 -0
  87. data/lib/jinx/metadata/dependency.rb +244 -0
  88. data/lib/jinx/metadata/id_alias.rb +23 -0
  89. data/lib/jinx/metadata/introspector.rb +179 -0
  90. data/lib/jinx/metadata/inverse.rb +170 -0
  91. data/lib/jinx/metadata/java_property.rb +169 -0
  92. data/lib/jinx/metadata/propertied.rb +500 -0
  93. data/lib/jinx/metadata/property.rb +401 -0
  94. data/lib/jinx/metadata/property_characteristics.rb +114 -0
  95. data/lib/jinx/resource.rb +862 -0
  96. data/lib/jinx/resource/copy_visitor.rb +36 -0
  97. data/lib/jinx/resource/inversible.rb +90 -0
  98. data/lib/jinx/resource/match_visitor.rb +180 -0
  99. data/lib/jinx/resource/matcher.rb +20 -0
  100. data/lib/jinx/resource/merge_visitor.rb +73 -0
  101. data/lib/jinx/resource/mergeable.rb +185 -0
  102. data/lib/jinx/resource/reference_enumerator.rb +49 -0
  103. data/lib/jinx/resource/reference_path_visitor.rb +38 -0
  104. data/lib/jinx/resource/reference_visitor.rb +55 -0
  105. data/lib/jinx/resource/unique.rb +35 -0
  106. data/lib/jinx/version.rb +3 -0
  107. data/spec/defaults_spec.rb +30 -0
  108. data/spec/definitions/model/alias/child.rb +5 -0
  109. data/spec/definitions/model/base/child.rb +5 -0
  110. data/spec/definitions/model/base/domain_object.rb +5 -0
  111. data/spec/definitions/model/base/independent.rb +5 -0
  112. data/spec/definitions/model/defaults/child.rb +5 -0
  113. data/spec/definitions/model/dependency/child.rb +5 -0
  114. data/spec/definitions/model/dependency/parent.rb +6 -0
  115. data/spec/definitions/model/inverse/child.rb +5 -0
  116. data/spec/definitions/model/inverse/independent.rb +5 -0
  117. data/spec/definitions/model/inverse/parent.rb +5 -0
  118. data/spec/definitions/model/mandatory/child.rb +6 -0
  119. data/spec/dependency_spec.rb +47 -0
  120. data/spec/family_spec.rb +64 -0
  121. data/spec/inverse_spec.rb +53 -0
  122. data/spec/mandatory_spec.rb +43 -0
  123. data/spec/metadata_spec.rb +68 -0
  124. data/spec/resource_spec.rb +30 -0
  125. data/spec/spec_helper.rb +3 -0
  126. data/spec/support/model.rb +19 -0
  127. data/test/fixtures/line_separator/cr_line_sep.txt +1 -0
  128. data/test/fixtures/line_separator/crlf_line_sep.txt +3 -0
  129. data/test/fixtures/line_separator/lf_line_sep.txt +3 -0
  130. data/test/fixtures/mixed/ext/build.xml +35 -0
  131. data/test/fixtures/mixed/ext/src/mixed/Case/Example.java +5 -0
  132. data/test/helper.rb +7 -0
  133. data/test/lib/jinx/command_test.rb +41 -0
  134. data/test/lib/jinx/helpers/boolean_test.rb +27 -0
  135. data/test/lib/jinx/helpers/class_test.rb +60 -0
  136. data/test/lib/jinx/helpers/collections_test.rb +402 -0
  137. data/test/lib/jinx/helpers/file_separator_test.rb +29 -0
  138. data/test/lib/jinx/helpers/inflector_test.rb +11 -0
  139. data/test/lib/jinx/helpers/lazy_hash_test.rb +32 -0
  140. data/test/lib/jinx/helpers/module_test.rb +24 -0
  141. data/test/lib/jinx/helpers/options_test.rb +66 -0
  142. data/test/lib/jinx/helpers/partial_order_test.rb +41 -0
  143. data/test/lib/jinx/helpers/pretty_print_test.rb +83 -0
  144. data/test/lib/jinx/helpers/stopwatch_test.rb +16 -0
  145. data/test/lib/jinx/helpers/transitive_closure_test.rb +80 -0
  146. data/test/lib/jinx/helpers/visitor_test.rb +288 -0
  147. data/test/lib/jinx/import/java_test.rb +78 -0
  148. data/test/lib/jinx/import/mixed_case_test.rb +16 -0
  149. 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