dm-core 0.10.0 → 0.10.1

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