ghost_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 (254) hide show
  1. data/.autotest +29 -0
  2. data/.document +5 -0
  3. data/.gitignore +35 -0
  4. data/.yardopts +1 -0
  5. data/Gemfile +65 -0
  6. data/LICENSE +20 -0
  7. data/README.md +269 -0
  8. data/Rakefile +4 -0
  9. data/dm-core.gemspec +24 -0
  10. data/lib/dm-core.rb +292 -0
  11. data/lib/dm-core/adapters.rb +222 -0
  12. data/lib/dm-core/adapters/abstract_adapter.rb +237 -0
  13. data/lib/dm-core/adapters/in_memory_adapter.rb +113 -0
  14. data/lib/dm-core/associations/many_to_many.rb +499 -0
  15. data/lib/dm-core/associations/many_to_one.rb +290 -0
  16. data/lib/dm-core/associations/one_to_many.rb +348 -0
  17. data/lib/dm-core/associations/one_to_one.rb +86 -0
  18. data/lib/dm-core/associations/relationship.rb +663 -0
  19. data/lib/dm-core/backwards.rb +13 -0
  20. data/lib/dm-core/collection.rb +1515 -0
  21. data/lib/dm-core/core_ext/kernel.rb +23 -0
  22. data/lib/dm-core/core_ext/pathname.rb +6 -0
  23. data/lib/dm-core/core_ext/symbol.rb +10 -0
  24. data/lib/dm-core/identity_map.rb +7 -0
  25. data/lib/dm-core/model.rb +874 -0
  26. data/lib/dm-core/model/hook.rb +103 -0
  27. data/lib/dm-core/model/is.rb +32 -0
  28. data/lib/dm-core/model/property.rb +249 -0
  29. data/lib/dm-core/model/relationship.rb +378 -0
  30. data/lib/dm-core/model/scope.rb +89 -0
  31. data/lib/dm-core/property.rb +866 -0
  32. data/lib/dm-core/property/binary.rb +21 -0
  33. data/lib/dm-core/property/boolean.rb +20 -0
  34. data/lib/dm-core/property/class.rb +17 -0
  35. data/lib/dm-core/property/date.rb +10 -0
  36. data/lib/dm-core/property/date_time.rb +10 -0
  37. data/lib/dm-core/property/decimal.rb +36 -0
  38. data/lib/dm-core/property/discriminator.rb +44 -0
  39. data/lib/dm-core/property/float.rb +16 -0
  40. data/lib/dm-core/property/integer.rb +22 -0
  41. data/lib/dm-core/property/invalid_value_error.rb +22 -0
  42. data/lib/dm-core/property/lookup.rb +27 -0
  43. data/lib/dm-core/property/numeric.rb +38 -0
  44. data/lib/dm-core/property/object.rb +34 -0
  45. data/lib/dm-core/property/serial.rb +14 -0
  46. data/lib/dm-core/property/string.rb +38 -0
  47. data/lib/dm-core/property/text.rb +9 -0
  48. data/lib/dm-core/property/time.rb +10 -0
  49. data/lib/dm-core/property_set.rb +177 -0
  50. data/lib/dm-core/query.rb +1366 -0
  51. data/lib/dm-core/query/conditions/comparison.rb +911 -0
  52. data/lib/dm-core/query/conditions/operation.rb +721 -0
  53. data/lib/dm-core/query/direction.rb +36 -0
  54. data/lib/dm-core/query/operator.rb +35 -0
  55. data/lib/dm-core/query/path.rb +114 -0
  56. data/lib/dm-core/query/sort.rb +39 -0
  57. data/lib/dm-core/relationship_set.rb +72 -0
  58. data/lib/dm-core/repository.rb +226 -0
  59. data/lib/dm-core/resource.rb +1214 -0
  60. data/lib/dm-core/resource/persistence_state.rb +75 -0
  61. data/lib/dm-core/resource/persistence_state/clean.rb +40 -0
  62. data/lib/dm-core/resource/persistence_state/deleted.rb +30 -0
  63. data/lib/dm-core/resource/persistence_state/dirty.rb +96 -0
  64. data/lib/dm-core/resource/persistence_state/immutable.rb +34 -0
  65. data/lib/dm-core/resource/persistence_state/persisted.rb +29 -0
  66. data/lib/dm-core/resource/persistence_state/transient.rb +80 -0
  67. data/lib/dm-core/spec/lib/adapter_helpers.rb +64 -0
  68. data/lib/dm-core/spec/lib/collection_helpers.rb +21 -0
  69. data/lib/dm-core/spec/lib/counter_adapter.rb +38 -0
  70. data/lib/dm-core/spec/lib/pending_helpers.rb +50 -0
  71. data/lib/dm-core/spec/lib/spec_helper.rb +74 -0
  72. data/lib/dm-core/spec/setup.rb +174 -0
  73. data/lib/dm-core/spec/shared/adapter_spec.rb +341 -0
  74. data/lib/dm-core/spec/shared/public/property_spec.rb +229 -0
  75. data/lib/dm-core/spec/shared/resource_spec.rb +1232 -0
  76. data/lib/dm-core/spec/shared/sel_spec.rb +111 -0
  77. data/lib/dm-core/spec/shared/semipublic/property_spec.rb +176 -0
  78. data/lib/dm-core/spec/shared/semipublic/query/conditions/abstract_comparison_spec.rb +261 -0
  79. data/lib/dm-core/support/assertions.rb +8 -0
  80. data/lib/dm-core/support/chainable.rb +18 -0
  81. data/lib/dm-core/support/deprecate.rb +12 -0
  82. data/lib/dm-core/support/descendant_set.rb +89 -0
  83. data/lib/dm-core/support/equalizer.rb +48 -0
  84. data/lib/dm-core/support/ext/array.rb +22 -0
  85. data/lib/dm-core/support/ext/blank.rb +25 -0
  86. data/lib/dm-core/support/ext/hash.rb +67 -0
  87. data/lib/dm-core/support/ext/module.rb +47 -0
  88. data/lib/dm-core/support/ext/object.rb +57 -0
  89. data/lib/dm-core/support/ext/string.rb +24 -0
  90. data/lib/dm-core/support/ext/try_dup.rb +12 -0
  91. data/lib/dm-core/support/hook.rb +405 -0
  92. data/lib/dm-core/support/inflections.rb +60 -0
  93. data/lib/dm-core/support/inflector/inflections.rb +211 -0
  94. data/lib/dm-core/support/inflector/methods.rb +151 -0
  95. data/lib/dm-core/support/lazy_array.rb +451 -0
  96. data/lib/dm-core/support/local_object_space.rb +13 -0
  97. data/lib/dm-core/support/logger.rb +201 -0
  98. data/lib/dm-core/support/mash.rb +176 -0
  99. data/lib/dm-core/support/naming_conventions.rb +90 -0
  100. data/lib/dm-core/support/ordered_set.rb +380 -0
  101. data/lib/dm-core/support/subject.rb +33 -0
  102. data/lib/dm-core/support/subject_set.rb +250 -0
  103. data/lib/dm-core/version.rb +3 -0
  104. data/script/performance.rb +275 -0
  105. data/script/profile.rb +218 -0
  106. data/spec/lib/rspec_immediate_feedback_formatter.rb +54 -0
  107. data/spec/public/associations/many_to_many/read_multiple_join_spec.rb +68 -0
  108. data/spec/public/associations/many_to_many_spec.rb +197 -0
  109. data/spec/public/associations/many_to_one_spec.rb +83 -0
  110. data/spec/public/associations/many_to_one_with_boolean_cpk_spec.rb +40 -0
  111. data/spec/public/associations/many_to_one_with_custom_fk_spec.rb +49 -0
  112. data/spec/public/associations/one_to_many_spec.rb +81 -0
  113. data/spec/public/associations/one_to_one_spec.rb +176 -0
  114. data/spec/public/associations/one_to_one_with_boolean_cpk_spec.rb +46 -0
  115. data/spec/public/collection_spec.rb +69 -0
  116. data/spec/public/finalize_spec.rb +76 -0
  117. data/spec/public/model/hook_spec.rb +246 -0
  118. data/spec/public/model/property_spec.rb +88 -0
  119. data/spec/public/model/relationship_spec.rb +1040 -0
  120. data/spec/public/model_spec.rb +462 -0
  121. data/spec/public/property/binary_spec.rb +41 -0
  122. data/spec/public/property/boolean_spec.rb +22 -0
  123. data/spec/public/property/class_spec.rb +28 -0
  124. data/spec/public/property/date_spec.rb +22 -0
  125. data/spec/public/property/date_time_spec.rb +22 -0
  126. data/spec/public/property/decimal_spec.rb +23 -0
  127. data/spec/public/property/discriminator_spec.rb +135 -0
  128. data/spec/public/property/float_spec.rb +22 -0
  129. data/spec/public/property/integer_spec.rb +22 -0
  130. data/spec/public/property/object_spec.rb +107 -0
  131. data/spec/public/property/serial_spec.rb +22 -0
  132. data/spec/public/property/string_spec.rb +22 -0
  133. data/spec/public/property/text_spec.rb +63 -0
  134. data/spec/public/property/time_spec.rb +22 -0
  135. data/spec/public/property_spec.rb +341 -0
  136. data/spec/public/resource_spec.rb +288 -0
  137. data/spec/public/sel_spec.rb +53 -0
  138. data/spec/public/setup_spec.rb +145 -0
  139. data/spec/public/shared/association_collection_shared_spec.rb +309 -0
  140. data/spec/public/shared/collection_finder_shared_spec.rb +267 -0
  141. data/spec/public/shared/collection_shared_spec.rb +1667 -0
  142. data/spec/public/shared/finder_shared_spec.rb +1629 -0
  143. data/spec/rcov.opts +6 -0
  144. data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
  145. data/spec/semipublic/adapters/in_memory_adapter_spec.rb +13 -0
  146. data/spec/semipublic/associations/many_to_many_spec.rb +94 -0
  147. data/spec/semipublic/associations/many_to_one_spec.rb +63 -0
  148. data/spec/semipublic/associations/one_to_many_spec.rb +55 -0
  149. data/spec/semipublic/associations/one_to_one_spec.rb +53 -0
  150. data/spec/semipublic/associations/relationship_spec.rb +200 -0
  151. data/spec/semipublic/associations_spec.rb +177 -0
  152. data/spec/semipublic/collection_spec.rb +110 -0
  153. data/spec/semipublic/model_spec.rb +96 -0
  154. data/spec/semipublic/property/binary_spec.rb +13 -0
  155. data/spec/semipublic/property/boolean_spec.rb +47 -0
  156. data/spec/semipublic/property/class_spec.rb +33 -0
  157. data/spec/semipublic/property/date_spec.rb +43 -0
  158. data/spec/semipublic/property/date_time_spec.rb +46 -0
  159. data/spec/semipublic/property/decimal_spec.rb +83 -0
  160. data/spec/semipublic/property/discriminator_spec.rb +19 -0
  161. data/spec/semipublic/property/float_spec.rb +82 -0
  162. data/spec/semipublic/property/integer_spec.rb +82 -0
  163. data/spec/semipublic/property/lookup_spec.rb +29 -0
  164. data/spec/semipublic/property/serial_spec.rb +13 -0
  165. data/spec/semipublic/property/string_spec.rb +13 -0
  166. data/spec/semipublic/property/text_spec.rb +31 -0
  167. data/spec/semipublic/property/time_spec.rb +50 -0
  168. data/spec/semipublic/property_spec.rb +114 -0
  169. data/spec/semipublic/query/conditions/comparison_spec.rb +1501 -0
  170. data/spec/semipublic/query/conditions/operation_spec.rb +1294 -0
  171. data/spec/semipublic/query/path_spec.rb +471 -0
  172. data/spec/semipublic/query_spec.rb +3682 -0
  173. data/spec/semipublic/resource/state/clean_spec.rb +88 -0
  174. data/spec/semipublic/resource/state/deleted_spec.rb +78 -0
  175. data/spec/semipublic/resource/state/dirty_spec.rb +162 -0
  176. data/spec/semipublic/resource/state/immutable_spec.rb +105 -0
  177. data/spec/semipublic/resource/state/transient_spec.rb +162 -0
  178. data/spec/semipublic/resource/state_spec.rb +230 -0
  179. data/spec/semipublic/resource_spec.rb +23 -0
  180. data/spec/semipublic/shared/condition_shared_spec.rb +9 -0
  181. data/spec/semipublic/shared/resource_shared_spec.rb +199 -0
  182. data/spec/semipublic/shared/resource_state_shared_spec.rb +79 -0
  183. data/spec/semipublic/shared/subject_shared_spec.rb +79 -0
  184. data/spec/spec.opts +5 -0
  185. data/spec/spec_helper.rb +38 -0
  186. data/spec/support/core_ext/hash.rb +10 -0
  187. data/spec/support/core_ext/inheritable_attributes.rb +46 -0
  188. data/spec/support/properties/huge_integer.rb +17 -0
  189. data/spec/unit/array_spec.rb +23 -0
  190. data/spec/unit/blank_spec.rb +73 -0
  191. data/spec/unit/data_mapper/ordered_set/append_spec.rb +26 -0
  192. data/spec/unit/data_mapper/ordered_set/clear_spec.rb +24 -0
  193. data/spec/unit/data_mapper/ordered_set/delete_spec.rb +28 -0
  194. data/spec/unit/data_mapper/ordered_set/each_spec.rb +19 -0
  195. data/spec/unit/data_mapper/ordered_set/empty_spec.rb +20 -0
  196. data/spec/unit/data_mapper/ordered_set/entries_spec.rb +22 -0
  197. data/spec/unit/data_mapper/ordered_set/eql_spec.rb +51 -0
  198. data/spec/unit/data_mapper/ordered_set/equal_value_spec.rb +84 -0
  199. data/spec/unit/data_mapper/ordered_set/hash_spec.rb +12 -0
  200. data/spec/unit/data_mapper/ordered_set/include_spec.rb +23 -0
  201. data/spec/unit/data_mapper/ordered_set/index_spec.rb +28 -0
  202. data/spec/unit/data_mapper/ordered_set/initialize_spec.rb +32 -0
  203. data/spec/unit/data_mapper/ordered_set/merge_spec.rb +36 -0
  204. data/spec/unit/data_mapper/ordered_set/shared/append_spec.rb +24 -0
  205. data/spec/unit/data_mapper/ordered_set/shared/clear_spec.rb +9 -0
  206. data/spec/unit/data_mapper/ordered_set/shared/delete_spec.rb +25 -0
  207. data/spec/unit/data_mapper/ordered_set/shared/each_spec.rb +17 -0
  208. data/spec/unit/data_mapper/ordered_set/shared/empty_spec.rb +9 -0
  209. data/spec/unit/data_mapper/ordered_set/shared/entries_spec.rb +9 -0
  210. data/spec/unit/data_mapper/ordered_set/shared/include_spec.rb +9 -0
  211. data/spec/unit/data_mapper/ordered_set/shared/index_spec.rb +13 -0
  212. data/spec/unit/data_mapper/ordered_set/shared/initialize_spec.rb +28 -0
  213. data/spec/unit/data_mapper/ordered_set/shared/merge_spec.rb +28 -0
  214. data/spec/unit/data_mapper/ordered_set/shared/size_spec.rb +13 -0
  215. data/spec/unit/data_mapper/ordered_set/shared/to_ary_spec.rb +11 -0
  216. data/spec/unit/data_mapper/ordered_set/size_spec.rb +27 -0
  217. data/spec/unit/data_mapper/ordered_set/to_ary_spec.rb +23 -0
  218. data/spec/unit/data_mapper/subject_set/append_spec.rb +47 -0
  219. data/spec/unit/data_mapper/subject_set/clear_spec.rb +34 -0
  220. data/spec/unit/data_mapper/subject_set/delete_spec.rb +40 -0
  221. data/spec/unit/data_mapper/subject_set/each_spec.rb +30 -0
  222. data/spec/unit/data_mapper/subject_set/empty_spec.rb +31 -0
  223. data/spec/unit/data_mapper/subject_set/entries_spec.rb +31 -0
  224. data/spec/unit/data_mapper/subject_set/get_spec.rb +34 -0
  225. data/spec/unit/data_mapper/subject_set/include_spec.rb +32 -0
  226. data/spec/unit/data_mapper/subject_set/named_spec.rb +33 -0
  227. data/spec/unit/data_mapper/subject_set/shared/append_spec.rb +18 -0
  228. data/spec/unit/data_mapper/subject_set/shared/clear_spec.rb +9 -0
  229. data/spec/unit/data_mapper/subject_set/shared/delete_spec.rb +9 -0
  230. data/spec/unit/data_mapper/subject_set/shared/each_spec.rb +9 -0
  231. data/spec/unit/data_mapper/subject_set/shared/empty_spec.rb +9 -0
  232. data/spec/unit/data_mapper/subject_set/shared/entries_spec.rb +9 -0
  233. data/spec/unit/data_mapper/subject_set/shared/get_spec.rb +9 -0
  234. data/spec/unit/data_mapper/subject_set/shared/include_spec.rb +9 -0
  235. data/spec/unit/data_mapper/subject_set/shared/named_spec.rb +9 -0
  236. data/spec/unit/data_mapper/subject_set/shared/size_spec.rb +13 -0
  237. data/spec/unit/data_mapper/subject_set/shared/to_ary_spec.rb +9 -0
  238. data/spec/unit/data_mapper/subject_set/shared/values_at_spec.rb +44 -0
  239. data/spec/unit/data_mapper/subject_set/size_spec.rb +42 -0
  240. data/spec/unit/data_mapper/subject_set/to_ary_spec.rb +34 -0
  241. data/spec/unit/data_mapper/subject_set/values_at_spec.rb +57 -0
  242. data/spec/unit/hash_spec.rb +28 -0
  243. data/spec/unit/hook_spec.rb +1235 -0
  244. data/spec/unit/inflections_spec.rb +16 -0
  245. data/spec/unit/lazy_array_spec.rb +1949 -0
  246. data/spec/unit/mash_spec.rb +312 -0
  247. data/spec/unit/module_spec.rb +71 -0
  248. data/spec/unit/object_spec.rb +38 -0
  249. data/spec/unit/try_dup_spec.rb +46 -0
  250. data/tasks/ci.rake +1 -0
  251. data/tasks/spec.rake +38 -0
  252. data/tasks/yard.rake +9 -0
  253. data/tasks/yardstick.rake +19 -0
  254. metadata +365 -0
