datamapper-dm-core 0.9.11 → 0.10.0

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 (192) hide show
  1. data/.autotest +17 -14
  2. data/.gitignore +3 -1
  3. data/FAQ +6 -5
  4. data/History.txt +5 -39
  5. data/Manifest.txt +67 -76
  6. data/QUICKLINKS +1 -1
  7. data/README.txt +21 -15
  8. data/Rakefile +16 -15
  9. data/SPECS +2 -29
  10. data/TODO +1 -1
  11. data/dm-core.gemspec +11 -15
  12. data/lib/dm-core/adapters/abstract_adapter.rb +182 -185
  13. data/lib/dm-core/adapters/data_objects_adapter.rb +482 -534
  14. data/lib/dm-core/adapters/in_memory_adapter.rb +90 -69
  15. data/lib/dm-core/adapters/mysql_adapter.rb +22 -115
  16. data/lib/dm-core/adapters/oracle_adapter.rb +249 -0
  17. data/lib/dm-core/adapters/postgres_adapter.rb +7 -173
  18. data/lib/dm-core/adapters/sqlite3_adapter.rb +4 -97
  19. data/lib/dm-core/adapters/yaml_adapter.rb +116 -0
  20. data/lib/dm-core/adapters.rb +135 -16
  21. data/lib/dm-core/associations/many_to_many.rb +372 -90
  22. data/lib/dm-core/associations/many_to_one.rb +220 -73
  23. data/lib/dm-core/associations/one_to_many.rb +319 -255
  24. data/lib/dm-core/associations/one_to_one.rb +66 -53
  25. data/lib/dm-core/associations/relationship.rb +560 -158
  26. data/lib/dm-core/collection.rb +1104 -381
  27. data/lib/dm-core/core_ext/kernel.rb +12 -0
  28. data/lib/dm-core/core_ext/symbol.rb +10 -0
  29. data/lib/dm-core/identity_map.rb +4 -34
  30. data/lib/dm-core/migrations.rb +1283 -0
  31. data/lib/dm-core/model/descendant_set.rb +81 -0
  32. data/lib/dm-core/model/hook.rb +45 -0
  33. data/lib/dm-core/model/is.rb +32 -0
  34. data/lib/dm-core/model/property.rb +248 -0
  35. data/lib/dm-core/model/relationship.rb +335 -0
  36. data/lib/dm-core/model/scope.rb +90 -0
  37. data/lib/dm-core/model.rb +570 -369
  38. data/lib/dm-core/property.rb +753 -280
  39. data/lib/dm-core/property_set.rb +141 -98
  40. data/lib/dm-core/query/conditions/comparison.rb +814 -0
  41. data/lib/dm-core/query/conditions/operation.rb +247 -0
  42. data/lib/dm-core/query/direction.rb +43 -0
  43. data/lib/dm-core/query/operator.rb +42 -0
  44. data/lib/dm-core/query/path.rb +102 -0
  45. data/lib/dm-core/query/sort.rb +45 -0
  46. data/lib/dm-core/query.rb +974 -492
  47. data/lib/dm-core/repository.rb +147 -107
  48. data/lib/dm-core/resource.rb +644 -429
  49. data/lib/dm-core/spec/adapter_shared_spec.rb +294 -0
  50. data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +106 -0
  51. data/lib/dm-core/support/chainable.rb +20 -0
  52. data/lib/dm-core/support/deprecate.rb +12 -0
  53. data/lib/dm-core/support/equalizer.rb +23 -0
  54. data/lib/dm-core/support/logger.rb +13 -0
  55. data/lib/dm-core/{naming_conventions.rb → support/naming_conventions.rb} +6 -6
  56. data/lib/dm-core/transaction.rb +333 -92
  57. data/lib/dm-core/type.rb +98 -60
  58. data/lib/dm-core/types/boolean.rb +1 -1
  59. data/lib/dm-core/types/discriminator.rb +34 -20
  60. data/lib/dm-core/types/object.rb +7 -4
  61. data/lib/dm-core/types/paranoid_boolean.rb +11 -9
  62. data/lib/dm-core/types/paranoid_datetime.rb +11 -9
  63. data/lib/dm-core/types/serial.rb +3 -3
  64. data/lib/dm-core/types/text.rb +3 -4
  65. data/lib/dm-core/version.rb +1 -1
  66. data/lib/dm-core.rb +106 -110
  67. data/script/performance.rb +102 -109
  68. data/script/profile.rb +169 -38
  69. data/spec/lib/adapter_helpers.rb +105 -0
  70. data/spec/lib/collection_helpers.rb +18 -0
  71. data/spec/lib/counter_adapter.rb +34 -0
  72. data/spec/lib/pending_helpers.rb +27 -0
  73. data/spec/lib/rspec_immediate_feedback_formatter.rb +53 -0
  74. data/spec/public/associations/many_to_many_spec.rb +193 -0
  75. data/spec/public/associations/many_to_one_spec.rb +73 -0
  76. data/spec/public/associations/one_to_many_spec.rb +77 -0
  77. data/spec/public/associations/one_to_one_spec.rb +156 -0
  78. data/spec/public/collection_spec.rb +65 -0
  79. data/spec/public/model/relationship_spec.rb +924 -0
  80. data/spec/public/model_spec.rb +159 -0
  81. data/spec/public/property_spec.rb +829 -0
  82. data/spec/public/resource_spec.rb +71 -0
  83. data/spec/public/sel_spec.rb +44 -0
  84. data/spec/public/setup_spec.rb +145 -0
  85. data/spec/public/shared/association_collection_shared_spec.rb +317 -0
  86. data/spec/public/shared/collection_shared_spec.rb +1723 -0
  87. data/spec/public/shared/finder_shared_spec.rb +1619 -0
  88. data/spec/public/shared/resource_shared_spec.rb +924 -0
  89. data/spec/public/shared/sel_shared_spec.rb +112 -0
  90. data/spec/public/transaction_spec.rb +129 -0
  91. data/spec/public/types/discriminator_spec.rb +130 -0
  92. data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
  93. data/spec/semipublic/adapters/in_memory_adapter_spec.rb +12 -0
  94. data/spec/semipublic/adapters/mysql_adapter_spec.rb +17 -0
  95. data/spec/semipublic/adapters/oracle_adapter_spec.rb +194 -0
  96. data/spec/semipublic/adapters/postgres_adapter_spec.rb +17 -0
  97. data/spec/semipublic/adapters/sqlite3_adapter_spec.rb +17 -0
  98. data/spec/semipublic/adapters/yaml_adapter_spec.rb +12 -0
  99. data/spec/semipublic/associations/many_to_one_spec.rb +53 -0
  100. data/spec/semipublic/associations/relationship_spec.rb +194 -0
  101. data/spec/semipublic/associations_spec.rb +177 -0
  102. data/spec/semipublic/collection_spec.rb +142 -0
  103. data/spec/semipublic/property_spec.rb +61 -0
  104. data/spec/semipublic/query/conditions_spec.rb +528 -0
  105. data/spec/semipublic/query/path_spec.rb +443 -0
  106. data/spec/semipublic/query_spec.rb +2626 -0
  107. data/spec/semipublic/resource_spec.rb +47 -0
  108. data/spec/semipublic/shared/resource_shared_spec.rb +126 -0
  109. data/spec/spec.opts +3 -1
  110. data/spec/spec_helper.rb +80 -57
  111. data/tasks/ci.rb +19 -31
  112. data/tasks/dm.rb +43 -48
  113. data/tasks/doc.rb +8 -11
  114. data/tasks/gemspec.rb +5 -5
  115. data/tasks/hoe.rb +15 -16
  116. data/tasks/install.rb +8 -10
  117. metadata +72 -93
  118. data/lib/dm-core/associations/relationship_chain.rb +0 -81
  119. data/lib/dm-core/associations.rb +0 -207
  120. data/lib/dm-core/auto_migrations.rb +0 -105
  121. data/lib/dm-core/dependency_queue.rb +0 -32
  122. data/lib/dm-core/hook.rb +0 -11
  123. data/lib/dm-core/is.rb +0 -16
  124. data/lib/dm-core/logger.rb +0 -232
  125. data/lib/dm-core/migrations/destructive_migrations.rb +0 -17
  126. data/lib/dm-core/migrator.rb +0 -29
  127. data/lib/dm-core/scope.rb +0 -58
  128. data/lib/dm-core/support/array.rb +0 -13
  129. data/lib/dm-core/support/assertions.rb +0 -8
  130. data/lib/dm-core/support/errors.rb +0 -23
  131. data/lib/dm-core/support/kernel.rb +0 -11
  132. data/lib/dm-core/support/symbol.rb +0 -41
  133. data/lib/dm-core/support.rb +0 -7
  134. data/lib/dm-core/type_map.rb +0 -80
  135. data/lib/dm-core/types.rb +0 -19
  136. data/script/all +0 -4
  137. data/spec/integration/association_spec.rb +0 -1382
  138. data/spec/integration/association_through_spec.rb +0 -203
  139. data/spec/integration/associations/many_to_many_spec.rb +0 -449
  140. data/spec/integration/associations/many_to_one_spec.rb +0 -163
  141. data/spec/integration/associations/one_to_many_spec.rb +0 -188
  142. data/spec/integration/auto_migrations_spec.rb +0 -413
  143. data/spec/integration/collection_spec.rb +0 -1073
  144. data/spec/integration/data_objects_adapter_spec.rb +0 -32
  145. data/spec/integration/dependency_queue_spec.rb +0 -46
  146. data/spec/integration/model_spec.rb +0 -197
  147. data/spec/integration/mysql_adapter_spec.rb +0 -85
  148. data/spec/integration/postgres_adapter_spec.rb +0 -731
  149. data/spec/integration/property_spec.rb +0 -253
  150. data/spec/integration/query_spec.rb +0 -514
  151. data/spec/integration/repository_spec.rb +0 -61
  152. data/spec/integration/resource_spec.rb +0 -513
  153. data/spec/integration/sqlite3_adapter_spec.rb +0 -352
  154. data/spec/integration/sti_spec.rb +0 -273
  155. data/spec/integration/strategic_eager_loading_spec.rb +0 -156
  156. data/spec/integration/transaction_spec.rb +0 -75
  157. data/spec/integration/type_spec.rb +0 -275
  158. data/spec/lib/logging_helper.rb +0 -18
  159. data/spec/lib/mock_adapter.rb +0 -27
  160. data/spec/lib/model_loader.rb +0 -100
  161. data/spec/lib/publicize_methods.rb +0 -28
  162. data/spec/models/content.rb +0 -16
  163. data/spec/models/vehicles.rb +0 -34
  164. data/spec/models/zoo.rb +0 -48
  165. data/spec/unit/adapters/abstract_adapter_spec.rb +0 -133
  166. data/spec/unit/adapters/adapter_shared_spec.rb +0 -15
  167. data/spec/unit/adapters/data_objects_adapter_spec.rb +0 -632
  168. data/spec/unit/adapters/in_memory_adapter_spec.rb +0 -98
  169. data/spec/unit/adapters/postgres_adapter_spec.rb +0 -133
  170. data/spec/unit/associations/many_to_many_spec.rb +0 -32
  171. data/spec/unit/associations/many_to_one_spec.rb +0 -159
  172. data/spec/unit/associations/one_to_many_spec.rb +0 -393
  173. data/spec/unit/associations/one_to_one_spec.rb +0 -7
  174. data/spec/unit/associations/relationship_spec.rb +0 -71
  175. data/spec/unit/associations_spec.rb +0 -242
  176. data/spec/unit/auto_migrations_spec.rb +0 -111
  177. data/spec/unit/collection_spec.rb +0 -182
  178. data/spec/unit/data_mapper_spec.rb +0 -35
  179. data/spec/unit/identity_map_spec.rb +0 -126
  180. data/spec/unit/is_spec.rb +0 -80
  181. data/spec/unit/migrator_spec.rb +0 -33
  182. data/spec/unit/model_spec.rb +0 -321
  183. data/spec/unit/naming_conventions_spec.rb +0 -36
  184. data/spec/unit/property_set_spec.rb +0 -90
  185. data/spec/unit/property_spec.rb +0 -753
  186. data/spec/unit/query_spec.rb +0 -571
  187. data/spec/unit/repository_spec.rb +0 -93
  188. data/spec/unit/resource_spec.rb +0 -649
  189. data/spec/unit/scope_spec.rb +0 -142
  190. data/spec/unit/transaction_spec.rb +0 -493
  191. data/spec/unit/type_map_spec.rb +0 -114
  192. data/spec/unit/type_spec.rb +0 -119
