dm-core 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. data/History.txt +25 -5
  2. data/Manifest.txt +1 -0
  3. data/README.txt +67 -23
  4. data/Rakefile +0 -2
  5. data/deps.rip +1 -1
  6. data/dm-core.gemspec +6 -6
  7. data/lib/dm-core/adapters/abstract_adapter.rb +3 -76
  8. data/lib/dm-core/adapters/data_objects_adapter.rb +8 -39
  9. data/lib/dm-core/associations/many_to_many.rb +28 -16
  10. data/lib/dm-core/associations/many_to_one.rb +1 -45
  11. data/lib/dm-core/associations/one_to_many.rb +1 -38
  12. data/lib/dm-core/associations/relationship.rb +43 -20
  13. data/lib/dm-core/collection.rb +33 -32
  14. data/lib/dm-core/model/property.rb +8 -8
  15. data/lib/dm-core/model/relationship.rb +10 -12
  16. data/lib/dm-core/property.rb +20 -85
  17. data/lib/dm-core/property_set.rb +8 -8
  18. data/lib/dm-core/query/conditions/comparison.rb +13 -71
  19. data/lib/dm-core/query/conditions/operation.rb +73 -47
  20. data/lib/dm-core/query/operator.rb +3 -45
  21. data/lib/dm-core/query/path.rb +5 -41
  22. data/lib/dm-core/query.rb +37 -108
  23. data/lib/dm-core/repository.rb +3 -79
  24. data/lib/dm-core/resource.rb +54 -49
  25. data/lib/dm-core/support/chainable.rb +0 -2
  26. data/lib/dm-core/support/equalizer.rb +23 -0
  27. data/lib/dm-core/types/object.rb +4 -4
  28. data/lib/dm-core/version.rb +1 -1
  29. data/lib/dm-core.rb +3 -11
  30. data/spec/public/model/relationship_spec.rb +4 -4
  31. data/spec/public/property_spec.rb +5 -449
  32. data/spec/public/sel_spec.rb +52 -2
  33. data/spec/public/shared/collection_shared_spec.rb +79 -26
  34. data/spec/public/shared/finder_shared_spec.rb +6 -6
  35. data/spec/public/shared/resource_shared_spec.rb +2 -2
  36. data/spec/semipublic/property_spec.rb +524 -9
  37. data/spec/semipublic/query_spec.rb +6 -6
  38. data/tasks/hoe.rb +2 -2
  39. metadata +24 -4
@@ -6,7 +6,7 @@ module DataMapper
6
6
  class Relationship
7
7
  include Extlib::Assertions
8
8
 
9
- OPTIONS = [ :child_repository_name, :parent_repository_name, :child_key, :parent_key, :min, :max, :inverse ].to_set
9
+ OPTIONS = [ :child_repository_name, :parent_repository_name, :child_key, :parent_key, :min, :max, :inverse, :reader_visibility, :writer_visibility ].to_set
10
10
 
11
11
  # Relationship name
12
12
  #
@@ -94,6 +94,22 @@ module DataMapper
94
94
  # @api semipublic
95
95
  attr_reader :max
96
96
 
97
+ # Returns the visibility for the source accessor
98
+ #
99
+ # @return [Symbol]
100
+ # the visibility for the accessor added to the source
101
+ #
102
+ # @api semipublic
103
+ attr_reader :reader_visibility
104
+
105
+ # Returns the visibility for the source mutator
106
+ #
107
+ # @return [Symbol]
108
+ # the visibility for the mutator added to the source
109
+ #
110
+ # @api semipublic
111
+ attr_reader :writer_visibility
112
+
97
113
  # Returns query options for relationship.
98
114
  #
99
115
  # For this base class, always returns query options
@@ -352,7 +368,6 @@ module DataMapper
352
368
  def ==(other)
353
369
  return true if equal?(other)
354
370
  return false if kind_of_inverse?(other)
355
-
356
371
  other.respond_to?(:cmp_repository?, true) &&
357
372
  other.respond_to?(:cmp_model?, true) &&
358
373
  other.respond_to?(:cmp_key?, true) &&
@@ -427,6 +442,8 @@ module DataMapper
427
442
  @parent_properties = @options[:parent_key].try_dup.freeze