@@ -0,0 +1,86 @@
1
+ module DataMapper
2
+ module Associations
3
+ module OneToOne #:nodoc:
4
+ class Relationship < Associations::Relationship
5
+ %w[ public protected private ].map do |visibility|
6
+ methods = superclass.send("#{visibility}_instance_methods", false) |
7
+ DataMapper::Subject.send("#{visibility}_instance_methods", false)
8
+
9
+ methods.each do |method|
10
+ undef_method method.to_sym unless method.to_s == 'initialize'
11
+ end
12
+ end
13
+
14
+ # Loads (if necessary) and returns association target
15
+ # for given source
16
+ #
17
+ # @api semipublic
18
+ def get(source, query = nil)
19
+ relationship.get(source, query).first
20
+ end
21
+
22
+ # Get the resource directly
23
+ #
24
+ # @api semipublic
25
+ def get!(source)
26
+ collection = relationship.get!(source)
27
+ collection.first if collection
28
+ end
29
+
30
+ # Sets and returns association target
31
+ # for given source
32
+ #
33
+ # @api semipublic
34
+ def set(source, target)
35
+ relationship.set(source, [ target ].compact).first
36
+ end
37
+
38
+ # Sets the resource directly
39
+ #
40
+ # @api semipublic
41
+ def set!(source, target)
42
+ set(source, target)
43
+ end
44
+
45
+ # @api semipublic
46
+ def default_for(source)
47
+ relationship.default_for(source).first
48
+ end
49
+
50
+ # @api public
51
+ def kind_of?(klass)
52
+ super || relationship.kind_of?(klass)
53
+ end
54
+
55
+ # @api public
56
+ def instance_of?(klass)
57
+ super || relationship.instance_of?(klass)
58
+ end
59
+
60
+ # @api public
61
+ def respond_to?(method, include_private = false)
62
+ super || relationship.respond_to?(method, include_private)
63
+ end
64
+
65
+ private
66
+
67
+ attr_reader :relationship
68
+
69
+ # Initializes the relationship. Always assumes target model class is
70
+ # a camel cased association name.
71
+ #
72
+ # @api semipublic
73
+ def initialize(name, target_model, source_model, options = {})
74
+ klass = options.key?(:through) ? ManyToMany::Relationship : OneToMany::Relationship
75
+ target_model ||= DataMapper::Inflector.camelize(name).freeze
76
+ @relationship = klass.new(name, target_model, source_model, options)
77
+ end
78
+
79
+ # @api private
80
+ def method_missing(method, *args, &block)
81
+ relationship.send(method, *args, &block)
82
+ end
83
+ end # class Relationship
84
+ end # module HasOne
85
+ end # module Associations
86
+ end # module DataMapper
@@ -0,0 +1,663 @@
1
+ # TODO: move argument and option validation into the class
2
+
3
+ module DataMapper
4
+ module Associations
5
+ # Base class for relationships. Each type of relationship
6
+ # (1 to 1, 1 to n, n to m) implements a subclass of this class
7
+ # with methods like get and set overridden.
8
+ class Relationship
9
+ include DataMapper::Assertions
10
+ include Subject
11
+
12
+ OPTIONS = [ :child_repository_name, :parent_repository_name, :child_key, :parent_key, :min, :max, :inverse, :reader_visibility, :writer_visibility, :default ].to_set
13
+
14
+ # Relationship name
15
+ #
16
+ # @example for :parent association in
17
+ #
18
+ # class VersionControl::Commit
19
+ # # ...
20
+ #
21
+ # belongs_to :parent
22
+ # end
23
+ #
24
+ # name is :parent
25
+ #
26
+ # @api semipublic
27
+ attr_reader :name
28
+
29
+ # Options used to set up association of this relationship
30
+ #
31
+ # @example for :author association in
32
+ #
33
+ # class VersionControl::Commit
34
+ # # ...
35
+ #
36
+ # belongs_to :author, :model => 'Person'
37
+ # end
38
+ #
39
+ # options is a hash with a single key, :model
40
+ #
41
+ # @api semipublic
42
+ attr_reader :options
43
+
44
+ # ivar used to store collection of child options in source
45
+ #
46
+ # @example for :commits association in
47
+ #
48
+ # class VersionControl::Branch
49
+ # # ...
50
+ #
51
+ # has n, :commits
52
+ # end
53
+ #
54
+ # instance variable name for source will be @commits
55
+ #
56
+ # @api semipublic
57
+ attr_reader :instance_variable_name
58
+
59
+ # Repository from where child objects are loaded
60
+ #
61
+ # @api semipublic
62
+ attr_reader :child_repository_name
63
+
64
+ # Repository from where parent objects are loaded
65
+ #
66
+ # @api semipublic
67
+ attr_reader :parent_repository_name
68
+
69
+ # Minimum number of child objects for relationship
70
+ #
71
+ # @example for :cores association in
72
+ #
73
+ # class CPU::Multicore
74
+ # # ...
75
+ #
76
+ # has 2..n, :cores
77
+ # end
78
+ #
79
+ # minimum is 2
80
+ #
81
+ # @api semipublic
82
+ attr_reader :min
83
+
84
+ # Maximum number of child objects for
85
+ # relationship
86
+ #
87
+ # @example for :fouls association in
88
+ #
89
+ # class Basketball::Player
90
+ # # ...
91
+ #
92
+ # has 0..5, :fouls
93
+ # end
94
+ #
95
+ # maximum is 5
96
+ #
97
+ # @api semipublic
98
+ attr_reader :max
99
+
100
+ # Returns the visibility for the source accessor
101
+ #
102
+ # @return [Symbol]
103
+ # the visibility for the accessor added to the source
104
+ #
105
+ # @api semipublic
106
+ attr_reader :reader_visibility
107
+
108
+ # Returns the visibility for the source mutator
109
+ #
110
+ # @return [Symbol]
111
+ # the visibility for the mutator added to the source
112
+ #
113
+ # @api semipublic
114
+ attr_reader :writer_visibility
115
+
116
+ # Returns query options for relationship.
117
+ #
118
+ # For this base class, always returns query options
119
+ # has been initialized with.
120
+ # Overriden in subclasses.
121
+ #
122
+ # @api private
123
+ attr_reader :query
124
+
125
+ # Returns the String the Relationship would use in a Hash
126
+ #
127
+ # @return [String]
128
+ # String name for the Relationship
129
+ #
130
+ # @api private
131
+ def field
132
+ name.to_s
133
+ end
134
+
135
+ # Returns a hash of conditions that scopes query that fetches
136
+ # target object
137
+ #
138
+ # @return [Hash]
139
+ # Hash of conditions that scopes query
140
+ #
141
+ # @api private
142
+ def source_scope(source)
143
+ { inverse => source }
144
+ end
145
+
146
+ # Creates and returns Query instance that fetches
147
+ # target resource(s) (ex.: articles) for given target resource (ex.: author)
148
+ #
149
+ # @api semipublic
150
+ def query_for(source, other_query = nil)
151
+ repository_name = relative_target_repository_name_for(source)
152
+
153
+ DataMapper.repository(repository_name).scope do
154
+ query = target_model.query.dup
155
+ query.update(self.query)
156
+ query.update(:conditions => source_scope(source))
157
+ query.update(other_query) if other_query
158
+ query.update(:fields => query.fields | target_key)
159
+ end
160
+ end
161
+
162
+ # Returns model class used by child side of the relationship
163
+ #
164
+ # @return [Resource]
165
+ # Model for association child
166
+ #
167
+ # @api private
168
+ def child_model
169
+ return @child_model if defined?(@child_model)
170
+ child_model_name = self.child_model_name
171
+ @child_model = DataMapper::Ext::Module.find_const(@parent_model || Object, child_model_name)
172
+ rescue NameError
173
+ raise NameError, "Cannot find the child_model #{child_model_name} for #{parent_model_name} in #{name}"
174
+ end
175
+
176
+ # @api private
177
+ def child_model?
178
+ child_model
179
+ true
180
+ rescue NameError
181
+ false
182
+ end
183
+
184
+ # @api private
185
+ def child_model_name
186
+ @child_model ? child_model.name : @child_model_name
187
+ end
188
+
189
+ # Returns a set of keys that identify the target model
190
+ #
191
+ # @return [PropertySet]
192
+ # a set of properties that identify the target model
193
+ #
194
+ # @api semipublic
195
+ def child_key
196
+ return @child_key if defined?(@child_key)
197
+
198
+ repository_name = child_repository_name || parent_repository_name
199
+ properties = child_model.properties(repository_name)
200
+
201
+ @child_key = if @child_properties
202
+ child_key = properties.values_at(*@child_properties)
203
+ properties.class.new(child_key).freeze
204
+ else
205
+ properties.key
206
+ end
207
+ end
208
+
209
+ # Access Relationship#child_key directly
210
+ #
211
+ # @api private
212
+ alias_method :relationship_child_key, :child_key
213
+ private :relationship_child_key
214
+
215
+ # Returns model class used by parent side of the relationship
216
+ #
217
+ # @return [Resource]
218
+ # Class of association parent
219
+ #
220
+ # @api private
221
+ def parent_model
222
+ return @parent_model if defined?(@parent_model)
223
+ parent_model_name = self.parent_model_name
224
+ @parent_model = DataMapper::Ext::Module.find_const(@child_model || Object, parent_model_name)
225
+ rescue NameError
226
+ raise NameError, "Cannot find the parent_model #{parent_model_name} for #{child_model_name} in #{name}"
227
+ end
228
+
229
+ # @api private
230
+ def parent_model?
231
+ parent_model
232
+ true
233
+ rescue NameError
234
+ false
235
+ end
236
+
237
+ # @api private
238
+ def parent_model_name
239
+ @parent_model ? parent_model.name : @parent_model_name
240
+ end
241
+
242
+ # Returns a set of keys that identify parent model
243
+ #
244
+ # @return [PropertySet]
245
+ # a set of properties that identify parent model
246
+ #
247
+ # @api private
248
+ def parent_key
249
+ return @parent_key if defined?(@parent_key)
250
+
251
+ repository_name = parent_repository_name || child_repository_name
252
+ properties = parent_model.properties(repository_name)
253
+
254
+ @parent_key = if @parent_properties
255
+ parent_key = properties.values_at(*@parent_properties)
256
+ properties.class.new(parent_key).freeze
257
+ else
258
+ properties.key
259
+ end
260
+ end
261
+
262
+ # Loads and returns "other end" of the association.
263
+ # Must be implemented in subclasses.
264
+ #
265
+ # @api semipublic
266
+ def get(resource, other_query = nil)
267
+ raise NotImplementedError, "#{self.class}#get not implemented"
268
+ end
269
+
270
+ # Gets "other end" of the association directly
271
+ # as @ivar on given resource. Subclasses usually
272
+ # use implementation of this class.
273
+ #
274
+ # @api semipublic
275
+ def get!(resource)
276
+ resource.instance_variable_get(instance_variable_name)
277
+ end
278
+
279
+ # Sets value of the "other end" of association
280
+ # on given resource. Must be implemented in subclasses.
281
+ #
282
+ # @api semipublic
283
+ def set(resource, association)
284
+ raise NotImplementedError, "#{self.class}#set not implemented"
285
+ end
286
+
287
+ # Sets "other end" of the association directly
288
+ # as @ivar on given resource. Subclasses usually
289
+ # use implementation of this class.
290
+ #
291
+ # @api semipublic
292
+ def set!(resource, association)
293
+ resource.instance_variable_set(instance_variable_name, association)
294
+ end
295
+
296
+ # Eager load the collection using the source as a base
297
+ #
298
+ # @param [Collection] source
299
+ # the source collection to query with
300
+ # @param [Query, Hash] query
301
+ # optional query to restrict the collection
302
+ #
303
+ # @return [Collection]
304
+ # the loaded collection for the source
305
+ #
306
+ # @api private
307
+ def eager_load(source, query = nil)
308
+ targets = source.model.all(query_for(source, query))
309
+
310
+ # FIXME: cannot associate targets to m:m collection yet
311
+ if source.loaded? && !source.kind_of?(ManyToMany::Collection)
312
+ associate_targets(source, targets)
313
+ end
314
+
315
+ targets
316
+ end
317
+
318
+ # Checks if "other end" of association is loaded on given
319
+ # resource.
320
+ #
321
+ # @api semipublic
322
+ def loaded?(resource)
323
+ resource.instance_variable_defined?(instance_variable_name)
324
+ end
325
+
326
+ # Test the resource to see if it is a valid target
327
+ #
328
+ # @param [Object] source
329
+ # the resource or collection to be tested
330
+ #
331
+ # @return [Boolean]
332
+ # true if the resource is valid
333
+ #
334
+ # @api semipulic
335
+ def valid?(value, negated = false)
336
+ case value
337
+ when Enumerable then valid_target_collection?(value, negated)
338
+ when Resource then valid_target?(value)
339
+ when nil then true
340
+ else
341
+ raise ArgumentError, "+value+ should be an Enumerable, Resource or nil, but was a #{value.class.name}"
342
+ end
343
+ end
344
+
345
+ # Compares another Relationship for equality
346
+ #
347
+ # @param [Relationship] other
348
+ # the other Relationship to compare with
349
+ #
350
+ # @return [Boolean]
351
+ # true if they are equal, false if not
352
+ #
353
+ # @api public
354
+ def eql?(other)
355
+ return true if equal?(other)
356
+ instance_of?(other.class) && cmp?(other, :eql?)
357
+ end
358
+
359
+ # Compares another Relationship for equivalency
360
+ #
361
+ # @param [Relationship] other
362
+ # the other Relationship to compare with
363
+ #
364
+ # @return [Boolean]
365
+ # true if they are equal, false if not
366
+ #
367
+ # @api public
368
+ def ==(other)
369
+ return true if equal?(other)
370
+ other.respond_to?(:cmp_repository?, true) &&
371
+ other.respond_to?(:cmp_model?, true) &&
372
+ other.respond_to?(:cmp_key?, true) &&
373
+ other.respond_to?(:min) &&
374
+ other.respond_to?(:max) &&
375
+ other.respond_to?(:query) &&
376
+ cmp?(other, :==)
377
+ end
378
+
379
+ # Get the inverse relationship from the target model
380
+ #
381
+ # @api semipublic
382
+ def inverse
383
+ return @inverse if defined?(@inverse)
384
+
385
+ @inverse = options[:inverse]
386
+
387
+ if kind_of_inverse?(@inverse)
388
+ return @inverse
389
+ end
390
+
391
+ relationships = target_model.relationships(relative_target_repository_name)
392
+
393
+ @inverse = relationships.detect { |relationship| inverse?(relationship) } ||
394
+ invert
395
+
396
+ @inverse.child_key
397
+
398
+ @inverse
399
+ end
400
+
401
+ # @api private
402
+ def relative_target_repository_name
403
+ target_repository_name || source_repository_name
404
+ end
405
+
406
+ # @api private
407
+ def relative_target_repository_name_for(source)
408
+ target_repository_name || if source.respond_to?(:repository)
409
+ source.repository.name
410
+ else
411
+ source_repository_name
412
+ end
413
+ end
414
+
415
+ # @api private
416
+ def hash
417
+ self.class.hash ^
418
+ name.hash ^
419
+ child_repository_name.hash ^
420
+ parent_repository_name.hash ^
421
+ child_model.hash ^
422
+ parent_model.hash ^
423
+ child_properties.hash ^
424
+ parent_properties.hash ^
425
+ min.hash ^
426
+ max.hash ^
427
+ query.hash
428
+ end
429
+
430
+ private
431
+
432
+ # @api private
433
+ attr_reader :child_properties
434
+
435
+ # @api private
436
+ attr_reader :parent_properties
437
+
438
+ # Initializes new Relationship: sets attributes of relationship
439
+ # from options as well as conventions: for instance, @ivar name
440
+ # for association is constructed by prefixing @ to association name.
441
+ #
442
+ # Once attributes are set, reader and writer are created for
443
+ # the resource association belongs to
444
+ #
445
+ # @api semipublic
446
+ def initialize(name, child_model, parent_model, options = {})
447
+ initialize_object_ivar('child_model', child_model)
448
+ initialize_object_ivar('parent_model', parent_model)
449
+
450
+ @name = name
451
+ @instance_variable_name = "@#{@name}".freeze
452
+ @options = options.dup.freeze
453
+ @child_repository_name = @options[:child_repository_name]
454
+ @parent_repository_name = @options[:parent_repository_name]
455
+
456
+ unless @options[:child_key].nil?
457
+ @child_properties = DataMapper::Ext.try_dup(@options[:child_key]).freeze
458
+ end
459
+ unless @options[:parent_key].nil?
460
+ @parent_properties = DataMapper::Ext.try_dup(@options[:parent_key]).freeze
461
+ end
462
+
463
+ @min = @options[:min]
464
+ @max = @options[:max]
465
+ @reader_visibility = @options.fetch(:reader_visibility, :public)
466
+ @writer_visibility = @options.fetch(:writer_visibility, :public)
467
+ @default = @options.fetch(:default, nil)
468
+
469
+ # TODO: normalize the @query to become :conditions => AndOperation
470
+ # - Property/Relationship/Path should be left alone
471
+ # - Symbol/String keys should become a Property, scoped to the target_repository and target_model
472
+ # - Extract subject (target) from Operator
473
+ # - subject should be processed same as above
474
+ # - each subject should be transformed into AbstractComparison
475
+ # object with the subject, operator and value
476
+ # - transform into an AndOperation object, and return the
477
+ # query as :condition => and_object from self.query
478
+ # - this should provide the best performance
479
+
480
+ @query = DataMapper::Ext::Hash.except(@options, *self.class::OPTIONS).freeze
481
+ end
482
+
483
+ # Set the correct ivars for the named object
484
+ #
485
+ # This method should set the object in an ivar with the same name
486
+ # provided, plus it should set a String form of the object in
487
+ # a second ivar.
488
+ #
489
+ # @param [String]
490
+ # the name of the ivar to set
491
+ # @param [#name, #to_str, #to_sym] object
492
+ # the object to set in the ivar
493
+ #
494
+ # @return [String]
495
+ # the String value
496
+ #
497
+ # @raise [ArgumentError]
498
+ # raise when object does not respond to expected methods
499
+ #
500
+ # @api private
501
+ def initialize_object_ivar(name, object)
502
+ if object.respond_to?(:name)
503
+ instance_variable_set("@#{name}", object)
504
+ initialize_object_ivar(name, object.name)
505
+ elsif object.respond_to?(:to_str)
506
+ instance_variable_set("@#{name}_name", object.to_str.dup.freeze)
507
+ elsif object.respond_to?(:to_sym)
508
+ instance_variable_set("@#{name}_name", object.to_sym)
509
+ else
510
+ raise ArgumentError, "#{name} does not respond to #to_str or #name"
511
+ end
512
+
513
+ object
514
+ end
515
+
516
+ # Sets the association targets in the resource
517
+ #
518
+ # @param [Resource] source
519
+ # the source to set
520
+ # @param [Array<Resource>] targets
521
+ # the targets for the association
522
+ # @param [Query, Hash] query
523
+ # the query to scope the association with
524
+ #
525
+ # @return [undefined]
526
+ #
527
+ # @api private
528
+ def eager_load_targets(source, targets, query)
529
+ raise NotImplementedError, "#{self.class}#eager_load_targets not implemented"
530
+ end
531
+
532
+ # @api private
533
+ def valid_target_collection?(collection, negated)
534
+ if collection.kind_of?(Collection)
535
+ # TODO: move the check for model_key into Collection#reloadable?
536
+ # since what we're really checking is a Collection's ability
537
+ # to reload itself, which is (currently) only possible if the
538
+ # key was loaded.
539
+ model = target_model
540
+ model_key = model.key(repository.name)
541
+
542
+ collection.model <= model &&
543
+ (collection.query.fields & model_key) == model_key &&
544
+ (collection.loaded? ? (collection.any? || negated) : true)
545
+ else
546
+ collection.all? { |resource| valid_target?(resource) }
547
+ end
548
+ end
549
+
550
+ # @api private
551
+ def valid_target?(target)
552
+ target.kind_of?(target_model) &&
553
+ source_key.valid?(target_key.get(target))
554
+ end
555
+
556
+ # @api private
557
+ def valid_source?(source)
558
+ source.kind_of?(source_model) &&
559
+ target_key.valid?(source_key.get(source))
560
+ end
561
+
562
+ # @api private
563
+ def inverse?(other)
564
+ return true if @inverse.equal?(other)
565
+
566
+ other != self &&
567
+ kind_of_inverse?(other) &&
568
+ cmp_repository?(other, :==, :child) &&
569
+ cmp_repository?(other, :==, :parent) &&
570
+ cmp_model?(other, :==, :child) &&
571
+ cmp_model?(other, :==, :parent) &&
572
+ cmp_key?(other, :==, :child) &&
573
+ cmp_key?(other, :==, :parent)
574
+
575
+ # TODO: match only when the Query is empty, or is the same as the
576
+ # default scope for the target model
577
+ end
578
+
579
+ # @api private
580
+ def inverse_name
581
+ inverse = options[:inverse]
582
+ if inverse.kind_of?(Relationship)
583
+ inverse.name
584
+ else
585
+ inverse
586
+ end
587
+ end
588
+
589
+ # @api private
590
+ def invert
591
+ inverse_class.new(inverse_name, child_model, parent_model, inverted_options)
592
+ end
593
+
594
+ # @api private
595
+ def inverted_options
596
+ DataMapper::Ext::Hash.only(options, *OPTIONS - [ :min, :max ]).update(:inverse => self)
597
+ end
598
+
599
+ # @api private
600
+ def kind_of_inverse?(other)
601
+ other.kind_of?(inverse_class)
602
+ end
603
+
604
+ # @api private
605
+ def cmp?(other, operator)
606
+ name.send(operator, other.name) &&
607
+ cmp_repository?(other, operator, :child) &&
608
+ cmp_repository?(other, operator, :parent) &&
609
+ cmp_model?(other, operator, :child) &&
610
+ cmp_model?(other, operator, :parent) &&
611
+ cmp_key?(other, operator, :child) &&
612
+ cmp_key?(other, operator, :parent) &&
613
+ min.send(operator, other.min) &&
614
+ max.send(operator, other.max) &&
615
+ query.send(operator, other.query)
616
+ end
617
+
618
+ # @api private
619
+ def cmp_repository?(other, operator, type)
620
+ # if either repository is nil, then the relationship is relative,
621
+ # and the repositories are considered equivalent
622
+ return true unless repository_name = send("#{type}_repository_name")
623
+ return true unless other_repository_name = other.send("#{type}_repository_name")
624
+
625
+ repository_name.send(operator, other_repository_name)
626
+ end
627
+
628
+ # @api private
629
+ def cmp_model?(other, operator, type)
630
+ send("#{type}_model?") &&
631
+ other.send("#{type}_model?") &&
632
+ send("#{type}_model").base_model.send(operator, other.send("#{type}_model").base_model)
633
+ end
634
+
635
+ # @api private
636
+ def cmp_key?(other, operator, type)
637
+ property_method = "#{type}_properties"
638
+
639
+ self_key = send(property_method)
640
+ other_key = other.send(property_method)
641
+
642
+ self_key.send(operator, other_key)
643
+ end
644
+
645
+ def associate_targets(source, targets)
646
+ # TODO: create an object that wraps this logic, and when the first
647
+ # kicker is fired, then it'll load up the collection, and then
648
+ # populate all the other methods
649
+
650
+ target_maps = Hash.new { |hash, key| hash[key] = [] }
651
+
652
+ targets.each do |target|
653
+ target_maps[target_key.get(target)] << target
654
+ end
655
+
656
+ Array(source).each do |source|
657
+ key = source_key.get(source)
658
+ eager_load_targets(source, target_maps[key], query)
659
+ end
660
+ end
661
+ end # class Relationship
662
+ end # module Associations
663
+ end # module DataMapper