@@ -1,7 +1,3 @@
1
- require 'date'
2
- require 'time'
3
- require 'bigdecimal'
4
-
5
1
  module DataMapper
6
2
 
7
3
  # :include:QUICKLINKS
@@ -13,15 +9,18 @@ module DataMapper
13
9
  # repository/database.
14
10
  #
15
11
  # If you are coming to DataMapper from another ORM framework, such as
16
- # ActiveRecord, this is a fundamental difference in thinking. However, there
17
- # are several advantages to defining your properties in your models:
12
+ # ActiveRecord, this may be a fundamental difference in thinking to you.
13
+ # However, there are several advantages to defining your properties in your
14
+ # models:
18
15
  #
19
16
  # * information about your model is centralized in one place: rather than
20
17
  # having to dig out migrations, xml or other configuration files.
18
+ # * use of mixins can be applied to model properties: better code reuse
21
19
  # * having information centralized in your models, encourages you and the
22
20
  # developers on your team to take a model-centric view of development.
23
21
  # * it provides the ability to use Ruby's access control functions.
24
- # * and, because DataMapper only cares about properties explicitly defined in
22
+ # * and, because DataMapper only cares about properties explicitly defined
23
+ # in
25
24
  # your models, DataMapper plays well with legacy databases, and shares
26
25
  # databases easily with other applications.
27
26
  #
@@ -32,16 +31,16 @@ module DataMapper
32
31
  #
33
32
  # class Post
34
33
  # include DataMapper::Resource
35
- # property :title, String, :nullable => false
36
- # # Cannot be null
37
- # property :publish, TrueClass, :default => false
38
- # # Default value for new records is false
34
+ #
35
+ # property :title, String, :nullable => false # Cannot be null
36
+ # property :publish, Boolean, :default => false # Default value for new records is false
39
37
  # end
40
38
  #
41
- # By default, DataMapper supports the following primitive types:
39
+ # By default, DataMapper supports the following primitive (Ruby) types
40
+ # also called core types:
42
41
  #
43
- # * TrueClass, Boolean
44
- # * String
42
+ # * Boolean
43
+ # * String (default length is 50)
45
44
  # * Text (limit of 65k characters by default)
46
45
  # * Float
47
46
  # * Integer
@@ -52,6 +51,8 @@ module DataMapper
52
51
  # * Object (marshalled out during serialization)
53
52
  # * Class (datastore primitive is the same as String. Used for Inheritance)
54
53
  #
54
+ # Other types are known as custom types.
55
+ #
55
56
  # For more information about available Types, see DataMapper::Type
56
57
  #
57
58
  # == Limiting Access
@@ -61,33 +62,30 @@ module DataMapper
61
62
  #
62
63
  # class Post
63
64
  # include DataMapper::Resource
64
- # property :title, String, :accessor => :private
65
- # # Both reader and writer are private
66
- # property :body, Text, :accessor => :protected
67
- # # Both reader and writer are protected
65
+ #
66
+ # property :title, String, :accessor => :private # Both reader and writer are private
67
+ # property :body, Text, :accessor => :protected # Both reader and writer are protected
68
68
  # end
69
69
  #
70
- # Access control is also analogous to Ruby accessors and mutators, and can
70
+ # Access control is also analogous to Ruby attribute readers and writers, and can
71
71
  # be declared using :reader and :writer, in addition to :accessor.
72
72
  #
73
73
  # class Post
74
74
  # include DataMapper::Resource
75
75
  #