428
443
  @min = @options[:min]
429
444
  @max = @options[:max]
445
+ @reader_visibility = @options.fetch(:reader_visibility, :public)
446
+ @writer_visibility = @options.fetch(:writer_visibility, :public)
430
447
 
431
448
  # TODO: normalize the @query to become :conditions => AndOperation
432
449
  # - Property/Relationship/Path should be left alone
@@ -478,22 +495,38 @@ module DataMapper
478
495
  object
479
496
  end
480
497
 
481
- # Creates reader method for association.
482
- #
483
- # Must be implemented by subclasses.
498
+ # Dynamically defines reader method for source side of association
499
+ # (for instance, method article for model Paragraph)
484
500
  #
485
501
  # @api semipublic
486
502
  def create_reader
487
- raise NotImplementedError, "#{self.class}#create_reader not implemented"
503
+ reader_name = name.to_s
504
+
505
+ return if source_model.resource_method_defined?(reader_name)
506
+
507
+ source_model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
508
+ #{reader_visibility} # public
509
+ def #{reader_name}(query = nil) # def author(query = nil)
510
+ relationships[#{name.inspect}].get(self, query) # relationships[:author].get(self, query)
511
+ end # end
512
+ RUBY
488
513
  end
489
514
 
490
- # Creates both writer method for association.
491
- #
492
- # Must be implemented by subclasses.
515
+ # Dynamically defines writer method for source side of association
516
+ # (for instance, method article= for model Paragraph)
493
517
  #
494
518
  # @api semipublic
495
519
  def create_writer
496
- raise NotImplementedError, "#{self.class}#create_writer not implemented"
520
+ writer_name = "#{name}="
521
+
522
+ return if source_model.resource_method_defined?(writer_name)
523
+
524
+ source_model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
525
+ #{writer_visibility} # public
526
+ def #{writer_name}(target) # def author=(target)
527
+ relationships[#{name.inspect}].set(self, target) # relationships[:author].set(self, target)
528
+ end # end
529
+ RUBY
497
530
  end
498
531
 
499
532
  # Sets the association targets in the resource
@@ -569,16 +602,6 @@ module DataMapper
569
602
  options.only(*OPTIONS - [ :min, :max ]).update(:inverse => self)
570
603
  end
571
604
 
572
- # TODO: document
573
- # @api private
574
- def options_with_inverse
575
- if child_model? && parent_model?
576
- options.merge(:inverse => inverse)
577
- else
578
- options.merge(:inverse => inverse_name)
579
- end
580
- end
581
-
582
605
  # TODO: document
583
606
  # @api private
584
607
  def kind_of_inverse?(other)
@@ -534,9 +534,10 @@ module DataMapper
534
534
  # the last Resource in the Collection
535
535
  #
536
536
  # @api public
537
- def pop
538
- return nil unless resource = super
539
- resource_removed(resource)
537
+ def pop(*)
538
+ if removed = super
539
+ resources_removed(removed)
540
+ end
540
541
  end
541
542
 
542
543
  # Removes and returns the first Resource in the Collection
@@ -545,9 +546,10 @@ module DataMapper
545
546
  # the first Resource in the Collection
546
547
  #
547
548
  # @api public
548
- def shift
549
- return nil unless resource = super
550
- resource_removed(resource)
549
+ def shift(*)
550
+ if removed = super
551
+ resources_removed(removed)
552
+ end
551
553
  end
552
554
 
553
555
  # Remove Resource from the Collection
@@ -566,8 +568,9 @@ module DataMapper
566
568
  #
567
569
  # @api public
568
570
  def delete(resource)
569
- return nil unless resource = super
570
- resource_removed(resource)
571
+ if resource = super
572
+ resource_removed(resource)
573
+ end
571
574
  end
572
575
 
573
576
  # Remove Resource from the Collection by offset
@@ -586,8 +589,9 @@ module DataMapper
586
589
  #
587
590
  # @api public
588
591
  def delete_at(offset)
589
- return nil unless resource = super
590
- resource_removed(resource)
592
+ if resource = super
593
+ resource_removed(resource)
594
+ end
591
595
  end
592
596
 
593
597
  # Deletes every Resource for which block evaluates to true.
@@ -1103,36 +1107,33 @@ module DataMapper
1103
1107
  #
1104
1108
  # @api private
1105
1109
  def default_attributes
1106
- @default_attributes ||=
1107
- begin
1108
- unless query.conditions.kind_of?(Query::Conditions::AndOperation)
1109
- return
1110
- end
1110
+ return @default_attributes if @default_attributes
1111
1111
 
1112
- default_attributes = {}
1112
+ default_attributes = {}
1113
1113
 
1114
- repository_name = repository.name
1115
- relationships = self.relationships.values
1116
- properties = model.properties(repository_name)
1117
- key = model.key(repository_name)
1114
+ conditions = query.conditions
1118
1115
 
1119
- # if all the key properties are included in the conditions,
1120
- # then do not allow them to be default attributes
1121
- if query.condition_properties.to_set.superset?(key.to_set)
1122
- properties -= key
1123
- end
1116
+ if conditions.kind_of?(Query::Conditions::AndOperation)
1117
+ repository_name = repository.name
1118
+ relationships = self.relationships.values
1119
+ properties = model.properties(repository_name)
1120
+ key = model.key(repository_name)
1124
1121
 
1125
- query.conditions.each do |condition|
1126
- unless condition.kind_of?(Query::Conditions::EqualToComparison) &&
1127
- (properties.include?(condition.subject) || (condition.relationship? && condition.subject.source_model == model))
1128
- next
1129
- end
1122
+ # if all the key properties are included in the conditions,
1123
+ # then do not allow them to be default attributes
1124
+ if query.condition_properties.to_set.superset?(key.to_set)
1125
+ properties -= key
1126
+ end
1130
1127
 
1128
+ conditions.each do |condition|
1129
+ if condition.kind_of?(Query::Conditions::EqualToComparison) &&
1130
+ (properties.include?(condition.subject) || (condition.relationship? && condition.subject.source_model == model))
1131
1131
  default_attributes[condition.subject] = condition.value
1132
1132
  end
1133
-
1134
- default_attributes.freeze
1135
1133
  end
1134
+ end
1135
+
1136
+ @default_attributes = default_attributes.freeze
1136
1137
  end
1137
1138
 
1138
1139
  # Set the default attributes for a non-frozen resource
@@ -24,9 +24,8 @@ module DataMapper
24
24
  model.instance_variable_set(:@paranoid_properties, @paranoid_properties.dup)
25
25
 
26
26
  @properties.each do |repository_name, properties|
27
- properties.each do |property|
28
- model.properties(repository_name) << property
29
- end
27
+ model_properties = model.properties(repository_name)
28
+ properties.each { |property| model_properties[property.name] ||= property }
30
29
  end
31
30
 
32
31
  super
@@ -74,8 +73,10 @@ module DataMapper
74
73
  context = options.fetch(:lazy, :default)
75
74
  context = :default if context == true
76
75
 
77
- Array(context).each do |item|
78
- properties(repository_name).lazy_context(item) << name
76
+ properties = properties(repository_name)
77
+
78
+ Array(context).each do |context|
79
+ properties.lazy_context(context) << self
79
80
  end
80
81
  end
81
82
 
@@ -83,8 +84,7 @@ module DataMapper
83
84
  # added after the child classes' properties have been copied from
84
85
  # the parent
85
86
  descendants.each do |descendant|
86
- next if descendant.properties(repository_name).named?(name)
87
- descendant.property(name, type, options)
87
+ descendant.properties(repository_name)[name] ||= property
88
88
  end
89
89
 
90
90
  create_reader_for(property)
@@ -156,7 +156,7 @@ module DataMapper
156
156
 
157
157
  descendants.each do |model|
158
158
  model.properties(repository_name).each do |property|
159
- properties << property unless properties.named?(property.name)
159
+ properties[property.name] ||= property
160
160
  end
161
161
  end
162
162
 
@@ -27,15 +27,11 @@ module DataMapper
27
27
  #
28
28
  # @api private
29
29
  def inherited(model)
30
- # TODO: Create a RelationshipSet class, and then add a method that allows copying the relationships to the supplied repository and model
31
- model.instance_variable_set(:@relationships, duped_relationships = {})
30
+ model.instance_variable_set(:@relationships, {})
32
31
 
33
32
  @relationships.each do |repository_name, relationships|
34
- dup = duped_relationships[repository_name] ||= Mash.new
35
-
36
- relationships.each do |name, relationship|
37
- dup[name] = relationship.inherited_by(model)
38
- end
33
+ model_relationships = model.relationships(repository_name)
34
+ relationships.each { |name, relationship| model_relationships[name] ||= relationship }
39
35
  end
40
36
 
41
37
  super
@@ -120,9 +116,11 @@ module DataMapper
120
116
 
121
117
  model ||= options.delete(:model)
122
118
 
119
+ repository_name = repository.name
120
+
123
121
  # TODO: change to :target_respository_name and :source_repository_name
124
122
  options[:child_repository_name] = options.delete(:repository)
125
- options[:parent_repository_name] = repository.name
123
+ options[:parent_repository_name] = repository_name
126
124
 
127
125
  klass = if options[:max] > 1
128
126
  options.key?(:through) ? Associations::ManyToMany::Relationship : Associations::OneToMany::Relationship
@@ -130,10 +128,10 @@ module DataMapper
130
128
  Associations::OneToOne::Relationship
131
129
  end
132
130
 
133
- relationship = relationships(repository.name)[name] = klass.new(name, model, self, options)
131
+ relationship = relationships(repository_name)[name] = klass.new(name, model, self, options)
134
132
 
135
133
  descendants.each do |descendant|
136
- descendant.relationships(repository.name)[name] ||= relationship.inherited_by(descendant)
134
+ descendant.relationships(repository_name)[name] ||= relationship
137
135
  end
138
136
 
139
137
  relationship
@@ -186,10 +184,10 @@ module DataMapper
186
184
  options[:child_repository_name] = repository_name
187
185
  options[:parent_repository_name] = options.delete(:repository)
188
186
 
189
- relationship = relationships(repository.name)[name] = Associations::ManyToOne::Relationship.new(name, self, model, options)
187
+ relationship = relationships(repository_name)[name] = Associations::ManyToOne::Relationship.new(name, self, model, options)
190
188
 
191
189
  descendants.each do |descendant|
192
- descendant.relationships(repository.name)[name] ||= relationship.inherited_by(descendant)
190
+ descendant.relationships(repository_name)[name] ||= relationship
193
191
  end
194
192
 
195
193
  relationship
@@ -293,10 +293,13 @@ module DataMapper
293
293
  class Property
294
294
  include Extlib::Assertions
295
295
  extend Deprecate
296
+ extend Equalizer
296
297
 
297
298
  deprecate :unique, :unique?
298
299
  deprecate :size, :length
299
300
 
301
+ equalize :model, :name
302
+
300
303
  # NOTE: PLEASE update OPTIONS in DataMapper::Type when updating
301
304
  # them here
302
305
  OPTIONS = [
@@ -363,56 +366,6 @@ module DataMapper
363
366
  @unique
364
367
  end
365
368
 
366
- # Compares another Property for equivalency
367
- #
368
- # TODO: needs example
369
- #
370
- # @param [Property] other
371
- # the other Property to compare with
372
- #
373
- # @return [Boolean]
374
- # true if they are equivalent, false if not
375
- #
376
- # @api semipublic
377
- def ==(other)
378
- if equal?(other)
379
- return true
380
- end
381
-
382
- unless other.respond_to?(:model)
383
- return false
384
- end
385
-
386
- unless other.respond_to?(:name)
387
- return false
388
- end
389
-
390
- cmp?(other, :==)
391
- end
392
-
393
- # Compares another Property for equality
394
- #
395
- # TODO: needs example
396
- #
397
- # @param [Property] other
398
- # the other Property to compare with
399
- #
400
- # @return [Boolean]
401
- # true if they are equal, false if not
402
- #
403
- # @api semipublic
404
- def eql?(other)
405
- if equal?(other)
406
- return true
407
- end
408
-
409
- unless instance_of?(other.class)
410
- return false
411
- end
412
-
413
- cmp?(other, :eql?)
414
- end
415
-
416
369
  # Returns the hash of the property name
417
370
  #
418
371
  # This is necessary to allow comparisons between different properties
@@ -640,11 +593,19 @@ module DataMapper
640
593
  #
641
594
  # @api private
642
595
  def lazy_load(resource)
643
- # If we're trying to load a lazy property, load it. Otherwise, lazy-load
644
- # any properties that should be eager-loaded but were not included
645
- # in the original :fields list
646
- property_names = lazy? ? [ name ] : model.properties(resource.repository.name).defaults.map { |property| property.name }
647
- resource.send(:lazy_load, property_names)
596
+ resource.send(:lazy_load, lazy_load_properties)
597
+ end
598
+
599
+ # TODO: document
600
+ # @api private
601
+ def lazy_load_properties
602
+ @lazy_load_properties ||= properties.in_context(lazy? ? [ self ] : properties.defaults)
603
+ end
604
+
605
+ # TODO: document
606
+ # @api private
607
+ def properties
608
+ @properties ||= model.properties(repository_name)
648
609
  end
649
610
 
650
611
  # typecasts values into a primitive (Ruby class that backs DataMapper
@@ -673,7 +634,7 @@ module DataMapper
673
634
  # @return [rue, String, Float, Integer, BigDecimal, DateTime, Date, Time, Class]
674
635
  # The typecasted +value+
675
636
  #
676
- # @api private
637
+ # @api semipublic
677
638
  def typecast(value)
678
639
  return type.typecast(value, self) if type.respond_to?(:typecast)
679
640
  return value if primitive?(value) || value.nil?
@@ -903,19 +864,16 @@ module DataMapper
903
864
  raise ArgumentError, "options[#{key.inspect}] must not be nil"
904
865
  end
905
866
 
906
- when :serial, :key, :nullable, :unique, :auto_validation
867
+ when :serial, :key, :nullable, :auto_validation
907
868
  unless value == true || value == false
908
869
  raise ArgumentError, "options[#{key.inspect}] must be either true or false"
909
870
  end
910
871
 
911
- when :lazy
912
- unless value == true || value == false || value.kind_of?(Symbol) || (value.kind_of?(Array) && value.all? { |val| val.kind_of?(Symbol) })
872
+ when :index, :unique_index, :unique, :lazy
873
+ unless value == true || value == false || value.kind_of?(Symbol) || (value.kind_of?(Array) && value.any? && value.all? { |val| val.kind_of?(Symbol) })
913
874
  raise ArgumentError, "options[#{key.inspect}] must be either true, false, a Symbol or an Array of Symbols"
914
875
  end
915
876
 
916
- when :index, :unique_index
917
- assert_kind_of "options[#{key.inspect}]", value, Symbol, Array, TrueClass
918
-
919
877
  when :length
920
878
  assert_kind_of "options[#{key.inspect}]", value, Range, Integer
921
879
 
@@ -1184,28 +1142,5 @@ module DataMapper
1184
1142
  rescue NameError
1185
1143
  value
1186
1144
  end
1187
-
1188
- # Return true if +other+'s is equivalent or equal to +self+'s
1189
- #
1190
- # @param [Property] other
1191
- # The Property whose attributes are to be compared with +self+'s
1192
- # @param [Symbol] operator
1193
- # The comparison operator to use to compare the attributes
1194
- #
1195
- # @return [Boolean]
1196
- # The result of the comparison of +other+'s attributes with +self+'s
1197
- #
1198
- # @api private
1199
- def cmp?(other, operator)
1200
- unless model.base_model.send(operator, other.model.base_model)
1201
- return false
1202
- end
1203
-
1204
- unless name.send(operator, other.name)
1205
- return false
1206
- end
1207
-
1208
- true
1209
- end
1210
1145
  end # class Property
1211
1146
  end # module DataMapper
@@ -132,10 +132,10 @@ module DataMapper
132
132
 
133
133
  # TODO: document
134
134
  # @api private
135
- def property_contexts(property_name)
135
+ def property_contexts(property)
136
136
  contexts = []
137
- lazy_contexts.each do |context, property_names|
138
- contexts << context if property_names.include?(property_name)
137
+ lazy_contexts.each do |context, properties|
138
+ contexts << context if properties.include?(property)
139
139
  end
140
140
  contexts
141
141
  end
@@ -148,16 +148,16 @@ module DataMapper
148
148
 
149
149
  # TODO: document
150
150
  # @api private
151
- def in_context(property_names)
152
- property_names_in_context = property_names.map do |property_name|
153
- if (contexts = property_contexts(property_name)).any?
151
+ def in_context(properties)
152
+ properties_in_context = properties.map do |property|
153
+ if (contexts = property_contexts(property)).any?
154
154
  lazy_contexts.values_at(*contexts)
155
155
  else
156
- property_name # not lazy
156
+ property
157
157
  end
158
158
  end
159
159
 
160
- values_at(*property_names_in_context.flatten.uniq)
160
+ properties_in_context.flatten.uniq
161
161
  end
162
162
 
163
163
  private
@@ -108,9 +108,12 @@ module DataMapper
108
108
  # A base class for the various comparison classes.
109
109
  class AbstractComparison
110
110
  extend Deprecate
111
+ extend Equalizer
111
112
 
112
113
  deprecate :property, :subject
113
114
 
115
+ equalize :slug, :subject, :value
116
+
114
117
  # The property or relationship which is being matched against
115
118
  #
116
119
  # @return [Property, Associations::Relationship]
@@ -188,6 +191,16 @@ module DataMapper
188
191
  slug ? @slug = slug : @slug
189
192
  end
190
193
 
194
+ # Return the comparison class slug
195
+ #
196
+ # @return [Symbol]
197
+ # the comparison class slug
198
+ #
199
+ # @api private
200
+ def slug
201
+ self.class.slug
202
+ end
203
+
191
204
  # Tests that the Comparison is valid
192
205
  #
193
206
  # Subclasses can overload this to customise the means by which they
@@ -227,57 +240,6 @@ module DataMapper
227
240
  subject.kind_of?(Property)
228
241
  end
229
242
 
230
- # Computes a hash-code for this Comparison
231
- #
232
- # Two Comparisons of the same class, and with the same subject and
233
- # value will have the same hash-code.
234
- #
235
- # @return [Fixnum] The computed hash-code.
236
- #
237
- # @api semipublic
238
- def hash
239
- [ self.class, @subject, @value ].hash
240
- end
241
-
242
- # Returns true if this object equals +other+
243
- #
244
- # The objects are considered equal if they are the same object, or
245
- # have the same slug, subject and value.
246
- #
247
- # @param [Object] other
248
- # Another object to be compared against this one.
249
- #
250
- # @return [Boolean]
251
- #
252
- # @api semipublic
253
- def ==(other)
254
- return true if equal?(other)
255
-
256
- return false unless other.class.respond_to?(:slug)
257
- return false unless other.respond_to?(:subject)
258
- return false unless other.respond_to?(:value)
259
-
260
- cmp?(other, :==)
261
- end
262
-
263
- # Returns true if this object equals +other+
264
- #
265
- # The objects are considered equal if they are the same object, or are
266
- # the same class, and have the same slug, subject and value.
267
- #
268
- # @param [Object] other
269
- # Another object to be compared against this one.
270
- #
271
- # @return [Boolean]
272
- #
273
- # @api semipublic
274
- def eql?(other)
275
- return true if equal?(other)
276
- return false unless instance_of?(other.class)
277
-
278
- cmp?(other, :eql?)
279
- end
280
-
281
243
  # Returns a human-readable representation of this object
282
244
  #
283
245
  # @return [String]
@@ -334,26 +296,6 @@ module DataMapper
334
296
  @loaded_value = @loaded_value.dup
335
297
  end
336
298
 
337
- # Compares this comparison with +other+ using the given +operator+
338
- #
339
- # Checks that the slug, subject and value all return true when
340
- # compared with their counterparts in +other+ with +operator+.
341
- #
342
- # @param [AbstractComparison] other
343
- # Another object to be compared against this one.
344
- #
345
- # @see AbstractComparison#==
346
- # @see AbstractComparison#eql?
347
- #
348
- # @return [Boolean]
349
- #
350
- # @api private
351
- def cmp?(other, operator)
352
- self.class.slug.send(operator, other.class.slug) &&
353
- subject.send(operator, other.subject) &&
354
- value.send(operator, other.value)
355
- end
356
-
357
299
  # Typecasts the given +val+ using subject#typecast
358
300
  #
359
301
  # If the subject has no typecast method the value is returned without