jinx 2.1.1

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