76
- # property :title, String, :writer => :private
77
- # # Only writer is private
78
- #
79
- # property :tags, String, :reader => :protected
80
- # # Only reader is protected
76
+ # property :title, String, :writer => :private # Only writer is private
77
+ # property :tags, String, :reader => :protected # Only reader is protected
81
78
  # end
82
79
  #
83
80
  # == Overriding Accessors
84
- # The accessor for any property can be overridden in the same manner that Ruby
85
- # class accessors can be. After the property is defined, just add your custom
86
- # accessor:
81
+ # The reader/writer for any property can be overridden in the same manner that Ruby
82
+ # attr readers/writers can be. After the property is defined, just add your custom
83
+ # reader or writer:
87
84
  #
88
85
  # class Post
89
86
  # include DataMapper::Resource
90
- # property :title, String
87
+ #
88
+ # property :title, String
91
89
  #
92
90
  # def title=(new_title)
93
91
  # raise ArgumentError if new_title != 'Luke is Awesome'
@@ -107,8 +105,9 @@ module DataMapper
107
105
  #
108
106
  # class Post
109
107
  # include DataMapper::Resource
110
- # property :title, String # Loads normally
111
- # property :body, DataMapper::Types::Text # Is lazily loaded by default
108
+ #
109
+ # property :title, String # Loads normally
110
+ # property :body, Text # Is lazily loaded by default
112
111
  # end
113
112
  #
114
113
  # If you want to over-ride the lazy loading on any field you can set it to a
@@ -119,17 +118,10 @@ module DataMapper
119
118
  # class Post
120
119
  # include DataMapper::Resource
121
120
  #
122
- # property :title, String
123
- # # Loads normally
124
- #
125
- # property :body, DataMapper::Types::Text, :lazy => false
126
- # # The default is now over-ridden
127
- #
128
- # property :comment, String, lazy => [:detailed]
129
- # # Loads in the :detailed context
130
- #
131
- # property :author, String, lazy => [:summary,:detailed]
132
- # # Loads in :summary & :detailed context
121
+ # property :title, String # Loads normally
122
+ # property :body, Text, :lazy => false # The default is now over-ridden
123
+ # property :comment, String, :lazy => [ :detailed ] # Loads in the :detailed context
124
+ # property :author, String, :lazy => [ :summary, :detailed ] # Loads in :summary & :detailed context
133
125
  # end
134
126
  #
135
127
  # Delaying the request for lazy-loaded attributes even applies to objects
@@ -140,14 +132,14 @@ module DataMapper
140
132
  #
141
133
  # Example:
142
134
  #
143
- # Widget[1].components
135
+ # Widget.get(1).components
144
136
  # # loads when the post object is pulled from database, by default
145
137
  #
146
- # Widget[1].components.first.body
138
+ # Widget.get(1).components.first.body
147
139
  # # loads the values for the body property on all objects in the
148
140
  # # association, rather than just this one.
149
141
  #
150
- # Widget[1].components.first.comment
142
+ # Widget.get(1).components.first.comment
151
143
  # # loads both comment and author for all objects in the association
152
144
  # # since they are both in the :detailed context
153
145
  #
@@ -157,21 +149,21 @@ module DataMapper
157
149
  #
158
150
  # Examples:
159
151
  #
160
- # property :id, Serial # auto-incrementing key
161
- # property :legacy_pk, String, :key => true # 'natural' key
152
+ # property :id, Serial # auto-incrementing key
153
+ # property :legacy_pk, String, :key => true # 'natural' key
162
154
  #
163
155
  # This is roughly equivalent to ActiveRecord's <tt>set_primary_key</tt>,
164
156
  # though non-integer data types may be used, thus DataMapper supports natural
165
157
  # keys. When a property is declared as a natural key, accessing the object
166
158
  # using the indexer syntax <tt>Class[key]</tt> remains valid.
167
159
  #
168
- # User[1]
160
+ # User.get(1)
169
161
  # # when :id is the primary key on the users table
170
- # User['bill']
162
+ # User.get('bill')
171
163
  # # when :name is the primary (natural) key on the users table
172
164
  #
173
- # == Indeces
174
- # You can add indeces for your properties by using the <tt>:index</tt>
165
+ # == Indices
166
+ # You can add indices for your properties by using the <tt>:index</tt>
175
167
  # option. If you use <tt>true</tt> as the option value, the index will be
176
168
  # automatically named. If you want to name the index yourself, use a symbol
177
169
  # as the value.
@@ -179,7 +171,7 @@ module DataMapper
179
171
  # property :last_name, String, :index => true
180
172
  # property :first_name, String, :index => :name
181
173
  #
182
- # You can create multi-column composite indeces by using the same symbol in
174
+ # You can create multi-column composite indices by using the same symbol in
183
175
  # all the columns belonging to the index. The columns will appear in the
184
176
  # index in the order they are declared.
185
177
  #
@@ -187,7 +179,7 @@ module DataMapper
187
179
  # property :first_name, String, :index => :name
188
180
  # # => index on (last_name, first_name)
189
181
  #
190
- # If you want to make the indeces unique, use <tt>:unique_index</tt> instead
182
+ # If you want to make the indices unique, use <tt>:unique_index</tt> instead
191
183
  # of <tt>:index</tt>
192
184
  #
193
185
  # == Inferred Validations
@@ -230,7 +222,7 @@ module DataMapper
230
222
  # proc. The proc is passed two values, the resource the property is being set
231
223
  # for and the property itself.
232
224
  #
233
- # property :display_name, String, :default => { |r, p| r.login }
225
+ # property :display_name, String, :default => { |resource, property| resource.login }
234
226
  #
235
227
  # Word of warning. Don't try to read the value of the property you're setting
236
228
  # the default for in the proc. An infinite loop will ensue.
@@ -239,6 +231,56 @@ module DataMapper
239
231
  # As an alternative to extraneous has_one relationships, consider using an
240
232
  # EmbeddedValue.
241
233
  #
234
+ # == Property options reference
235
+ #
236
+ # :accessor if false, neither reader nor writer methods are
237
+ # created for this property
238
+ #
239
+ # :reader if false, reader method is not created for this property
240
+ #
241
+ # :writer if false, writer method is not created for this property
242
+ #
243
+ # :lazy if true, property value is only loaded when on first read
244
+ # if false, property value is always loaded
245
+ # if a symbol, property value is loaded with other properties
246
+ # in the same group
247
+ #
248
+ # :default default value of this property
249
+ #
250
+ # :nullable if true, property may have a nil value on save
251
+ #
252
+ # :key name of the key associated with this property.
253
+ #
254
+ # :serial if true, field value is auto incrementing
255
+ #
256
+ # :field field in the data-store which the property corresponds to
257
+ #
258
+ # :length string field length
259
+ #
260
+ # :format format for autovalidation. Use with dm-validations plugin.
261
+ #
262
+ # :index if true, index is created for the property. If a Symbol, index
263
+ # is named after Symbol value instead of being based on property name.
264
+ #
265
+ # :unique_index true specifies that index on this property should be unique
266
+ #
267
+ # :auto_validation if true, automatic validation is performed on the property
268
+ #
269
+ # :validates validation context. Use together with dm-validations.
270
+ #
271
+ # :unique if true, property column is unique. Properties of type Serial
272
+ # are unique by default.
273
+ #
274
+ # :precision Indicates the number of significant digits. Usually only makes sense
275
+ # for float type properties. Must be >= scale option value. Default is 10.
276
+ #
277
+ # :scale The number of significant digits to the right of the decimal point.
278
+ # Only makes sense for float type properties. Must be > 0.
279
+ # Default is nil for Float type and 10 for BigDecimal type.
280
+ #
281
+ # All other keys you pass to +property+ method are stored and available
282
+ # as options[:extra_keys].
283
+ #
242
284
  # == Misc. Notes
243
285
  # * Properties declared as strings will default to a length of 50, rather than
244
286
  # 255 (typical max varchar column size). To overload the default, pass
@@ -249,28 +291,27 @@ module DataMapper
249
291
  # * You may declare a Property with the data-type of <tt>Class</tt>.
250
292
  # see SingleTableInheritance for more on how to use <tt>Class</tt> columns.
251
293
  class Property
