sbf-dm-core 1.3.0.beta

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 (259) hide show
  1. checksums.yaml +7 -0
  2. data/.autotest +29 -0
  3. data/.document +5 -0
  4. data/.gitignore +44 -0
  5. data/.rspec +1 -0
  6. data/.rubocop.yml +468 -0
  7. data/.travis.yml +57 -0
  8. data/.yardopts +1 -0
  9. data/Gemfile +70 -0
  10. data/LICENSE +20 -0
  11. data/README.md +269 -0
  12. data/Rakefile +4 -0
  13. data/dm-core.gemspec +21 -0
  14. data/lib/dm-core/adapters/abstract_adapter.rb +233 -0
  15. data/lib/dm-core/adapters/in_memory_adapter.rb +110 -0
  16. data/lib/dm-core/adapters.rb +249 -0
  17. data/lib/dm-core/associations/many_to_many.rb +477 -0
  18. data/lib/dm-core/associations/many_to_one.rb +282 -0
  19. data/lib/dm-core/associations/one_to_many.rb +332 -0
  20. data/lib/dm-core/associations/one_to_one.rb +84 -0
  21. data/lib/dm-core/associations/relationship.rb +650 -0
  22. data/lib/dm-core/backwards.rb +11 -0
  23. data/lib/dm-core/collection.rb +1486 -0
  24. data/lib/dm-core/core_ext/kernel.rb +21 -0
  25. data/lib/dm-core/core_ext/pathname.rb +4 -0
  26. data/lib/dm-core/core_ext/symbol.rb +10 -0
  27. data/lib/dm-core/identity_map.rb +6 -0
  28. data/lib/dm-core/model/hook.rb +99 -0
  29. data/lib/dm-core/model/is.rb +30 -0
  30. data/lib/dm-core/model/property.rb +244 -0
  31. data/lib/dm-core/model/relationship.rb +366 -0
  32. data/lib/dm-core/model/scope.rb +87 -0
  33. data/lib/dm-core/model.rb +876 -0
  34. data/lib/dm-core/property/binary.rb +19 -0
  35. data/lib/dm-core/property/boolean.rb +35 -0
  36. data/lib/dm-core/property/class.rb +23 -0
  37. data/lib/dm-core/property/date.rb +45 -0
  38. data/lib/dm-core/property/date_time.rb +44 -0
  39. data/lib/dm-core/property/decimal.rb +47 -0
  40. data/lib/dm-core/property/discriminator.rb +40 -0
  41. data/lib/dm-core/property/float.rb +27 -0
  42. data/lib/dm-core/property/integer.rb +32 -0
  43. data/lib/dm-core/property/invalid_value_error.rb +17 -0
  44. data/lib/dm-core/property/lookup.rb +26 -0
  45. data/lib/dm-core/property/numeric.rb +35 -0
  46. data/lib/dm-core/property/object.rb +33 -0
  47. data/lib/dm-core/property/serial.rb +13 -0
  48. data/lib/dm-core/property/string.rb +47 -0
  49. data/lib/dm-core/property/text.rb +12 -0
  50. data/lib/dm-core/property/time.rb +46 -0
  51. data/lib/dm-core/property/typecast/numeric.rb +32 -0
  52. data/lib/dm-core/property/typecast/time.rb +33 -0
  53. data/lib/dm-core/property.rb +856 -0
  54. data/lib/dm-core/property_set.rb +177 -0
  55. data/lib/dm-core/query/conditions/comparison.rb +886 -0
  56. data/lib/dm-core/query/conditions/operation.rb +710 -0
  57. data/lib/dm-core/query/direction.rb +33 -0
  58. data/lib/dm-core/query/operator.rb +34 -0
  59. data/lib/dm-core/query/path.rb +113 -0
  60. data/lib/dm-core/query/sort.rb +38 -0
  61. data/lib/dm-core/query.rb +1352 -0
  62. data/lib/dm-core/relationship_set.rb +69 -0
  63. data/lib/dm-core/repository.rb +226 -0
  64. data/lib/dm-core/resource/persistence_state/clean.rb +36 -0
  65. data/lib/dm-core/resource/persistence_state/deleted.rb +26 -0
  66. data/lib/dm-core/resource/persistence_state/dirty.rb +91 -0
  67. data/lib/dm-core/resource/persistence_state/immutable.rb +32 -0
  68. data/lib/dm-core/resource/persistence_state/persisted.rb +25 -0
  69. data/lib/dm-core/resource/persistence_state/transient.rb +87 -0
  70. data/lib/dm-core/resource/persistence_state.rb +70 -0
  71. data/lib/dm-core/resource.rb +1220 -0
  72. data/lib/dm-core/spec/lib/adapter_helpers.rb +63 -0
  73. data/lib/dm-core/spec/lib/collection_helpers.rb +21 -0
  74. data/lib/dm-core/spec/lib/counter_adapter.rb +38 -0
  75. data/lib/dm-core/spec/lib/pending_helpers.rb +50 -0
  76. data/lib/dm-core/spec/lib/spec_helper.rb +74 -0
  77. data/lib/dm-core/spec/setup.rb +164 -0
  78. data/lib/dm-core/spec/shared/adapter_spec.rb +366 -0
  79. data/lib/dm-core/spec/shared/public/property_spec.rb +229 -0
  80. data/lib/dm-core/spec/shared/resource_spec.rb +1221 -0
  81. data/lib/dm-core/spec/shared/sel_spec.rb +111 -0
  82. data/lib/dm-core/spec/shared/semipublic/property_spec.rb +184 -0
  83. data/lib/dm-core/spec/shared/semipublic/query/conditions/abstract_comparison_spec.rb +261 -0
  84. data/lib/dm-core/support/assertions.rb +8 -0
  85. data/lib/dm-core/support/chainable.rb +18 -0
  86. data/lib/dm-core/support/deprecate.rb +12 -0
  87. data/lib/dm-core/support/descendant_set.rb +89 -0
  88. data/lib/dm-core/support/equalizer.rb +48 -0
  89. data/lib/dm-core/support/ext/array.rb +22 -0
  90. data/lib/dm-core/support/ext/blank.rb +25 -0
  91. data/lib/dm-core/support/ext/hash.rb +67 -0
  92. data/lib/dm-core/support/ext/module.rb +47 -0
  93. data/lib/dm-core/support/ext/object.rb +57 -0
  94. data/lib/dm-core/support/ext/string.rb +24 -0
  95. data/lib/dm-core/support/ext/try_dup.rb +12 -0
  96. data/lib/dm-core/support/hook.rb +388 -0
  97. data/lib/dm-core/support/inflections.rb +60 -0
  98. data/lib/dm-core/support/inflector/inflections.rb +211 -0
  99. data/lib/dm-core/support/inflector/methods.rb +151 -0
  100. data/lib/dm-core/support/lazy_array.rb +451 -0
  101. data/lib/dm-core/support/local_object_space.rb +13 -0
  102. data/lib/dm-core/support/logger.rb +201 -0
  103. data/lib/dm-core/support/mash.rb +176 -0
  104. data/lib/dm-core/support/naming_conventions.rb +109 -0
  105. data/lib/dm-core/support/ordered_set.rb +381 -0
  106. data/lib/dm-core/support/subject.rb +33 -0
  107. data/lib/dm-core/support/subject_set.rb +251 -0
  108. data/lib/dm-core/version.rb +3 -0
  109. data/lib/dm-core.rb +274 -0
  110. data/script/performance.rb +275 -0
  111. data/script/profile.rb +218 -0
  112. data/spec/lib/rspec_immediate_feedback_formatter.rb +54 -0
  113. data/spec/public/associations/many_to_many/read_multiple_join_spec.rb +69 -0
  114. data/spec/public/associations/many_to_many_spec.rb +197 -0
  115. data/spec/public/associations/many_to_one_spec.rb +83 -0
  116. data/spec/public/associations/many_to_one_with_boolean_cpk_spec.rb +40 -0
  117. data/spec/public/associations/many_to_one_with_custom_fk_spec.rb +49 -0
  118. data/spec/public/associations/one_to_many_spec.rb +81 -0
  119. data/spec/public/associations/one_to_one_spec.rb +176 -0
  120. data/spec/public/associations/one_to_one_with_boolean_cpk_spec.rb +46 -0
  121. data/spec/public/collection_spec.rb +69 -0
  122. data/spec/public/finalize_spec.rb +77 -0
  123. data/spec/public/model/hook_spec.rb +245 -0
  124. data/spec/public/model/property_spec.rb +91 -0
  125. data/spec/public/model/relationship_spec.rb +1040 -0
  126. data/spec/public/model_spec.rb +456 -0
  127. data/spec/public/property/binary_spec.rb +43 -0
  128. data/spec/public/property/boolean_spec.rb +21 -0
  129. data/spec/public/property/class_spec.rb +27 -0
  130. data/spec/public/property/date_spec.rb +21 -0
  131. data/spec/public/property/date_time_spec.rb +21 -0
  132. data/spec/public/property/decimal_spec.rb +23 -0
  133. data/spec/public/property/discriminator_spec.rb +134 -0
  134. data/spec/public/property/float_spec.rb +22 -0
  135. data/spec/public/property/integer_spec.rb +22 -0
  136. data/spec/public/property/object_spec.rb +117 -0
  137. data/spec/public/property/serial_spec.rb +22 -0
  138. data/spec/public/property/string_spec.rb +21 -0
  139. data/spec/public/property/text_spec.rb +62 -0
  140. data/spec/public/property/time_spec.rb +21 -0
  141. data/spec/public/property_spec.rb +333 -0
  142. data/spec/public/resource/state_spec.rb +72 -0
  143. data/spec/public/resource_spec.rb +289 -0
  144. data/spec/public/sel_spec.rb +53 -0
  145. data/spec/public/setup_spec.rb +145 -0
  146. data/spec/public/shared/association_collection_shared_spec.rb +309 -0
  147. data/spec/public/shared/collection_finder_shared_spec.rb +267 -0
  148. data/spec/public/shared/collection_shared_spec.rb +1637 -0
  149. data/spec/public/shared/finder_shared_spec.rb +1647 -0
  150. data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
  151. data/spec/semipublic/adapters/in_memory_adapter_spec.rb +13 -0
  152. data/spec/semipublic/associations/many_to_many_spec.rb +94 -0
  153. data/spec/semipublic/associations/many_to_one_spec.rb +63 -0
  154. data/spec/semipublic/associations/one_to_many_spec.rb +55 -0
  155. data/spec/semipublic/associations/one_to_one_spec.rb +53 -0
  156. data/spec/semipublic/associations/relationship_spec.rb +200 -0
  157. data/spec/semipublic/associations_spec.rb +177 -0
  158. data/spec/semipublic/collection_spec.rb +110 -0
  159. data/spec/semipublic/model_spec.rb +96 -0
  160. data/spec/semipublic/property/binary_spec.rb +13 -0
  161. data/spec/semipublic/property/boolean_spec.rb +47 -0
  162. data/spec/semipublic/property/class_spec.rb +33 -0
  163. data/spec/semipublic/property/date_spec.rb +43 -0
  164. data/spec/semipublic/property/date_time_spec.rb +46 -0
  165. data/spec/semipublic/property/decimal_spec.rb +83 -0
  166. data/spec/semipublic/property/discriminator_spec.rb +19 -0
  167. data/spec/semipublic/property/float_spec.rb +82 -0
  168. data/spec/semipublic/property/integer_spec.rb +82 -0
  169. data/spec/semipublic/property/lookup_spec.rb +29 -0
  170. data/spec/semipublic/property/serial_spec.rb +13 -0
  171. data/spec/semipublic/property/string_spec.rb +13 -0
  172. data/spec/semipublic/property/text_spec.rb +31 -0
  173. data/spec/semipublic/property/time_spec.rb +50 -0
  174. data/spec/semipublic/property_spec.rb +114 -0
  175. data/spec/semipublic/query/conditions/comparison_spec.rb +1502 -0
  176. data/spec/semipublic/query/conditions/operation_spec.rb +1296 -0
  177. data/spec/semipublic/query/path_spec.rb +471 -0
  178. data/spec/semipublic/query_spec.rb +3665 -0
  179. data/spec/semipublic/resource/state/clean_spec.rb +89 -0
  180. data/spec/semipublic/resource/state/deleted_spec.rb +79 -0
  181. data/spec/semipublic/resource/state/dirty_spec.rb +163 -0
  182. data/spec/semipublic/resource/state/immutable_spec.rb +107 -0
  183. data/spec/semipublic/resource/state/transient_spec.rb +163 -0
  184. data/spec/semipublic/resource/state_spec.rb +230 -0
  185. data/spec/semipublic/resource_spec.rb +23 -0
  186. data/spec/semipublic/shared/condition_shared_spec.rb +9 -0
  187. data/spec/semipublic/shared/resource_shared_spec.rb +198 -0
  188. data/spec/semipublic/shared/resource_state_shared_spec.rb +91 -0
  189. data/spec/semipublic/shared/subject_shared_spec.rb +79 -0
  190. data/spec/spec_helper.rb +34 -0
  191. data/spec/support/core_ext/hash.rb +10 -0
  192. data/spec/support/core_ext/inheritable_attributes.rb +46 -0
  193. data/spec/support/properties/huge_integer.rb +17 -0
  194. data/spec/unit/array_spec.rb +23 -0
  195. data/spec/unit/blank_spec.rb +73 -0
  196. data/spec/unit/data_mapper/ordered_set/append_spec.rb +26 -0
  197. data/spec/unit/data_mapper/ordered_set/clear_spec.rb +24 -0
  198. data/spec/unit/data_mapper/ordered_set/delete_spec.rb +28 -0
  199. data/spec/unit/data_mapper/ordered_set/each_spec.rb +19 -0
  200. data/spec/unit/data_mapper/ordered_set/empty_spec.rb +20 -0
  201. data/spec/unit/data_mapper/ordered_set/entries_spec.rb +22 -0
  202. data/spec/unit/data_mapper/ordered_set/eql_spec.rb +51 -0
  203. data/spec/unit/data_mapper/ordered_set/equal_value_spec.rb +84 -0
  204. data/spec/unit/data_mapper/ordered_set/hash_spec.rb +12 -0
  205. data/spec/unit/data_mapper/ordered_set/include_spec.rb +23 -0
  206. data/spec/unit/data_mapper/ordered_set/index_spec.rb +28 -0
  207. data/spec/unit/data_mapper/ordered_set/initialize_spec.rb +32 -0
  208. data/spec/unit/data_mapper/ordered_set/merge_spec.rb +36 -0
  209. data/spec/unit/data_mapper/ordered_set/shared/append_spec.rb +24 -0
  210. data/spec/unit/data_mapper/ordered_set/shared/clear_spec.rb +9 -0
  211. data/spec/unit/data_mapper/ordered_set/shared/delete_spec.rb +25 -0
  212. data/spec/unit/data_mapper/ordered_set/shared/each_spec.rb +17 -0
  213. data/spec/unit/data_mapper/ordered_set/shared/empty_spec.rb +9 -0
  214. data/spec/unit/data_mapper/ordered_set/shared/entries_spec.rb +9 -0
  215. data/spec/unit/data_mapper/ordered_set/shared/include_spec.rb +9 -0
  216. data/spec/unit/data_mapper/ordered_set/shared/index_spec.rb +13 -0
  217. data/spec/unit/data_mapper/ordered_set/shared/initialize_spec.rb +28 -0
  218. data/spec/unit/data_mapper/ordered_set/shared/merge_spec.rb +28 -0
  219. data/spec/unit/data_mapper/ordered_set/shared/size_spec.rb +13 -0
  220. data/spec/unit/data_mapper/ordered_set/shared/to_ary_spec.rb +11 -0
  221. data/spec/unit/data_mapper/ordered_set/size_spec.rb +27 -0
  222. data/spec/unit/data_mapper/ordered_set/to_ary_spec.rb +23 -0
  223. data/spec/unit/data_mapper/subject_set/append_spec.rb +47 -0
  224. data/spec/unit/data_mapper/subject_set/clear_spec.rb +34 -0
  225. data/spec/unit/data_mapper/subject_set/delete_spec.rb +40 -0
  226. data/spec/unit/data_mapper/subject_set/each_spec.rb +30 -0
  227. data/spec/unit/data_mapper/subject_set/empty_spec.rb +31 -0
  228. data/spec/unit/data_mapper/subject_set/entries_spec.rb +31 -0
  229. data/spec/unit/data_mapper/subject_set/get_spec.rb +34 -0
  230. data/spec/unit/data_mapper/subject_set/include_spec.rb +32 -0
  231. data/spec/unit/data_mapper/subject_set/named_spec.rb +33 -0
  232. data/spec/unit/data_mapper/subject_set/shared/append_spec.rb +18 -0
  233. data/spec/unit/data_mapper/subject_set/shared/clear_spec.rb +9 -0
  234. data/spec/unit/data_mapper/subject_set/shared/delete_spec.rb +9 -0
  235. data/spec/unit/data_mapper/subject_set/shared/each_spec.rb +9 -0
  236. data/spec/unit/data_mapper/subject_set/shared/empty_spec.rb +9 -0
  237. data/spec/unit/data_mapper/subject_set/shared/entries_spec.rb +9 -0
  238. data/spec/unit/data_mapper/subject_set/shared/get_spec.rb +9 -0
  239. data/spec/unit/data_mapper/subject_set/shared/include_spec.rb +9 -0
  240. data/spec/unit/data_mapper/subject_set/shared/named_spec.rb +9 -0
  241. data/spec/unit/data_mapper/subject_set/shared/size_spec.rb +13 -0
  242. data/spec/unit/data_mapper/subject_set/shared/to_ary_spec.rb +9 -0
  243. data/spec/unit/data_mapper/subject_set/shared/values_at_spec.rb +44 -0
  244. data/spec/unit/data_mapper/subject_set/size_spec.rb +42 -0
  245. data/spec/unit/data_mapper/subject_set/to_ary_spec.rb +34 -0
  246. data/spec/unit/data_mapper/subject_set/values_at_spec.rb +57 -0
  247. data/spec/unit/hash_spec.rb +27 -0
  248. data/spec/unit/hook_spec.rb +1216 -0
  249. data/spec/unit/inflections_spec.rb +14 -0
  250. data/spec/unit/lazy_array_spec.rb +1949 -0
  251. data/spec/unit/mash_spec.rb +289 -0
  252. data/spec/unit/module_spec.rb +70 -0
  253. data/spec/unit/object_spec.rb +38 -0
  254. data/spec/unit/try_dup_spec.rb +46 -0
  255. data/tasks/ci.rake +1 -0
  256. data/tasks/spec.rake +18 -0
  257. data/tasks/yard.rake +9 -0
  258. data/tasks/yardstick.rake +19 -0
  259. metadata +323 -0
