dm-core 1.1.0 → 1.2.0.rc1

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 (104) hide show
  1. data/Gemfile +13 -11
  2. data/README.rdoc +1 -1
  3. data/Rakefile +1 -2
  4. data/VERSION +1 -1
  5. data/dm-core.gemspec +30 -176
  6. data/lib/dm-core.rb +32 -67
  7. data/lib/dm-core/adapters/abstract_adapter.rb +1 -2
  8. data/lib/dm-core/associations/many_to_many.rb +11 -5
  9. data/lib/dm-core/associations/many_to_one.rb +17 -2
  10. data/lib/dm-core/associations/one_to_many.rb +16 -0
  11. data/lib/dm-core/backwards.rb +13 -0
  12. data/lib/dm-core/collection.rb +1 -1
  13. data/lib/dm-core/model.rb +99 -41
  14. data/lib/dm-core/model/property.rb +24 -27
  15. data/lib/dm-core/model/relationship.rb +22 -28
  16. data/lib/dm-core/property.rb +37 -50
  17. data/lib/dm-core/property/boolean.rb +6 -10
  18. data/lib/dm-core/property/date.rb +0 -2
  19. data/lib/dm-core/property/date_time.rb +0 -2
  20. data/lib/dm-core/property/decimal.rb +5 -1
  21. data/lib/dm-core/property/discriminator.rb +24 -26
  22. data/lib/dm-core/property/float.rb +5 -1
  23. data/lib/dm-core/property/numeric.rb +6 -9
  24. data/lib/dm-core/property/string.rb +2 -1
  25. data/lib/dm-core/property/time.rb +0 -2
  26. data/lib/dm-core/property/typecast/time.rb +7 -2
  27. data/lib/dm-core/property_set.rb +1 -3
  28. data/lib/dm-core/query.rb +3 -10
  29. data/lib/dm-core/query/conditions/comparison.rb +5 -1
  30. data/lib/dm-core/query/conditions/operation.rb +1 -1
  31. data/lib/dm-core/relationship_set.rb +0 -2
  32. data/lib/dm-core/resource.rb +27 -28
  33. data/lib/dm-core/resource/{state.rb → persistence_state.rb} +2 -2
  34. data/lib/dm-core/resource/{state → persistence_state}/clean.rb +4 -4
  35. data/lib/dm-core/resource/{state → persistence_state}/deleted.rb +2 -2
  36. data/lib/dm-core/resource/{state → persistence_state}/dirty.rb +2 -2
  37. data/lib/dm-core/resource/{state → persistence_state}/immutable.rb +3 -3
  38. data/lib/dm-core/resource/{state → persistence_state}/persisted.rb +3 -3
  39. data/lib/dm-core/resource/{state → persistence_state}/transient.rb +3 -3
  40. data/lib/dm-core/spec/lib/adapter_helpers.rb +2 -5
  41. data/lib/dm-core/spec/setup.rb +3 -2
  42. data/lib/dm-core/spec/shared/public/property_spec.rb +8 -0
  43. data/lib/dm-core/spec/shared/resource_spec.rb +14 -0
  44. data/lib/dm-core/spec/shared/semipublic/property_spec.rb +1 -1
  45. data/lib/dm-core/support/descendant_set.rb +0 -2
  46. data/lib/dm-core/support/ext/array.rb +0 -19
  47. data/lib/dm-core/support/ext/blank.rb +1 -0
  48. data/lib/dm-core/support/hook.rb +0 -3
  49. data/lib/dm-core/support/naming_conventions.rb +6 -0
  50. data/lib/dm-core/support/ordered_set.rb +0 -2
  51. data/lib/dm-core/support/subject_set.rb +0 -2
  52. data/lib/dm-core/version.rb +1 -1
  53. data/spec/public/associations/many_to_many_spec.rb +0 -1
  54. data/spec/public/model/property_spec.rb +55 -9
  55. data/spec/public/model/relationship_spec.rb +24 -2
  56. data/spec/public/model_spec.rb +32 -0
  57. data/spec/public/property/binary_spec.rb +14 -6
  58. data/spec/public/property/boolean_spec.rb +14 -6
  59. data/spec/public/property/class_spec.rb +14 -6
  60. data/spec/public/property/date_spec.rb +14 -6
  61. data/spec/public/property/date_time_spec.rb +14 -6
  62. data/spec/public/property/decimal_spec.rb +10 -2
  63. data/spec/public/property/discriminator_spec.rb +15 -1
  64. data/spec/public/property/float_spec.rb +14 -6
  65. data/spec/public/property/integer_spec.rb +14 -6
  66. data/spec/public/property/object_spec.rb +8 -0
  67. data/spec/public/property/serial_spec.rb +14 -6
  68. data/spec/public/property/string_spec.rb +14 -6
  69. data/spec/public/property/text_spec.rb +14 -6
  70. data/spec/public/property/time_spec.rb +14 -6
  71. data/spec/public/resource_spec.rb +58 -0
  72. data/spec/public/shared/finder_shared_spec.rb +8 -4
  73. data/spec/semipublic/associations/many_to_many_spec.rb +2 -0
  74. data/spec/semipublic/associations/many_to_one_spec.rb +2 -0
  75. data/spec/semipublic/associations/one_to_many_spec.rb +2 -0
  76. data/spec/semipublic/associations/one_to_one_spec.rb +2 -0
  77. data/spec/semipublic/property/binary_spec.rb +5 -5
  78. data/spec/semipublic/property/boolean_spec.rb +5 -5
  79. data/spec/semipublic/property/class_spec.rb +5 -5
  80. data/spec/semipublic/property/date_spec.rb +5 -5
  81. data/spec/semipublic/property/date_time_spec.rb +5 -5
  82. data/spec/semipublic/property/decimal_spec.rb +2 -2
  83. data/spec/semipublic/property/discriminator_spec.rb +5 -5
  84. data/spec/semipublic/property/float_spec.rb +5 -5
  85. data/spec/semipublic/property/integer_spec.rb +5 -5
  86. data/spec/semipublic/property/lookup_spec.rb +3 -3
  87. data/spec/semipublic/property/serial_spec.rb +5 -5
  88. data/spec/semipublic/property/string_spec.rb +5 -5
  89. data/spec/semipublic/property/text_spec.rb +5 -5
  90. data/spec/semipublic/property/time_spec.rb +5 -5
  91. data/spec/semipublic/query/conditions/comparison_spec.rb +44 -4
  92. data/spec/semipublic/query_spec.rb +2 -11
  93. data/spec/semipublic/resource/state/clean_spec.rb +6 -6
  94. data/spec/semipublic/resource/state/deleted_spec.rb +4 -4
  95. data/spec/semipublic/resource/state/dirty_spec.rb +8 -8
  96. data/spec/semipublic/resource/state/immutable_spec.rb +6 -6
  97. data/spec/semipublic/resource/state/transient_spec.rb +5 -5
  98. data/spec/semipublic/resource/state_spec.rb +15 -15
  99. data/spec/semipublic/shared/resource_shared_spec.rb +7 -1
  100. data/spec/semipublic/shared/resource_state_shared_spec.rb +8 -8
  101. data/spec/unit/array_spec.rb +0 -14
  102. data/spec/unit/blank_spec.rb +11 -0
  103. metadata +70 -188
  104. data/lib/dm-core/support/inflector.rb +0 -3