252
- include Assertions
294
+ include Extlib::Assertions
295
+ extend Deprecate
296
+ extend Equalizer
253
297
 
254
- # NOTE: check is only for psql, so maybe the postgres adapter should
255
- # define its own property options. currently it will produce a warning tho
256
- # since PROPERTY_OPTIONS is a constant
257
- #
258
- # NOTE: PLEASE update PROPERTY_OPTIONS in DataMapper::Type when updating
298
+ deprecate :unique, :unique?
299
+ deprecate :size, :length
300
+
301
+ equalize :model, :name
302
+
303
+ # NOTE: PLEASE update OPTIONS in DataMapper::Type when updating
259
304
  # them here
260
- PROPERTY_OPTIONS = [
305
+ OPTIONS = [
261
306
  :accessor, :reader, :writer,
262
307
  :lazy, :default, :nullable, :key, :serial, :field, :size, :length,
263
- :format, :index, :unique_index, :check, :ordinal, :auto_validation,
264
- :validates, :unique, :track, :precision, :scale
308
+ :format, :index, :unique_index, :auto_validation,
309
+ :validates, :unique, :precision, :scale, :min, :max
265
310
  ]
266
311
 
267
- # FIXME: can we pull the keys from
268
- # DataMapper::Adapters::DataObjectsAdapter::TYPES
269
- # for this?
270
- TYPES = [
312
+ PRIMITIVES = [
271
313
  TrueClass,
272
314
  String,
273
- DataMapper::Types::Text,
274
315
  Float,
275
316
  Integer,
276
317
  BigDecimal,
@@ -279,71 +320,113 @@ module DataMapper
279
320
  Time,
280
321
  Object,
281
322
  Class,
282
- DataMapper::Types::Discriminator,
283
- DataMapper::Types::Serial
284
- ]
285
-
286
- IMMUTABLE_TYPES = [ TrueClass, Float, Integer, BigDecimal]
323
+ ].to_set.freeze
287
324
 
288
- VISIBILITY_OPTIONS = [ :public, :protected, :private ]
325
+ # Possible :visibility option values
326
+ VISIBILITY_OPTIONS = [ :public, :protected, :private ].to_set.freeze
289
327
 
290
- DEFAULT_LENGTH = 50
291
- DEFAULT_PRECISION = 10
292
- DEFAULT_SCALE_BIGDECIMAL = 0
293
- DEFAULT_SCALE_FLOAT = nil
328
+ DEFAULT_LENGTH = 50
329
+ DEFAULT_PRECISION = 10
330
+ DEFAULT_SCALE_BIGDECIMAL = 0 # Default scale for BigDecimal type
331
+ DEFAULT_SCALE_FLOAT = nil # Default scale for Float type
332
+ DEFAULT_NUMERIC_MIN = 0
333
+ DEFAULT_NUMERIC_MAX = 2**31-1
294
334
 
295
335
  attr_reader :primitive, :model, :name, :instance_variable_name,
296
- :type, :reader_visibility, :writer_visibility, :getter, :options,
297
- :default, :precision, :scale, :track, :extra_options
336
+ :type, :reader_visibility, :writer_visibility, :options,
337
+ :default, :precision, :scale, :min, :max, :repository_name
298
338
 
299
339
  # Supplies the field in the data-store which the property corresponds to
300
340
  #
301
- # @return <String> name of field in data-store
302
- # -
303
- # @api semi-public
341
+ # @return [String] name of field in data-store
342
+ #
343
+ # @api semipublic
304
344
  def field(repository_name = nil)
305
- @field || @fields[repository_name] ||= self.model.field_naming_convention(repository_name).call(self)
345
+ if repository_name
346
+ warn "Passing in +repository_name+ to #{self.class}#field is deprecated (#{caller[0]})"
347
+
348
+ if repository_name != self.repository_name
349
+ raise ArgumentError, "Mismatching +repository_name+ with #{self.class}#repository_name (#{repository_name.inspect} != #{self.repository_name.inspect})"
350
+ end
351
+ end
352
+
353
+ # defer setting the field with the adapter specific naming
354
+ # conventions until after the adapter has been setup
355
+ @field ||= model.field_naming_convention(self.repository_name).call(self).freeze
306
356
  end
307
357
 
308
- def unique
309
- @unique ||= @options.fetch(:unique, @serial || @key || false)
358
+ # Returns true if property is unique. Serial properties and keys
359
+ # are unique by default.
360
+ #
361
+ # @return [Boolean]
362
+ # true if property has uniq index defined, false otherwise
363
+ #
364
+ # @api public
365
+ def unique?
366
+ @unique
310
367
  end
311
368
 
369
+ # Returns the hash of the property name
370
+ #
371
+ # This is necessary to allow comparisons between different properties
372
+ # in different models, having the same base model
373
+ #
374
+ # @return [Integer]
375
+ # the property name hash
376
+ #
377
+ # @api semipublic
312
378
  def hash
313
- if @custom && !@bound
314
- @type.bind(self)
315
- @bound = true
316
- end
317
-
318
- return @model.hash + @name.hash
379
+ name.hash
319
380
  end
320
381
 
321
- def eql?(o)
322
- if o.is_a?(Property)
323
- return o.model == @model && o.name == @name
382
+ # Returns maximum property length (if applicable).
383
+ # This usually only makes sense when property is of
384
+ # type Range or custom type.
385
+ #
386
+ # @return [Integer, NilClass]
387
+ # the maximum length of this property
388
+ #
389
+ # @api semipublic
390
+ def length
391
+ if @length.kind_of?(Range)
392
+ @length.max
324
393
  else
325
- return false
394
+ @length
326
395
  end
327
396
  end
328
397
 
329
- def length
330
- @length.is_a?(Range) ? @length.max : @length
331
- end
332
- alias size length
333
-
398
+ # Returns index name if property has index.
399
+ #
400
+ # @return [true, Symbol, Array, nil]
401
+ # returns true if property is indexed by itself
402
+ # returns a Symbol if the property is indexed with other properties
403
+ # returns an Array if the property belongs to multiple indexes
404
+ # returns nil if the property does not belong to any indexes
405
+ #
406
+ # @api public
334
407
  def index
335
408
  @index
336
409
  end
337
410
 
411
+ # Returns true if property has unique index. Serial properties and
412
+ # keys are unique by default.
413
+ #
414
+ # @return [true, Symbol, Array, nil]
415
+ # returns true if property is indexed by itself
416
+ # returns a Symbol if the property is indexed with other properties
417
+ # returns an Array if the property belongs to multiple indexes
418
+ # returns nil if the property does not belong to any indexes
419
+ #
420
+ # @api public
338
421
  def unique_index
339
422
  @unique_index
340
423
  end
341
424
 
342
425
  # Returns whether or not the property is to be lazy-loaded
343
426
  #
344
- # @return <TrueClass, FalseClass> whether or not the property is to be
345
- # lazy-loaded
346
- # -
427
+ # @return [Boolean]
428
+ # true if the property is to be lazy-loaded
429
+ #
347
430
  # @api public
348
431
  def lazy?
349
432
  @lazy
@@ -351,9 +434,9 @@ module DataMapper
351
434
 
352
435
  # Returns whether or not the property is a key or a part of a key
353
436
  #
354
- # @return <TrueClass, FalseClass> whether the property is a key or a part of
355
- # a key
356
- #-
437
+ # @return [Boolean]
438
+ # true if the property is a key or a part of a key
439
+ #
357
440
  # @api public
358
441
  def key?
359
442
  @key
@@ -361,8 +444,9 @@ module DataMapper
361
444
 
362
445
  # Returns whether or not the property is "serial" (auto-incrementing)
363
446
  #
364
- # @return <TrueClass, FalseClass> whether or not the property is "serial"
365
- #-
447
+ # @return [Boolean]
448
+ # whether or not the property is "serial"
449
+ #
366
450
  # @api public
367
451
  def serial?
368
452
  @serial
@@ -370,232 +454,368 @@ module DataMapper
370
454
 
371
455
  # Returns whether or not the property can accept 'nil' as it's value
372
456
  #
373
- # @return <TrueClass, FalseClass> whether or not the property can accept 'nil'
374
- #-
457
+ # @return [Boolean]
458
+ # whether or not the property can accept 'nil'
459
+ #
375
460
  # @api public
376
461
  def nullable?
377
462
  @nullable
378
463
  end
379
464
 
465
+ # Returns whether or not the property is custom (not provided by dm-core)
466
+ #
467
+ # @return [Boolean]
468
+ # whether or not the property is custom
469
+ #
470
+ # @api public
380
471
  def custom?
381
472
  @custom
382
473
  end
383
474
 
384
- # Provides a standardized getter method for the property
475
+ # Standardized reader method for the property
476
+ #
477
+ # @param [Resource] resource
478
+ # model instance for which this property is to be loaded
479
+ #
480
+ # @return [Object]
481
+ # the value of this property for the provided instance
482
+ #
483
+ # @raise [ArgumentError] "+resource+ should be a Resource, but was ...."
385
484
  #
386
- # @raise <ArgumentError> "+resource+ should be a DataMapper::Resource, but was ...."
387
- #-
388
485
  # @api private
389
486
  def get(resource)
390
- lazy_load(resource)
391
-
392
- value = get!(resource)
393
-
394
- set_original_value(resource, value)
487
+ lazy_load(resource) unless loaded?(resource) || resource.new?
395
488
 
396
- # [YK] Why did we previously care whether options[:default] is nil.
397
- # The default value of nil will be applied either way
398
- if value.nil? && resource.new_record? && !resource.attribute_loaded?(name)
399
- value = default_for(resource)
400
- set(resource, value)
489
+ if loaded?(resource)
490
+ get!(resource)
491
+ else
492
+ set(resource, default? ? default_for(resource) : nil)
401
493
  end
402
-
403
- value
404
494
  end
405
495
 
496
+ # Fetch the ivar value in the resource
497
+ #
498
+ # @param [Resource] resource
499
+ # model instance for which this property is to be unsafely loaded
500
+ #
501
+ # @return [Object]
502
+ # current @ivar value of this property in +resource+
503
+ #
504
+ # @api private
406
505
  def get!(resource)
407
506
  resource.instance_variable_get(instance_variable_name)
408
507
  end
409
508
 
410
- def set_original_value(resource, val)
411
- unless resource.original_values.key?(name)
412
- val = val.try_dup
413
- val = val.hash if track == :hash
414
- resource.original_values[name] = val
509
+ # Sets original value of the property on given resource.
510
+ # When property is set on DataMapper resource instance,
511
+ # original value is preserved. This makes possible to
512
+ # track dirty attributes and save only those really changed,
513
+ # and avoid extra queries to the data source in certain
514
+ # situations.
515
+ #
516
+ # @param [Resource] resource
517
+ # model instance for which to set the original value
518
+ # @param [Object] original
519
+ # value to set as original value for this property in +resource+
520
+ #
521
+ # @api private
522
+ def set_original_value(resource, original)
523
+ original_attributes = resource.original_attributes
524
+ original = self.value(original)
525
+
526
+ if original_attributes.key?(self)
527
+ # stop tracking the value if it has not changed
528
+ original_attributes.delete(self) if original == original_attributes[self] && resource.saved?
529
+ else
530
+ original_attributes[self] = original
415
531
  end
416
532
  end
417
533
 
418
534
  # Provides a standardized setter method for the property
419
535
  #
420
- # @raise <ArgumentError> "+resource+ should be a DataMapper::Resource, but was ...."
421
- #-
536
+ # @param [Resource] resource
537
+ # the resource to get the value from
538
+ # @param [Object] value
539
+ # the value to set in the resource
540
+ #
541
+ # @return [Object]
542
+ # +value+ after being typecasted according to this property's primitive
543
+ #
544
+ # @raise [ArgumentError] "+resource+ should be a Resource, but was ...."
545
+ #
422
546
  # @api private
423
547
  def set(resource, value)
424
- # [YK] We previously checked for new_record? here, but lazy loading
425
- # is blocked anyway if we're in a new record by by
426
- # Resource#reload_attributes. This may eventually be useful for
427
- # optimizing, but let's (a) benchmark it first, and (b) do
428
- # whatever refactoring is necessary, which will benefit from the
429
- # centralize checking
430
- lazy_load(resource)
548
+ loaded = loaded?(resource)
549
+ original = get!(resource) if loaded
550
+ value = typecast(value)
431
551
 
432
- new_value = typecast(value)
433
- old_value = get!(resource)
552
+ if loaded && value == original
553
+ return original
554
+ end
434
555
 
435
- set_original_value(resource, old_value)
556
+ set_original_value(resource, original)
436
557
 
437
- set!(resource, new_value)
558
+ set!(resource, value)
438
559
  end
439
560
 
561
+ # Set the ivar value in the resource
562
+ #
563
+ # @param [Resource] resource
564
+ # the resource to set
565
+ # @param [Object] value
566
+ # the value to set in the resource
567
+ #
568
+ # @return [Object]
569
+ # the value set in the resource
570
+ #
571
+ # @api private
440
572
  def set!(resource, value)
441
573
  resource.instance_variable_set(instance_variable_name, value)
442
574
  end
443
575
 
576
+ # Check if the attribute corresponding to the property is loaded
577
+ #
578
+ # @param [Resource] resource
579
+ # model instance for which the attribute is to be tested
580
+ #
581
+ # @return [Boolean]
582
+ # true if the attribute is loaded in the resource
583
+ #
584
+ # @api private
585
+ def loaded?(resource)
586
+ resource.instance_variable_defined?(instance_variable_name)
587
+ end
588
+
444
589
  # Loads lazy columns when get or set is called.
445
- #-
590
+ #
591
+ # @param [Resource] resource
592
+ # model instance for which lazy loaded attribute are loaded
593
+ #
446
594
  # @api private
447
595
  def lazy_load(resource)
448
- # It is faster to bail out at at a new_record? rather than to process
449
- # which properties would be loaded and then not load them.
450
- return if resource.new_record? || resource.attribute_loaded?(name)
451
- # If we're trying to load a lazy property, load it. Otherwise, lazy-load
452
- # any properties that should be eager-loaded but were not included
453
- # in the original :fields list
454
- contexts = lazy? ? name : model.eager_properties(resource.repository.name)
455
- resource.send(:lazy_load, contexts)
596
+ resource.send(:lazy_load, lazy_load_properties)
456
597
  end
457
598
 
458
- # typecasts values into a primitive
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)
609
+ end
610
+
611
+ # typecasts values into a primitive (Ruby class that backs DataMapper
612
+ # property type). If property type can handle typecasting, it is delegated.
613
+ # How typecasting is perfomed, depends on the primitive of the type.
614
+ #
615
+ # If type's primitive is a TrueClass, values of 1, t and true are casted to true.
616
+ #
617
+ # For String primitive, +to_s+ is called on value.
618
+ #
619
+ # For Float primitive, +to_f+ is called on value but only if value is a number
620
+ # otherwise value is returned.
621
+ #
622
+ # For Integer primitive, +to_i+ is called on value but only if value is a
623
+ # number, otherwise value is returned.
624
+ #
625
+ # For BigDecimal primitive, +to_d+ is called on value but only if value is a
626
+ # number, otherwise value is returned.
627
+ #
628
+ # Casting to DateTime, Time and Date can handle both hashes with keys like :day or
629
+ # :hour and strings in format methods like Time.parse can handle.
630
+ #
631
+ # @param [#to_s, #to_f, #to_i, #to_d, Hash] value
632
+ # the value to typecast
633
+ #
634
+ # @return [rue, String, Float, Integer, BigDecimal, DateTime, Date, Time, Class]
635
+ # The typecasted +value+
459
636
  #