@@ -0,0 +1,477 @@
1
+ module DataMapper
2
+ module Associations
3
+ module ManyToMany # :nodoc:
4
+ class Relationship < Associations::OneToMany::Relationship
5
+ extend Chainable
6
+
7
+ OPTIONS = superclass::OPTIONS.dup << :through << :via
8
+
9
+ # Returns a set of keys that identify the target model
10
+ #
11
+ # @return [DataMapper::PropertySet]
12
+ # a set of properties that identify the target model
13
+ #
14
+ # @api semipublic
15
+ def child_key
16
+ return @child_key if defined?(@child_key)
17
+
18
+ repository_name = child_repository_name || parent_repository_name
19
+ properties = child_model&.properties(repository_name)
20
+
21
+ @child_key = if @child_properties
22
+ child_key = properties&.values_at(*@child_properties)
23
+ properties.class.new(child_key).freeze
24
+ else
25
+ properties&.key
26
+ end
27
+ end
28
+
29
+ # @api semipublic
30
+ alias_method :target_key, :child_key
31
+
32
+ # Intermediate association for through model
33
+ # relationships
34
+ #
35
+ # Example: for :bugs association in
36
+ #
37
+ # class Software::Engineer
38
+ # include DataMapper::Resource
39
+ #
40
+ # has n, :missing_tests
41
+ # has n, :bugs, :through => :missing_tests
42
+ # end
43
+ #
44
+ # through is :missing_tests
45
+ #
46
+ # TODO: document a case when
47
+ # through option is a model and
48
+ # not an association name
49
+ #
50
+ # @api semipublic
51
+ def through
52
+ return @through if defined?(@through)
53
+
54
+ @through = options[:through]
55
+
56
+ return @through if @through.is_a?(Associations::Relationship)
57
+
58
+ model = source_model
59
+ repository_name = source_repository_name
60
+ relationships = model&.relationships(repository_name)
61
+ name = through_relationship_name
62
+
63
+ @through = relationships[name] ||
64
+ DataMapper.repository(repository_name) do
65
+ model&.has(min..max, name, through_model, one_to_many_options)
66
+ end
67
+
68
+ @through.child_key
69
+
70
+ @through
71
+ end
72
+
73
+ # @api semipublic
74
+ def via
75
+ return @via if defined?(@via)
76
+
77
+ @via = options[:via]
78
+
79
+ return @via if @via.is_a?(Associations::Relationship)
80
+
81
+ name = self.name
82
+ through = self.through
83
+ repository_name = through.relative_target_repository_name
84
+ through_model = through.target_model
85
+ relationships = through_model.relationships(repository_name)
86
+ singular_name = DataMapper::Inflector.singularize(name.to_s).to_sym
87
+
88
+ @via = relationships[@via] ||
89
+ relationships[name] ||
90
+ relationships[singular_name]
91
+
92
+ @via ||= if anonymous_through_model?
93
+ DataMapper.repository(repository_name) do
94
+ through_model.belongs_to(singular_name, target_model, many_to_one_options)
95
+ end
96
+ else
97
+ raise UnknownRelationshipError, "No relationships named #{name} or #{singular_name} in #{through_model}"
98
+ end
99
+
100
+ @via.child_key
101
+
102
+ @via
103
+ end
104
+
105
+ # @api semipublic
106
+ def links
107
+ return @links if defined?(@links)
108
+
109
+ @links = []
110
+ links = [through, via]
111
+
112
+ while (relationship = links.shift)
113
+ if relationship.respond_to?(:links)
114
+ links.unshift(*relationship.links)
115
+ else
116
+ @links << relationship
117
+ end
118
+ end
119
+
120
+ @links.freeze
121
+ end
122
+
123
+ # Initialize the chain for "many to many" relationships
124
+ #
125
+ # @return [self]
126
+ #
127
+ # @api public
128
+ def finalize
129
+ through
130
+ via
131
+ self
132
+ end
133
+
134
+ # @api private
135
+ def source_scope(source)
136
+ {through.inverse => source}
137
+ end
138
+
139
+ # @api private
140
+ def query
141
+ # TODO: consider making this a query_for method, so that ManyToMany::Relationship#query only
142
+ # returns the query supplied in the definition
143
+ @many_to_many_query ||= super.merge(links: links).freeze
144
+ end
145
+
146
+ # Eager load the collection using the source as a base
147
+ #
148
+ # @param [Resource, Collection] source
149
+ # the source to query with
150
+ # @param [Query, Hash] other_query
151
+ # optional query to restrict the collection
152
+ #
153
+ # @return [ManyToMany::Collection]
154
+ # the loaded collection for the source
155
+ #
156
+ # @api private
157
+ def eager_load(source, other_query = nil)
158
+ # FIXME: enable SEL for m:m relationships
159
+ source.model.all(query_for(source, other_query))
160
+ end
161
+
162
+ # @api private
163
+ private def through_model
164
+ namespace, name = through_model_namespace_name
165
+
166
+ if namespace.const_defined?(name)
167
+ namespace.const_get(name)
168
+ else
169
+ Model.new(name, namespace) do
170
+ # all properties added to the anonymous through model are keys
171
+ def property(name, type, options = {})
172
+ options[:key] = true
173
+ options.delete(:index)
174
+ super
175
+ end
176
+ end
177
+ end
178
+ end
179
+
180
+ # @api private
181
+ private def through_model_namespace_name
182
+ target_parts = target_model&.base_model&.name&.split('::')
183
+ source_parts = source_model&.base_model&.name&.split('::')
184
+
185
+ name = [target_parts&.pop, source_parts&.pop].sort.join
186
+
187
+ namespace = Object
188
+
189
+ # find the common namespace between the target_model and source_model
190
+ target_parts&.zip(source_parts) do |target_part, source_part|
191
+ break if target_part != source_part
192
+
193
+ namespace = namespace.const_get(target_part)
194
+ end
195
+
196
+ [namespace, name]
197
+ end
198
+
199
+ # @api private
200
+ private def through_relationship_name
201
+ if anonymous_through_model?
202
+ namespace = through_model_namespace_name.first
203
+ relationship_name = DataMapper::Inflector.underscore(through_model.name.sub(/\A#{namespace&.name}::/, '')).tr('/', '_')
204
+ DataMapper::Inflector.pluralize(relationship_name).to_sym
205
+ else
206
+ options[:through]
207
+ end
208
+ end
209
+
210
+ # Check if the :through association uses an anonymous model
211
+ #
212
+ # An anonymous model means that DataMapper creates the model
213
+ # in-memory, and sets the relationships to join the source
214
+ # and the target model.
215
+ #
216
+ # @return [Boolean]
217
+ # true if the through model is anonymous
218
+ #
219
+ # @api private
220
+ private def anonymous_through_model?
221
+ options[:through] == Resource
222
+ end
223
+
224
+ # @api private
225
+ private def nearest_relationship
226
+ return @nearest_relationship if defined?(@nearest_relationship)
227
+
228
+ nearest_relationship = self
229
+
230
+ nearest_relationship = nearest_relationship.through while nearest_relationship.respond_to?(:through)
231
+
232
+ @nearest_relationship = nearest_relationship
233
+ end
234
+
235
+ # @api private
236
+ private def valid_target?(target)
237
+ relationship = via
238
+ source_key = relationship.source_key
239
+ target_key = relationship.target_key
240
+
241
+ target.is_a?(target_model) &&
242
+ source_key.valid?(target_key.get(target))
243
+ end
244
+
245
+ # @api private
246
+ private def valid_source?(source)
247
+ relationship = nearest_relationship
248
+ source_key = relationship.source_key
249
+ target_key = relationship.target_key
250
+
251
+ source.is_a?(source_model) &&
252
+ target_key.valid?(source_key.get(source))
253
+ end
254
+
255
+ chainable do
256
+ # @api semipublic
257
+ def many_to_one_options
258
+ {parent_key: target_key.map(&:name)}
259
+ end
260
+
261
+ # @api semipublic
262
+ def one_to_many_options
263
+ {parent_key: source_key.map(&:name)}
264
+ end
265
+ end
266
+
267
+ # Returns the inverse relationship class
268
+ #
269
+ # @api private
270
+ private def inverse_class
271
+ self.class
272
+ end
273
+
274
+ # @api private
275
+ private def invert
276
+ inverse_class.new(inverse_name, parent_model, child_model, inverted_options)
277
+ end
278
+
279
+ # @api private
280
+ private def inverted_options
281
+ links = self.links.dup
282
+ through = links.pop&.inverse
283
+
284
+ links.reverse_each do |relationship|
285
+ inverse = relationship.inverse
286
+
287
+ through = self.class.new(
288
+ inverse.name,
289
+ inverse.child_model,
290
+ inverse.parent_model,
291
+ inverse.options.merge(through: through)
292
+ )
293
+ end
294
+
295
+ options = self.options
296
+
297
+ DataMapper::Ext::Hash.only(options, *OPTIONS - %i(min max)).update(
298
+ through: through,
299
+ child_key: options[:parent_key],
300
+ parent_key: options[:child_key],
301
+ inverse: self
302
+ )
303
+ end
304
+
305
+ # Returns collection class used by this type of
306
+ # relationship
307
+ #
308
+ # @api private
309
+ private def collection_class
310
+ ManyToMany::Collection
311
+ end
312
+ end
313
+
314
+ class Collection < Associations::OneToMany::Collection
315
+ # Remove every Resource in the m:m Collection from the repository
316
+ #
317
+ # This performs a deletion of each Resource in the Collection from
318
+ # the repository and clears the Collection.
319
+ #
320
+ # @return [Boolean]
321
+ # true if the resources were successfully destroyed
322
+ #
323
+ # @api public
324
+ def destroy
325
+ assert_source_saved 'The source must be saved before mass-deleting the collection'
326
+
327
+ # make sure the records are loaded so they can be found when
328
+ # the intermediaries are removed
329
+ lazy_load
330
+
331
+ return false unless intermediaries.all(via => self).destroy
332
+
333
+ super
334
+ end
335
+
336
+ # Remove every Resource in the m:m Collection from the repository, bypassing validation
337
+ #
338
+ # This performs a deletion of each Resource in the Collection from
339
+ # the repository and clears the Collection while skipping
340
+ # validation.
341
+ #
342
+ # @return [Boolean]
343
+ # true if the resources were successfully destroyed
344
+ #
345
+ # @api public
346
+ def destroy!
347
+ assert_source_saved 'The source must be saved before mass-deleting the collection'
348
+
349
+ model = self.model
350
+ key = model.key(repository_name)
351
+ conditions = Query.target_conditions(self, key, key)
352
+
353
+ return false unless intermediaries.all(via => self).destroy!
354
+
355
+ return false unless model.all(repository: repository, conditions: conditions).destroy!
356
+
357
+ each do |resource|
358
+ resource.persistence_state = Resource::PersistenceState::Immutable.new(resource)
359
+ end
360
+
361
+ clear
362
+
363
+ true
364
+ end
365
+
366
+ # Return the intermediaries linking the source to the targets
367
+ #
368
+ # @return [Collection]
369
+ # the intermediary collection
370
+ #
371
+ # @api public
372
+ def intermediaries
373
+ through = self.through
374
+ source = self.source
375
+
376
+ @intermediaries ||= if through.loaded?(source)
377
+ through.get_collection(source)
378
+ else
379
+ reset_intermediaries
380
+ end
381
+ end
382
+
383
+ # Map the resources in the collection to the intermediaries
384
+ #
385
+ # @return [Hash]
386
+ # the map of resources to their intermediaries
387
+ #
388
+ # @api private
389
+ protected def intermediary_for
390
+ @intermediary_for ||= {}
391
+ end
392
+
393
+ # @api private
394
+ protected def through
395
+ relationship.through
396
+ end
397
+
398
+ # @api private
399
+ protected def via
400
+ relationship.via
401
+ end
402
+
403
+ private def _create(attributes, execute_hooks = true)
404
+ via = self.via
405
+ if via.respond_to?(:resource_for)
406
+ resource = super
407
+ resource if create_intermediary(execute_hooks, resource)
408
+ elsif (intermediary = create_intermediary(execute_hooks))
409
+ super(attributes.merge(via.inverse => intermediary), execute_hooks)
410
+ end
411
+ end
412
+
413
+ # @api private
414
+ private def _save(execute_hooks = true)
415
+ via = self.via
416
+
417
+ if @removed.any?
418
+ # delete only intermediaries linked to the removed targets
419
+ return false unless intermediaries.all(via => @removed).send(execute_hooks ? :destroy : :destroy!)
420
+
421
+ # reset the intermediaries so that it reflects the current state of the datastore
422
+ reset_intermediaries
423
+ end
424
+
425
+ loaded_entries = self.loaded_entries
426
+
427
+ if via.respond_to?(:resource_for)
428
+ super
429
+ loaded_entries.all? { |resource| create_intermediary(execute_hooks, resource) }
430
+ else
431
+ if loaded_entries.any? && (intermediary = create_intermediary(execute_hooks))
432
+ inverse = via.inverse
433
+ loaded_entries.each { |resource| inverse.set(resource, intermediary) }
434
+ end
435
+
436
+ super
437
+ end
438
+ end
439
+
440
+ # @api private
441
+ private def create_intermediary(execute_hooks, resource = nil)
442
+ intermediary_for = self.intermediary_for
443
+
444
+ intermediary_resource = intermediary_for[resource]
445
+ return intermediary_resource if intermediary_resource
446
+
447
+ intermediaries = self.intermediaries
448
+ method = execute_hooks ? :save : :save!
449
+
450
+ return unless intermediaries.send(method)
451
+
452
+ attributes = {}
453
+ attributes[via] = resource if resource
454
+
455
+ intermediary = intermediaries.first_or_new(attributes)
456
+ return unless intermediary.__send__(method)
457
+
458
+ # map the resource, even if it is nil, to the intermediary
459
+ intermediary_for[resource] = intermediary
460
+ end
461
+
462
+ # @api private
463
+ private def reset_intermediaries
464
+ through = self.through
465
+ source = self.source
466
+
467
+ through.set_collection(source, through.collection_for(source))
468
+ end
469
+
470
+ # @api private
471
+ private def inverse_set(*)
472
+ # do nothing
473
+ end
474
+ end
475
+ end
476
+ end
477
+ end