@@ -208,8 +208,7 @@ module DataMapper
208
208
  #
209
209
  # @api semipublic
210
210
  def attributes_as_fields(attributes)
211
- pairs = attributes.map { |property, value| [ property.field, property.dump(value) ] }
212
- DataMapper::Ext::Array.to_hash(pairs)
211
+ Hash[ attributes.map { |property, value| [ property.field, property.dump(value) ] } ]
213
212
  end
214
213
 
215
214
  private
@@ -124,6 +124,14 @@ module DataMapper
124
124
  @links.freeze
125
125
  end
126
126
 
127
+ # Initialize the chain for "many to many" relationships
128
+ #
129
+ # @api public
130
+ def finalize
131
+ through
132
+ via
133
+ end
134
+
127
135
  # @api private
128
136
  def source_scope(source)
129
137
  { through.inverse => source }
@@ -248,15 +256,13 @@ module DataMapper
248
256
  target_key.valid?(source_key.get(source))
249
257
  end
250
258
 
251
- # @api semipublic
252
259
  chainable do
260
+ # @api semipublic
253
261
  def many_to_one_options
254
262
  { :parent_key => target_key.map { |property| property.name } }
255
263
  end
256
- end
257
264
 
258
- # @api semipublic
259
- chainable do
265
+ # @api semipublic
260
266
  def one_to_many_options