460
- # @return <TrueClass, String, Float, Integer, BigDecimal, DateTime, Date, Time
461
- # Class> the primitive data-type, defaults to TrueClass
462
- #-
463
637
  # @api private
464
638
  def typecast(value)
465
639
  return type.typecast(value, self) if type.respond_to?(:typecast)
466
- return value if value.kind_of?(primitive) || value.nil?
467
- begin
468
- if primitive == TrueClass then %w[ true 1 t ].include?(value.to_s.downcase)
469
- elsif primitive == String then value.to_s
470
- elsif primitive == Float then value.to_f
471
- elsif primitive == Integer
472
- # The simplest possible implementation, i.e. value.to_i, is not
473
- # desirable because "junk".to_i gives "0". We want nil instead,
474
- # because this makes it clear that the typecast failed.
475
- #
476
- # After benchmarking, we preferred the current implementation over
477
- # these two alternatives:
478
- # * Integer(value) rescue nil
479
- # * Integer(value_to_s =~ /(\d+)/ ? $1 : value_to_s) rescue nil
480
- #
481
- # [YK] The previous implementation used a rescue. Why use a rescue
482
- # when the list of cases where a valid string other than "0" could
483
- # produce 0 is known?
484
- value_to_i = value.to_i
485
- if value_to_i == 0
486
- value.to_s =~ /^(0x|0b)?0+/ ? 0 : nil
487
- else
488
- value_to_i
489
- end
490
- elsif primitive == BigDecimal then BigDecimal(value.to_s)
491
- elsif primitive == DateTime then typecast_to_datetime(value)
492
- elsif primitive == Date then typecast_to_date(value)
493
- elsif primitive == Time then typecast_to_time(value)
494
- elsif primitive == Class then self.class.find_const(value)
495
- else
496
- value
497
- end
498
- rescue
640
+ return value if primitive?(value) || value.nil?
641
+
642
+ if primitive == Integer then typecast_to_integer(value)
643
+ elsif primitive == String then typecast_to_string(value)
644
+ elsif primitive == TrueClass then typecast_to_boolean(value)
645
+ elsif primitive == BigDecimal then typecast_to_bigdecimal(value)
646
+ elsif primitive == Float then typecast_to_float(value)
647
+ elsif primitive == DateTime then typecast_to_datetime(value)
648
+ elsif primitive == Time then typecast_to_time(value)
649
+ elsif primitive == Date then typecast_to_date(value)
650
+ elsif primitive == Class then typecast_to_class(value)
651
+ else
499
652
  value
