datamapper-dm-core 0.9.11 → 0.10.0

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