261
267
  { :parent_key => source_key.map { |property| property.name } }
262
268
  end
@@ -359,7 +365,7 @@ module DataMapper
359
365
  end
360
366
 
361
367
  each do |resource|
362
- resource.persisted_state = Resource::State::Immutable.new(resource)
368
+ resource.persistence_state = Resource::PersistenceState::Immutable.new(resource)
363
369
  end
364
370
 
365
371
  clear
@@ -4,7 +4,7 @@ module DataMapper
4
4
  # Relationship class with implementation specific
5
5
  # to n side of 1 to n association
6
6
  class Relationship < Associations::Relationship
7
- OPTIONS = superclass::OPTIONS.dup << :required << :key
7
+ OPTIONS = superclass::OPTIONS.dup << :required << :key << :unique
8
8
 
9
9
  # @api semipublic
10
10
  alias_method :source_repository_name, :child_repository_name
@@ -31,6 +31,11 @@ module DataMapper
31
31
  @key
32
32
  end
33
33
 
34
+ # @api semipublic
35
+ def unique?
36
+ !!@unique
37
+ end
38
+
34
39
  # @deprecated
35
40
  def nullable?
36
41
  raise "#{self.class}#nullable? is deprecated, use #{self.class}#required? instead (#{caller.first})"
@@ -64,6 +69,14 @@ module DataMapper
64
69
  # @api semipublic
65
70
  alias_method :source_key, :child_key
66
71
 
72
+ # Initialize the foreign key property this "many to one"
73
+ # relationship uses to persist itself
74
+ #
75
+ # @api public
76
+ def finalize
77
+ child_key
78
+ end
79
+
67
80
  # Returns a hash of conditions that scopes query that fetches
68
81
  # target object
69
82
  #
@@ -193,6 +206,7 @@ module DataMapper
193
206
 
194
207
  @required = options.fetch(:required, true)
195
208
  @key = options.fetch(:key, false)
209
+ @unique = options.fetch(:unique, false)
196
210
  target_model ||= DataMapper::Inflector.camelize(name)
197
211
  options = { :min => @required ? 1 : 0, :max => 1 }.update(options)
198
212
  super
@@ -248,7 +262,8 @@ module DataMapper
248
262
  options = DataMapper::Ext::Hash.only(target_property.options, :length, :precision, :scale).update(
249
263
  :index => name,
250
264
  :required => required?,
251
- :key => key?
265
+ :key => key?,
266
+ :unique => @unique
252
267
  )
253
268
 
254
269
  if target_property.primitive == Integer
@@ -100,6 +100,22 @@ module DataMapper
100
100
  end
101
101
  end
102
102
 
103
+ # initialize the inverse "many to one" relationships explicitly before
104
+ # initializing other relationships. This makes sure that foreign key
105
+ # properties always appear in the order they were declared.
106
+ #
107
+ # @api public
108
+ def finalize
109
+ child_model.relationships.each do |relationship|
110
+ # TODO: should this check #inverse?
111
+ # relationship.child_key if inverse?(relationship)
112
+ if relationship.kind_of?(Associations::ManyToOne::Relationship)
113
+ relationship.finalize
114
+ end
115
+ end
116
+ inverse.finalize
117
+ end
118
+
103
119
  # @api semipublic
104
120
  def default_for(source)
105
121
  collection_for(source).replace(Array(super))
@@ -0,0 +1,13 @@
1
+ require "dm-core/support/deprecate"
2
+
3
+ module DataMapper
4
+ module Resource
5
+ extend Deprecate
6
+
7
+ deprecate :persisted_state, :persistence_state
8
+ deprecate :persisted_state=, :persistence_state=
9
+ deprecate :persisted_state?, :persistence_state?
10
+
11
+ end # module Resource
12
+
13
+ end # module DataMapper
@@ -919,7 +919,7 @@ module DataMapper
919
919
  end