500
653
  end
501
654
  end
502
655
 
656
+ # Returns a default value of the
657
+ # property for given resource.
658
+ #
659
+ # When default value is a callable object,
660
+ # it is called with resource and property passed
661
+ # as arguments.
662
+ #
663
+ # @param [Resource] resource
664
+ # the model instance for which the default is to be set
665
+ #
666
+ # @return [Object]
667
+ # the default value of this property for +resource+
668
+ #
669
+ # @api semipublic
503
670
  def default_for(resource)
504
- @default.respond_to?(:call) ? @default.call(resource, self) : @default
671
+ if @default.respond_to?(:call)
672
+ @default.call(resource, self)
673
+ else
674
+ @default.try_dup
675
+ end
505
676
  end
506
677
 
507
- def value(val)
508
- custom? ? self.type.dump(val, self) : val
678
+ # Returns true if the property has a default value
679
+ #
680
+ # @return [Boolean]
681
+ # true if the property has a default value
682
+ #
683
+ # @api semipublic
684
+ def default?
685
+ @options.key?(:default)
509
686
  end
510
687
 
511
- def inspect
512
- "#<Property:#{@model}:#{@name}>"
688
+ # Returns given value unchanged for core types and
689
+ # uses +dump+ method of the property type for custom types.
690
+ #
691
+ # @param [Object] value
692
+ # the value to be converted into a storeable (ie., primitive) value
693
+ #
694
+ # @return [Object]
695
+ # the primitive value to be stored in the repository for +val+
696
+ #
697
+ # @api semipublic
698
+ def value(value)
699
+ if custom?
700
+ type.dump(value, self)
701
+ else
702
+ value
703
+ end
513
704
  end
514
705
 
515
- # TODO: add docs
516
- # @api private
517
- def _dump(*)
518
- Marshal.dump([ repository, model, name ])
706
+ # Test the value to see if it is a valid value for this Property
707
+ #
708
+ # @param [Object] value
709
+ # the value to be tested
710
+ #
711
+ # @return [Boolean]
712
+ # true if the value is valid
713
+ #
714
+ # @api semipulic
715
+ def valid?(value)
716
+ value = self.value(value)
717
+ primitive?(value) || (value.nil? && nullable?)
519
718
  end
520
719
 
521
- # TODO: add docs
522
- # @api private
523
- def self._load(marshalled)
524
- repository, model, name = Marshal.load(marshalled)
525
- model.properties(repository.name)[name]
720
+ # Returns a concise string representation of the property instance.
721
+ #
722
+ # @return [String]
723
+ # Concise string representation of the property instance.
724
+ #
725
+ # @api public
726
+ def inspect
727
+ "#<#{self.class.name} @model=#{model.inspect} @name=#{name.inspect}>"
728
+ end
729
+
730
+ # Test a value to see if it matches the primitive type
731
+ #
732
+ # @param [Object] value
733
+ # value to test
734
+ #
735
+ # @return [Boolean]
736
+ # true if the value is the correct type
737
+ #
738
+ # @api semipublic
739
+ def primitive?(value)
740
+ if primitive == TrueClass
741
+ value == true || value == false
742
+ else
743
+ value.kind_of?(primitive)
744
+ end
526
745
  end
527
746
 
528
747
  private
529
748
 
749
+ # TODO: document
750
+ # @api semipublic
530
751
  def initialize(model, name, type, options = {})
531
- assert_kind_of 'model', model, Model
532
- assert_kind_of 'name', name, Symbol
533
- assert_kind_of 'type', type, Class
534
-
535
- if Fixnum == type
536
- # It was decided that Integer is a more expressively names class to
537
- # use instead of Fixnum. Fixnum only represents smaller numbers,
538
- # so there was some confusion over whether or not it would also
539
- # work with Bignum too (it will). Any Integer, which includes
540
- # Fixnum and Bignum, can be stored in this property.
541
- warn "#{type} properties are deprecated. Please use Integer instead"
542
- type = Integer
752
+ assert_kind_of 'model', model, Model
753
+ assert_kind_of 'name', name, Symbol
754
+ assert_kind_of 'type', type, Class, Module
755
+ assert_kind_of 'options', options, Hash
756
+
757
+ options = options.dup
758
+
759
+ if TrueClass == type
760
+ warn "#{type} is deprecated, use Boolean instead at #{caller[2]}"
761
+ type = Types::Boolean
762
+ elsif Integer == type && options.delete(:serial)
763
+ warn "#{type} with explicit :serial option is deprecated, use Serial instead (#{caller[2]})"
764
+ type = Types::Serial
765
+ elsif options.key?(:size)
766
+ if String == type
767
+ warn ":size option is deprecated, use #{type} with :length instead (#{caller[2]})"
768
+ length = options.delete(:size)
769
+ options[:length] = length unless options.key?(:length)
770
+ elsif Numeric > type
771
+ warn ":size option is deprecated, specify :min and :max instead (#{caller[2]})"
772
+ end
543
773
  end
544
774
 
545
- unless TYPES.include?(type) || (DataMapper::Type > type && TYPES.include?(type.primitive))
546
- raise ArgumentError, "+type+ was #{type.inspect}, which is not a supported type: #{TYPES * ', '}", caller
775
+ assert_valid_options(options)
776
+
777
+ # if the type can be found within Types then
778
+ # use that class rather than the primitive
779
+ unless type.name.blank?
780
+ type = Types.find_const(type.name)
547
781
  end
548
782
 
549
- @extra_options = {}
550
- (options.keys - PROPERTY_OPTIONS).each do |key|
551
- @extra_options[key] = options.delete(key)
783
+ unless PRIMITIVES.include?(type) || (Type > type && PRIMITIVES.include?(type.primitive))
784
+ raise ArgumentError, "+type+ was #{type.inspect}, which is not a supported type"
552
785
  end
553
786
 
787
+ @repository_name = model.repository_name
554
788
  @model = model
555
789
  @name = name.to_s.sub(/\?$/, '').to_sym
556
790
  @type = type
557
- @custom = DataMapper::Type > @type
558
- @options = @custom ? @type.options.merge(options) : options
559
- @instance_variable_name = "@#{@name}"
791
+ @custom = Type > @type
792
+ @options = (@custom ? @type.options.merge(options) : options.dup).freeze
793
+ @instance_variable_name = "@#{@name}".freeze
560
794
 
561
- # TODO: This default should move to a DataMapper::Types::Text
562
- # Custom-Type and out of Property.
563
- @primitive = @options.fetch(:primitive, @type.respond_to?(:primitive) ? @type.primitive : @type)
795
+ @primitive = @type.respond_to?(:primitive) ? @type.primitive : @type
796
+ @field = @options[:field].freeze
797
+ @default = @options[:default]
564
798
 
565
- @getter = TrueClass == @primitive ? "#{@name}?".to_sym : @name
566
- @field = @options.fetch(:field, nil)
567
799
  @serial = @options.fetch(:serial, false)
568
800
  @key = @options.fetch(:key, @serial || false)
569
- @default = @options.fetch(:default, nil)
570
801
  @nullable = @options.fetch(:nullable, @key == false)
571
- @index = @options.fetch(:index, false)
572
- @unique_index = @options.fetch(:unique_index, false)
802
+ @index = @options.fetch(:index, nil)
803
+ @unique_index = @options.fetch(:unique_index, nil)
804
+ @unique = @options.fetch(:unique, @serial || @key || false)
573
805
  @lazy = @options.fetch(:lazy, @type.respond_to?(:lazy) ? @type.lazy : false) && !@key
574
- @fields = {}
575
-
576
- @track = @options.fetch(:track) do
577
- if @custom && @type.respond_to?(:track) && @type.track
578
- @type.track
579
- else
580
- IMMUTABLE_TYPES.include?(@primitive) ? :set : :get
581
- end
582
- end
583
806
 
584
807
  # assign attributes per-type
585
808
  if String == @primitive || Class == @primitive
586
- @length = @options.fetch(:length, @options.fetch(:size, DEFAULT_LENGTH))
809
+ @length = @options.fetch(:length, DEFAULT_LENGTH)
587
810
  elsif BigDecimal == @primitive || Float == @primitive
588
811
  @precision = @options.fetch(:precision, DEFAULT_PRECISION)
589
-
590
- default_scale = (Float == @primitive) ? DEFAULT_SCALE_FLOAT : DEFAULT_SCALE_BIGDECIMAL
591
- @scale = @options.fetch(:scale, default_scale)
592
- # @scale = @options.fetch(:scale, DEFAULT_SCALE_BIGDECIMAL)
812
+ @scale = @options.fetch(:scale, Float == @primitive ? DEFAULT_SCALE_FLOAT : DEFAULT_SCALE_BIGDECIMAL)
593
813
 
594
814
  unless @precision > 0
595
815
  raise ArgumentError, "precision must be greater than 0, but was #{@precision.inspect}"
596
816
  end
597
817
 
598
- if (BigDecimal == @primitive) || (Float == @primitive && !@scale.nil?)
818
+ unless Float == @primitive && @scale.nil?
599
819
  unless @scale >= 0
600
820
  raise ArgumentError, "scale must be equal to or greater than 0, but was #{@scale.inspect}"
601
821
  end
@@ -606,71 +826,324 @@ module DataMapper
606
826
  end
607
827
  end
608
828
 
829
+ if Numeric > @primitive && (@options.keys & [ :min, :max ]).any?
830
+ @min = @options.fetch(:min, DEFAULT_NUMERIC_MIN)
831
+ @max = @options.fetch(:max, DEFAULT_NUMERIC_MAX)
832
+
833
+ if @max < DEFAULT_NUMERIC_MIN && !@options.key?(:min)
834
+ raise ArgumentError, "min should be specified when the max is less than #{DEFAULT_NUMERIC_MIN}"
835
+ elsif @max < @min
836
+ raise ArgumentError, "max must be less than the min, but was #{@max} while the min was #{@min}"
837
+ end
838
+ end
839
+
609
840
  determine_visibility
610
841
 
611
- @model.auto_generate_validations(self) if @model.respond_to?(:auto_generate_validations)
612
- @model.property_serialization_setup(self) if @model.respond_to?(:property_serialization_setup)
842
+ if custom?
843
+ type.bind(self)
844
+ end
845
+
846
+ # comes from dm-validations
847
+ @model.auto_generate_validations(self) if @model.respond_to?(:auto_generate_validations)
613
848
  end
614
849
 
615
- def determine_visibility # :nodoc:
850
+ # TODO: document
851
+ # @api private
852
+ def assert_valid_options(options)
853
+ if (unknown_keys = options.keys - OPTIONS).any?
854
+ raise ArgumentError, "options #{unknown_keys.map { |key| key.inspect }.join(' and ')} are unknown"
855
+ end
856
+
857
+ options.each do |key, value|
858
+ case key
859
+ when :field
860
+ assert_kind_of "options[#{key.inspect}]", value, String
861
+
862
+ when :default
863
+ if value.nil?
864
+ raise ArgumentError, "options[#{key.inspect}] must not be nil"
865
+ end
866
+
867
+ when :serial, :key, :nullable, :unique, :auto_validation
868
+ unless value == true || value == false
869
+ raise ArgumentError, "options[#{key.inspect}] must be either true or false"
870
+ end
871
+
872
+ when :lazy
873
+ unless value == true || value == false || value.kind_of?(Symbol) || (value.kind_of?(Array) && value.all? { |val| val.kind_of?(Symbol) })
874
+ raise ArgumentError, "options[#{key.inspect}] must be either true, false, a Symbol or an Array of Symbols"
875
+ end
876
+
877
+ when :index, :unique_index
878
+ assert_kind_of "options[#{key.inspect}]", value, Symbol, Array, TrueClass
879
+
880
+ when :length
881
+ assert_kind_of "options[#{key.inspect}]", value, Range, Integer
882
+
883
+ when :size, :precision, :scale
884
+ assert_kind_of "options[#{key.inspect}]", value, Integer
885
+
886
+ when :reader, :writer, :accessor
887
+ assert_kind_of "options[#{key.inspect}]", value, Symbol
888
+
889
+ unless VISIBILITY_OPTIONS.include?(value)
890
+ raise ArgumentError, "options[#{key.inspect}] must be #{VISIBILITY_OPTIONS.join(' or ')}"
891
+ end
892
+ end
893
+ end
894
+ end
895
+
896
+ # Assert given visibility value is supported.
897
+ #
898
+ # Will raise ArgumentError if this Property's reader and writer
899
+ # visibilities are not included in VISIBILITY_OPTIONS.
900
+ # @return [NilClass]
901
+ #
902
+ # @raise [ArgumentError] "property visibility must be :public, :protected, or :private"
903
+ #
904
+ # @api private
905
+ def determine_visibility
616
906
  @reader_visibility = @options[:reader] || @options[:accessor] || :public
617
907
  @writer_visibility = @options[:writer] || @options[:accessor] || :public
908
+ end
909
+
910
+
911
+ # Typecast a value to an Integer
912
+ #
913
+ # @param [#to_str, #to_i] value
914
+ # value to typecast
915
+ #
916
+ # @return [Integer]
917
+ # Integer constructed from value
918
+ #
919
+ # @api private
920
+ def typecast_to_integer(value)
921
+ typecast_to_numeric(value, :to_i)
922
+ end
618
923
 
619
- unless VISIBILITY_OPTIONS.include?(@reader_visibility) && VISIBILITY_OPTIONS.include?(@writer_visibility)
620
- raise ArgumentError, 'property visibility must be :public, :protected, or :private', caller(2)
924
+ # Typecast a value to a String
925
+ #
926
+ # @param [#to_s] value
927
+ # value to typecast
928
+ #
929
+ # @return [String]
930
+ # String constructed from value
931
+ #
932
+ # @api private
933
+ def typecast_to_string(value)
934
+ value.to_s
935
+ end
936
+
937
+ # Typecast a value to a true or false
938
+ #
939
+ # @param [Integer, #to_str] value
940
+ # value to typecast
941
+ #
942
+ # @return [Boolean]
943
+ # true or false constructed from value
944
+ #
945
+ # @api private
946
+ def typecast_to_boolean(value)
947
+ if value.kind_of?(Integer)
948
+ return true if value == 1
949
+ return false if value == 0
950
+ elsif value.respond_to?(:to_str)
951
+ return true if %w[ true 1 t ].include?(value.to_str.downcase)
952
+ return false if %w[ false 0 f ].include?(value.to_str.downcase)
621
953
  end