920
920
 
921
921
  each do |resource|
922
- resource.persisted_state = Resource::State::Immutable.new(resource)
922
+ resource.persistence_state = Resource::PersistenceState::Immutable.new(resource)
923
923
  end
924
924
 
925
925
  clear
@@ -1,14 +1,10 @@
1
- # TODO: add Model#create!, Model#update, Model#update!, Model#destroy and Model#destroy!
2
-
3
- # TODO: DRY up raise_on_save_failure with attr_accessor_with_default
4
- # once AS branch is merged in
5
-
6
1
  module DataMapper
7
2
  module Model
8
- extend Chainable
9
-
10
3
  include Enumerable
11
4
 
5
+ WRITER_METHOD_REGEXP = /=\z/.freeze
6
+ INVALID_WRITER_METHODS = %w[ == != === []= taguri= attributes= collection= persistence_state= raise_on_save_failure= ].to_set.freeze
7
+
12
8
  # Creates a new Model class with its constant already set
13
9
  #
14
10
  # If a block is passed, it will be eval'd in the context of the new Model
@@ -132,6 +128,19 @@ module DataMapper
132
128
  @raise_on_save_failure = raise_on_save_failure
133
129
  end
134
130
 
131
+ # Finish model setup and verify it is valid
132
+ #
133
+ # @return [undefined]
134
+ #
135
+ # @api public
136
+ def finalize
137
+ finalize_relationships
138
+ finalize_allowed_writer_methods
139
+ assert_valid_name
140
+ assert_valid_properties
141
+ assert_valid_key
142
+ end
143
+
135
144
  # Appends a module for inclusion into the model class after Resource.
136
145
  #
137
146
  # This is a useful way to extend Resource while still retaining a
@@ -211,15 +220,13 @@ module DataMapper
211
220
  end
212
221
 
213
222
  # @api private
214
- chainable do
215
- def inherited(descendant)
216
- descendants << descendant
217
-
218
- descendant.instance_variable_set(:@valid, false)
219
- descendant.instance_variable_set(:@base_model, base_model)
220
- descendant.instance_variable_set(:@storage_names, @storage_names.dup)
221
- descendant.instance_variable_set(:@default_order, @default_order.dup)
222
- end
223
+ def inherited(descendant)
224
+ descendants << descendant
225
+
226
+ descendant.instance_variable_set(:@valid, false)
227
+ descendant.instance_variable_set(:@base_model, base_model)
228
+ descendant.instance_variable_set(:@storage_names, @storage_names.dup)
229
+ descendant.instance_variable_set(:@default_order, @default_order.dup)
223
230
  end
224
231
 
225
232
  # Gets the name of the storage receptacle for this resource in the given
@@ -564,8 +571,7 @@ module DataMapper
564
571
  discriminator = properties(repository_name).discriminator
565
572
  no_reload = !query.reload?
566
573
 
567
- field_map = fields.map { |property| [ property, property.field ] }
568
- field_map = DataMapper::Ext::Array.to_hash(field_map)
574
+ field_map = Hash[ fields.map { |property| [ property, property.field ] } ]
569
575
 
570
576
  records.map do |record|
571
577
  identity_map = nil
@@ -621,13 +627,13 @@ module DataMapper
621
627
  resource.instance_variable_set(:@_repository, repository)
622
628
 
623
629
  if identity_map
624
- resource.persisted_state = Resource::State::Clean.new(resource) unless resource.persisted_state?
630
+ resource.persistence_state = Resource::PersistenceState::Clean.new(resource) unless resource.persistence_state?
625
631
 
626
632
  # defer setting the IdentityMap so second level caches can
627
633
  # record the state of the resource after loaded
628
634
  identity_map[key_values] = resource
629
635
  else
630
- resource.persisted_state = Resource::State::Immutable.new(resource)
636
+ resource.persistence_state = Resource::PersistenceState::Immutable.new(resource)
631
637
  end
632
638
 
633
639
  resource
@@ -637,6 +643,13 @@ module DataMapper
637
643
  # @api semipublic
638
644
  attr_reader :base_model
639
645
 
646
+ # The list of writer methods that can be mass-assigned to in #attributes=
647
+ #
648
+ # @return [Set]
649
+ #
650
+ # @api private
651
+ attr_reader :allowed_writer_methods
652
+
640
653
  # @api semipublic
641
654
  def default_repository_name
642
655
  Repository.default_name
@@ -760,32 +773,32 @@ module DataMapper
760
773
  end
761
774
  end
762
775
 
776
+ # Initialize all foreign key properties established by relationships
777
+ #
778
+ # @return [undefined]
779
+ #
780
+ # @api private
781
+ def finalize_relationships
782
+ relationships(repository_name).each { |relationship| relationship.finalize }
783
+ end
784
+
785
+ # Initialize the list of allowed writer methods
786
+ #
787
+ # @return [undefined]
788
+ #
789
+ # @api private
790
+ def finalize_allowed_writer_methods
791
+ @allowed_writer_methods = public_instance_methods.map { |method| method.to_s }.grep(WRITER_METHOD_REGEXP).to_set
792
+ @allowed_writer_methods -= INVALID_WRITER_METHODS
793
+ @allowed_writer_methods.freeze
794
+ end
795
+
763
796
  # @api private
764
797
  # TODO: Remove this once appropriate warnings can be added.
765
798
  def assert_valid(force = false) # :nodoc:
766
799
  return if @valid && !force
767
800
  @valid = true
768
-
769
- name = self.name
770
- repository_name = self.repository_name
771
-
772
- if properties(repository_name).empty? &&
773
- !relationships(repository_name).any? { |(relationship_name, relationship)| relationship.kind_of?(Associations::ManyToOne::Relationship) }
774
- raise IncompleteModelError, "#{name} must have at least one property or many to one relationship to be valid"
775
- end
776
-
777
- if key(repository_name).empty?
778
- raise IncompleteModelError, "#{name} must have a key to be valid"
779
- end
780
-
781
- # initialize join models and target keys
782
- @relationships.values.each do |relationships|
783
- relationships.each do |relationship|
784
- relationship.child_key
785
- relationship.through if relationship.respond_to?(:through)
786
- relationship.via if relationship.respond_to?(:via)
787
- end
788
- end
801
+ finalize
789
802
  end
790
803
 
791
804
  # Raises an exception if #get receives the wrong number of arguments
@@ -807,5 +820,50 @@ module DataMapper
807
820
  raise ArgumentError, "The number of arguments for the key is invalid, expected #{expected_key_size} but was #{actual_key_size}"
808
821
  end
809
822
  end
823
+
824
+ # Test if the model name is valid
825
+ #
826
+ # @return [undefined]
827
+ #
828
+ # @api private
829
+ def assert_valid_name
830
+ if name.to_s.strip.empty?
831
+ raise IncompleteModelError, "#{inspect} must have a name"
832
+ end
833
+ end
834
+
835
+ # Test if the model has properties
836
+ #
837
+ # A model may also be valid if it has at least one m:1 relationships which
838
+ # will add inferred foreign key properties.
839
+ #
840
+ # @return [undefined]
841
+ #
842
+ # @raise [IncompleteModelError]
843
+ # raised if the model has no properties
844
+ #
845
+ # @api private
846
+ def assert_valid_properties
847
+ repository_name = self.repository_name
848
+ if properties(repository_name).empty? &&
849
+ !relationships(repository_name).any? { |relationship| relationship.kind_of?(Associations::ManyToOne::Relationship) }
850
+ raise IncompleteModelError, "#{name} must have at least one property or many to one relationship to be valid"
851
+ end
852
+ end
853
+
854
+ # Test if the model has a valid key
855
+ #
856
+ # @return [undefined]
857
+ #
858
+ # @raise [IncompleteModelError]
859
+ # raised if the model does not have a valid key
860
+ #
861
+ # @api private
862
+ def assert_valid_key
863
+ if key(repository_name).empty?
864
+ raise IncompleteModelError, "#{name} must have a key to be valid"
865
+ end
866
+ end
867
+
810
868
  end # module Model