954
+
955
+ value
622
956
  end
623
957
 
624
- # Typecasts an arbitrary value to a DateTime
958
+ # Typecast a value to a BigDecimal
959
+ #
960
+ # @param [#to_str, #to_d, Integer] value
961
+ # value to typecast
962
+ #
963
+ # @return [BigDecimal]
964
+ # BigDecimal constructed from value
965
+ #
966
+ # @api private
967
+ def typecast_to_bigdecimal(value)
968
+ if value.kind_of?(Integer)
969
+ # TODO: remove this case when Integer#to_d added by extlib
970
+ value.to_s.to_d
971
+ else
972
+ typecast_to_numeric(value, :to_d)
973
+ end
974
+ end
975
+
976
+ # Typecast a value to a Float
977
+ #
978
+ # @param [#to_str, #to_f] value
979
+ # value to typecast
980
+ #
981
+ # @return [Float]
982
+ # Float constructed from value
983
+ #
984
+ # @api private
985
+ def typecast_to_float(value)
986
+ typecast_to_numeric(value, :to_f)
987
+ end
988
+
989
+ # Match numeric string
990
+ #
991
+ # @param [#to_str, Numeric] value
992
+ # value to typecast
993
+ # @param [Symbol] method
994
+ # method to typecast with
995
+ #
996
+ # @return [Numeric]
997
+ # number if matched, value if no match
998
+ #
999
+ # @api private
1000
+ def typecast_to_numeric(value, method)
1001
+ if value.respond_to?(:to_str)
1002
+ if value.to_str =~ /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/
1003
+ $1.send(method)
1004
+ else
1005
+ value
1006
+ end
1007
+ elsif value.respond_to?(method)
1008
+ value.send(method)
1009
+ else
1010
+ value
1011
+ end
1012
+ end
1013
+
1014
+ # Typecasts an arbitrary value to a DateTime.
1015
+ # Handles both Hashes and DateTime instances.
1016
+ #
1017
+ # @param [#to_mash, #to_s] value
1018
+ # value to be typecast
1019
+ #
1020
+ # @return [DateTime]
1021
+ # DateTime constructed from value
1022
+ #
1023
+ # @api private
625
1024
  def typecast_to_datetime(value)
626
- case value
627
- when Hash then typecast_hash_to_datetime(value)
628
- else DateTime.parse(value.to_s)
1025
+ if value.respond_to?(:to_mash)
1026
+ typecast_hash_to_datetime(value)
1027
+ else
1028
+ DateTime.parse(value.to_s)
629
1029
  end
1030
+ rescue ArgumentError
1031
+ value
630
1032
  end
631
1033
 
632
1034
  # Typecasts an arbitrary value to a Date
1035
+ # Handles both Hashes and Date instances.
1036
+ #
1037
+ # @param [#to_mash, #to_s] value
1038
+ # value to be typecast
1039
+ #
1040
+ # @return [Date]
1041
+ # Date constructed from value
1042
+ #
1043
+ # @api private
633
1044
  def typecast_to_date(value)
634
- case value
635
- when Hash then typecast_hash_to_date(value)
636
- else Date.parse(value.to_s)
1045
+ if value.respond_to?(:to_mash)
1046
+ typecast_hash_to_date(value)
1047
+ else
1048
+ Date.parse(value.to_s)
637
1049
  end
1050
+ rescue ArgumentError
1051
+ value
638
1052
  end
639
1053
 
640
1054
  # Typecasts an arbitrary value to a Time
1055
+ # Handles both Hashes and Time instances.
1056
+ #
1057
+ # @param [#to_mash, #to_s] value
1058
+ # value to be typecast
1059
+ #
1060
+ # @return [Time]
1061
+ # Time constructed from value
1062
+ #
1063
+ # @api private
641
1064
  def typecast_to_time(value)
642
- case value
643
- when Hash then typecast_hash_to_time(value)
644
- else Time.parse(value.to_s)
1065
+ if value.respond_to?(:to_mash)
1066
+ typecast_hash_to_time(value)
1067
+ else
1068
+ Time.parse(value.to_s)
645
1069
  end
1070
+ rescue ArgumentError
1071
+ value
646
1072
  end
647
1073
 
648
- def typecast_hash_to_datetime(hash)
649
- args = extract_time_args_from_hash(hash, :year, :month, :day, :hour, :min, :sec)
650
- DateTime.new(*args)
651
- rescue ArgumentError => e
652
- t = typecast_hash_to_time(hash)
653
- DateTime.new(t.year, t.month, t.day, t.hour, t.min, t.sec)
1074
+ # Creates a DateTime instance from a Hash with keys :year, :month, :day,
1075
+ # :hour, :min, :sec
1076
+ #
1077
+ # @param [#to_mash] value
1078
+ # value to be typecast
1079
+ #
1080
+ # @return [DateTime]
1081
+ # DateTime constructed from hash
1082
+ #
1083
+ # @api private
1084
+ def typecast_hash_to_datetime(value)
1085
+ DateTime.new(*extract_time(value))
654
1086
  end
655
1087
 
656
- def typecast_hash_to_date(hash)
657
- args = extract_time_args_from_hash(hash, :year, :month, :day)
658
- Date.new(*args)
659
- rescue ArgumentError
660
- t = typecast_hash_to_time(hash)
661
- Date.new(t.year, t.month, t.day)
1088
+ # Creates a Date instance from a Hash with keys :year, :month, :day
1089
+ #
1090
+ # @param [#to_mash] value
1091
+ # value to be typecast
1092
+ #
1093
+ # @return [Date]
1094
+ # Date constructed from hash
1095
+ #
1096
+ # @api private
1097
+ def typecast_hash_to_date(value)
1098
+ Date.new(*extract_time(value)[0, 3])
662
1099
  end
663
1100
 
664
- def typecast_hash_to_time(hash)
665
- args = extract_time_args_from_hash(hash, :year, :month, :day, :hour, :min, :sec)
666
- Time.local(*args)
1101
+ # Creates a Time instance from a Hash with keys :year, :month, :day,
1102
+ # :hour, :min, :sec
1103
+ #
1104
+ # @param [#to_mash] value
1105
+ # value to be typecast
1106
+ #
1107
+ # @return [Time]
1108
+ # Time constructed from hash
1109
+ #
1110
+ # @api private
1111
+ def typecast_hash_to_time(value)
1112
+ Time.local(*extract_time(value))
667
1113
  end
668
1114
 
669
1115
  # Extracts the given args from the hash. If a value does not exist, it
670
- # uses the value of Time.now
671
- def extract_time_args_from_hash(hash, *args)
672
- now = Time.now
673
- args.map { |arg| hash[arg] || hash[arg.to_s] || now.send(arg) }
1116
+ # uses the value of Time.now.
1117
+ #
1118
+ # @param [#to_mash] value
1119
+ # value to extract time args from
1120
+ #
1121
+ # @return [Array]
1122
+ # Extracted values
1123
+ #
1124
+ # @api private
1125
+ def extract_time(value)
1126
+ mash = value.to_mash
1127
+ now = Time.now
1128
+
1129
+ [ :year, :month, :day, :hour, :min, :sec ].map do |segment|
1130
+ typecast_to_numeric(mash.fetch(segment, now.send(segment)), :to_i)
1131
+ end
1132
+ end
1133
+
1134
+ # Typecast a value to a Class
1135
+ #
1136
+ # @param [#to_s] value
1137
+ # value to typecast
1138
+ #
1139
+ # @return [Class]
1140
+ # Class constructed from value
1141
+ #
1142
+ # @api private
1143
+ def typecast_to_class(value)
1144
+ model.find_const(value.to_s)
1145
+ rescue NameError
1146
+ value
674
1147
  end
675
1148
  end # class Property
676
1149
  end # module DataMapper