811
869
  end # module DataMapper
@@ -6,25 +6,22 @@ module DataMapper
6
6
  module Property
7
7
  Model.append_extensions self, DataMapper::Property::Lookup
8
8
 
9
- extend Chainable
10
-
11
9
  def self.extended(model)
12
10
  model.instance_variable_set(:@properties, {})
13
11
  model.instance_variable_set(:@field_naming_conventions, {})
14
12
  end
15
13
 
16
- chainable do
17
- def inherited(model)
18
- model.instance_variable_set(:@properties, {})
19
- model.instance_variable_set(:@field_naming_conventions, @field_naming_conventions.dup)
20
14
 
21
- @properties.each do |repository_name, properties|
22
- model_properties = model.properties(repository_name)
23
- properties.each { |property| model_properties << property }
24
- end
15
+ def inherited(model)
16
+ model.instance_variable_set(:@properties, {})
17
+ model.instance_variable_set(:@field_naming_conventions, @field_naming_conventions.dup)
25
18
 
26
- super
19
+ @properties.each do |repository_name, properties|
20
+ model_properties = model.properties(repository_name)
21
+ properties.each { |property| model_properties << property }
27
22
  end
23
+
24
+ super
28
25
  end
29
26
 
30
27
  # Defines a Property on the Resource
@@ -64,13 +61,16 @@ module DataMapper
64
61
 
65
62
  # Add property to the other mappings as well if this is for the default
66
63
  # repository.
64
+
67
65
  if repository_name == default_repository_name
68
- DataMapper::Ext::Hash.except(@properties, default_repository_name).each do |other_repository_name, properties|
66
+ other_repository_properties = DataMapper::Ext::Hash.except(@properties, default_repository_name)
67
+
68
+ other_repository_properties.each do |other_repository_name, properties|
69
69
  next if properties.named?(name)
70
70
 
71
71
  # make sure the property is created within the correct repository scope
72
72
  DataMapper.repository(other_repository_name) do
73
- properties << klass.new(self, name, options, type)
73
+ properties << klass.new(self, name, options)
74
74
  end
75
75
  end
76
76
  end
@@ -175,11 +175,10 @@ module DataMapper
175
175
 
176
176
  # @api private
177
177
  def key_conditions(repository, key)
178
- conditions = self.key(repository.name).zip(key.nil? ? [] : key)
179
- DataMapper::Ext::Array.to_hash(conditions)
178
+ Hash[ self.key(repository.name).zip(key.nil? ? [] : key) ]
180
179
  end
181
180
 
182
- private
181
+ private
183
182
 
184
183
  # Defines the anonymous module that is used to add properties.
185
184
  # Using a single module here prevents having a very large number
@@ -207,7 +206,7 @@ module DataMapper
207
206
  def #{name}
208
207
  return #{instance_variable_name} if defined?(#{instance_variable_name})
209
208
  property = properties[#{name.inspect}]
210
- #{instance_variable_name} = property ? persisted_state.get(property) : nil
209
+ #{instance_variable_name} = property ? persistence_state.get(property) : nil
211
210
  end
212
211
  RUBY
213
212
 
@@ -235,21 +234,19 @@ module DataMapper
235
234
  #{writer_visibility}
236
235
  def #{writer_name}(value)
237
236
  property = properties[#{name.inspect}]
238
- self.persisted_state = persisted_state.set(property, value)
239
- persisted_state.get(property)
237
+ self.persistence_state = persistence_state.set(property, value)
238
+ persistence_state.get(property)
240
239
  end
241
240
  RUBY
242
241
  end
243
242
 
244
- chainable do
245
- # @api public
246
- def method_missing(method, *args, &block)
247
- if property = properties(repository_name)[method]
248
- return property
249
- end
250
-
251
- super
243
+ # @api public
244
+ def method_missing(method, *args, &block)
245
+ if property = properties(repository_name)[method]
246
+ return property
252
247
  end
248
+
249
+ super
253
250
  end
254
251
  end # module Property
255
252
  end # module Model