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 (194) hide show
  1. data/.autotest +17 -14
  2. data/.gitignore +3 -1
  3. data/FAQ +6 -5
  4. data/History.txt +5 -50
  5. data/Manifest.txt +66 -76
  6. data/QUICKLINKS +1 -1
  7. data/README.txt +21 -15
  8. data/Rakefile +6 -7
  9. data/SPECS +2 -29
  10. data/TODO +1 -1
  11. data/deps.rip +2 -0
  12. data/dm-core.gemspec +11 -15
  13. data/lib/dm-core.rb +105 -110
  14. data/lib/dm-core/adapters.rb +135 -16
  15. data/lib/dm-core/adapters/abstract_adapter.rb +251 -181
  16. data/lib/dm-core/adapters/data_objects_adapter.rb +482 -534
  17. data/lib/dm-core/adapters/in_memory_adapter.rb +90 -69
  18. data/lib/dm-core/adapters/mysql_adapter.rb +22 -115
  19. data/lib/dm-core/adapters/oracle_adapter.rb +249 -0
  20. data/lib/dm-core/adapters/postgres_adapter.rb +7 -173
  21. data/lib/dm-core/adapters/sqlite3_adapter.rb +4 -97
  22. data/lib/dm-core/adapters/yaml_adapter.rb +116 -0
  23. data/lib/dm-core/associations/many_to_many.rb +372 -90
  24. data/lib/dm-core/associations/many_to_one.rb +220 -73
  25. data/lib/dm-core/associations/one_to_many.rb +319 -255
  26. data/lib/dm-core/associations/one_to_one.rb +66 -53
  27. data/lib/dm-core/associations/relationship.rb +561 -156
  28. data/lib/dm-core/collection.rb +1101 -379
  29. data/lib/dm-core/core_ext/kernel.rb +12 -0
  30. data/lib/dm-core/core_ext/symbol.rb +10 -0
  31. data/lib/dm-core/identity_map.rb +4 -34
  32. data/lib/dm-core/migrations.rb +1283 -0
  33. data/lib/dm-core/model.rb +570 -369
  34. data/lib/dm-core/model/descendant_set.rb +81 -0
  35. data/lib/dm-core/model/hook.rb +45 -0
  36. data/lib/dm-core/model/is.rb +32 -0
  37. data/lib/dm-core/model/property.rb +247 -0
  38. data/lib/dm-core/model/relationship.rb +335 -0
  39. data/lib/dm-core/model/scope.rb +90 -0
  40. data/lib/dm-core/property.rb +808 -273
  41. data/lib/dm-core/property_set.rb +141 -98
  42. data/lib/dm-core/query.rb +1037 -483
  43. data/lib/dm-core/query/conditions/comparison.rb +872 -0
  44. data/lib/dm-core/query/conditions/operation.rb +221 -0
  45. data/lib/dm-core/query/direction.rb +43 -0
  46. data/lib/dm-core/query/operator.rb +84 -0
  47. data/lib/dm-core/query/path.rb +138 -0
  48. data/lib/dm-core/query/sort.rb +45 -0
  49. data/lib/dm-core/repository.rb +210 -94
  50. data/lib/dm-core/resource.rb +641 -421
  51. data/lib/dm-core/spec/adapter_shared_spec.rb +294 -0
  52. data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +106 -0
  53. data/lib/dm-core/support/chainable.rb +22 -0
  54. data/lib/dm-core/support/deprecate.rb +12 -0
  55. data/lib/dm-core/support/logger.rb +13 -0
  56. data/lib/dm-core/{naming_conventions.rb → support/naming_conventions.rb} +6 -6
  57. data/lib/dm-core/transaction.rb +333 -92
  58. data/lib/dm-core/type.rb +98 -60
  59. data/lib/dm-core/types/boolean.rb +1 -1
  60. data/lib/dm-core/types/discriminator.rb +34 -20
  61. data/lib/dm-core/types/object.rb +7 -4
  62. data/lib/dm-core/types/paranoid_boolean.rb +11 -9
  63. data/lib/dm-core/types/paranoid_datetime.rb +11 -9
  64. data/lib/dm-core/types/serial.rb +3 -3
  65. data/lib/dm-core/types/text.rb +3 -4
  66. data/lib/dm-core/version.rb +1 -1
  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/migrations_spec.rb +359 -0
  80. data/spec/public/model/relationship_spec.rb +924 -0
  81. data/spec/public/model_spec.rb +159 -0
  82. data/spec/public/property_spec.rb +829 -0
  83. data/spec/public/resource_spec.rb +71 -0
  84. data/spec/public/sel_spec.rb +44 -0
  85. data/spec/public/setup_spec.rb +145 -0
  86. data/spec/public/shared/association_collection_shared_spec.rb +317 -0
  87. data/spec/public/shared/collection_shared_spec.rb +1670 -0
  88. data/spec/public/shared/finder_shared_spec.rb +1619 -0
  89. data/spec/public/shared/resource_shared_spec.rb +924 -0
  90. data/spec/public/shared/sel_shared_spec.rb +112 -0
  91. data/spec/public/transaction_spec.rb +129 -0
  92. data/spec/public/types/discriminator_spec.rb +130 -0
  93. data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
  94. data/spec/semipublic/adapters/in_memory_adapter_spec.rb +12 -0
  95. data/spec/semipublic/adapters/mysql_adapter_spec.rb +17 -0
  96. data/spec/semipublic/adapters/oracle_adapter_spec.rb +194 -0
  97. data/spec/semipublic/adapters/postgres_adapter_spec.rb +17 -0
  98. data/spec/semipublic/adapters/sqlite3_adapter_spec.rb +17 -0
  99. data/spec/semipublic/adapters/yaml_adapter_spec.rb +12 -0
  100. data/spec/semipublic/associations/many_to_one_spec.rb +53 -0
  101. data/spec/semipublic/associations/relationship_spec.rb +194 -0
  102. data/spec/semipublic/associations_spec.rb +177 -0
  103. data/spec/semipublic/collection_spec.rb +142 -0
  104. data/spec/semipublic/property_spec.rb +61 -0
  105. data/spec/semipublic/query/conditions_spec.rb +528 -0
  106. data/spec/semipublic/query/path_spec.rb +443 -0
  107. data/spec/semipublic/query_spec.rb +2626 -0
  108. data/spec/semipublic/resource_spec.rb +47 -0
  109. data/spec/semipublic/shared/condition_shared_spec.rb +9 -0
  110. data/spec/semipublic/shared/resource_shared_spec.rb +126 -0
  111. data/spec/spec.opts +3 -1
  112. data/spec/spec_helper.rb +80 -57
  113. data/tasks/ci.rb +19 -31
  114. data/tasks/dm.rb +43 -48
  115. data/tasks/doc.rb +8 -11
  116. data/tasks/gemspec.rb +5 -5
  117. data/tasks/hoe.rb +15 -16
  118. data/tasks/install.rb +8 -10
  119. metadata +74 -111
  120. data/lib/dm-core/associations.rb +0 -207
  121. data/lib/dm-core/associations/relationship_chain.rb +0 -81
  122. data/lib/dm-core/auto_migrations.rb +0 -105
  123. data/lib/dm-core/dependency_queue.rb +0 -32
  124. data/lib/dm-core/hook.rb +0 -11
  125. data/lib/dm-core/is.rb +0 -16
  126. data/lib/dm-core/logger.rb +0 -232
  127. data/lib/dm-core/migrations/destructive_migrations.rb +0 -17
  128. data/lib/dm-core/migrator.rb +0 -29
  129. data/lib/dm-core/scope.rb +0 -58
  130. data/lib/dm-core/support.rb +0 -7
  131. data/lib/dm-core/support/array.rb +0 -13
  132. data/lib/dm-core/support/assertions.rb +0 -8
  133. data/lib/dm-core/support/errors.rb +0 -23
  134. data/lib/dm-core/support/kernel.rb +0 -11
  135. data/lib/dm-core/support/symbol.rb +0 -41
  136. data/lib/dm-core/type_map.rb +0 -80
  137. data/lib/dm-core/types.rb +0 -19
  138. data/script/all +0 -4
  139. data/spec/integration/association_spec.rb +0 -1382
  140. data/spec/integration/association_through_spec.rb +0 -203
  141. data/spec/integration/associations/many_to_many_spec.rb +0 -449
  142. data/spec/integration/associations/many_to_one_spec.rb +0 -163
  143. data/spec/integration/associations/one_to_many_spec.rb +0 -188
  144. data/spec/integration/auto_migrations_spec.rb +0 -413
  145. data/spec/integration/collection_spec.rb +0 -1073
  146. data/spec/integration/data_objects_adapter_spec.rb +0 -32
  147. data/spec/integration/dependency_queue_spec.rb +0 -46
  148. data/spec/integration/model_spec.rb +0 -197
  149. data/spec/integration/mysql_adapter_spec.rb +0 -85
  150. data/spec/integration/postgres_adapter_spec.rb +0 -731
  151. data/spec/integration/property_spec.rb +0 -253
  152. data/spec/integration/query_spec.rb +0 -514
  153. data/spec/integration/repository_spec.rb +0 -61
  154. data/spec/integration/resource_spec.rb +0 -513
  155. data/spec/integration/sqlite3_adapter_spec.rb +0 -352
  156. data/spec/integration/sti_spec.rb +0 -273
  157. data/spec/integration/strategic_eager_loading_spec.rb +0 -156
  158. data/spec/integration/transaction_spec.rb +0 -75
  159. data/spec/integration/type_spec.rb +0 -275
  160. data/spec/lib/logging_helper.rb +0 -18
  161. data/spec/lib/mock_adapter.rb +0 -27
  162. data/spec/lib/model_loader.rb +0 -100
  163. data/spec/lib/publicize_methods.rb +0 -28
  164. data/spec/models/content.rb +0 -16
  165. data/spec/models/vehicles.rb +0 -34
  166. data/spec/models/zoo.rb +0 -48
  167. data/spec/unit/adapters/abstract_adapter_spec.rb +0 -133
  168. data/spec/unit/adapters/adapter_shared_spec.rb +0 -15
  169. data/spec/unit/adapters/data_objects_adapter_spec.rb +0 -632
  170. data/spec/unit/adapters/in_memory_adapter_spec.rb +0 -98
  171. data/spec/unit/adapters/postgres_adapter_spec.rb +0 -133
  172. data/spec/unit/associations/many_to_many_spec.rb +0 -32
  173. data/spec/unit/associations/many_to_one_spec.rb +0 -159
  174. data/spec/unit/associations/one_to_many_spec.rb +0 -393
  175. data/spec/unit/associations/one_to_one_spec.rb +0 -7
  176. data/spec/unit/associations/relationship_spec.rb +0 -71
  177. data/spec/unit/associations_spec.rb +0 -242
  178. data/spec/unit/auto_migrations_spec.rb +0 -111
  179. data/spec/unit/collection_spec.rb +0 -182
  180. data/spec/unit/data_mapper_spec.rb +0 -35
  181. data/spec/unit/identity_map_spec.rb +0 -126
  182. data/spec/unit/is_spec.rb +0 -80
  183. data/spec/unit/migrator_spec.rb +0 -33
  184. data/spec/unit/model_spec.rb +0 -321
  185. data/spec/unit/naming_conventions_spec.rb +0 -36
  186. data/spec/unit/property_set_spec.rb +0 -90
  187. data/spec/unit/property_spec.rb +0 -753
  188. data/spec/unit/query_spec.rb +0 -571
  189. data/spec/unit/repository_spec.rb +0 -93
  190. data/spec/unit/resource_spec.rb +0 -649
  191. data/spec/unit/scope_spec.rb +0 -142
  192. data/spec/unit/transaction_spec.rb +0 -493
  193. data/spec/unit/type_map_spec.rb +0 -114
  194. data/spec/unit/type_spec.rb +0 -119
@@ -0,0 +1,90 @@
1
+ module DataMapper
2
+ module Model
3
+ # Module with query scoping functionality.
4
+ #
5
+ # Scopes are implemented using simple array based
6
+ # stack that is thread local. Default scope can be set
7
+ # on a per repository basis.
8
+ #
9
+ # Scopes are merged as new queries are nested.
10
+ # It is also possible to get exclusive scope access
11
+ # using +with_exclusive_scope+
12
+ module Scope
13
+ # TODO: document
14
+ # @api private
15
+ def default_scope(repository_name = default_repository_name)
16
+ @default_scope ||= {}
17
+
18
+ @default_scope[repository_name] ||= if repository_name == default_repository_name
19
+ {}
20
+ else
21
+ default_scope(default_repository_name).dup
22
+ end
23
+ end
24
+
25
+ # Returns query on top of scope stack
26
+ #
27
+ # @api private
28
+ def query
29
+ Query.new(repository, self, current_scope).freeze
30
+ end
31
+
32
+ # TODO: document
33
+ # @api private
34
+ def current_scope
35
+ scope_stack.last || default_scope(repository.name)
36
+ end
37
+
38
+ protected
39
+
40
+ # Pushes given query on top of the stack
41
+ #
42
+ # @param [Hash, Query] Query to add to current scope nesting
43
+ #
44
+ # @api private
45
+ def with_scope(query)
46
+ options = if query.kind_of?(Hash)
47
+ query
48
+ else
49
+ query.options
50
+ end
51
+
52
+ # merge the current scope with the passed in query
53
+ with_exclusive_scope(self.query.merge(options)) { |*block_args| yield(*block_args) }
54
+ end
55
+
56
+ # Pushes given query on top of scope stack and yields
57
+ # given block, then pops the stack. During block execution
58
+ # queries previously pushed onto the stack
59
+ # have no effect.
60
+ #
61
+ # @api private
62
+ def with_exclusive_scope(query)
63
+ query = if query.kind_of?(Hash)
64
+ Query.new(repository, self, query)
65
+ else
66
+ query.dup
67
+ end
68
+
69
+ scope_stack << query.options
70
+
71
+ begin
72
+ yield query.freeze
73
+ ensure
74
+ scope_stack.pop
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ # Initializes (if necessary) and returns current scope stack
81
+ # @api private
82
+ def scope_stack
83
+ scope_stack_for = Thread.current[:dm_scope_stack] ||= {}
84
+ scope_stack_for[self] ||= []
85
+ end
86
+ end # module Scope
87
+
88
+ include Scope
89
+ end # module Model
90
+ end # module DataMapper
@@ -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,24 @@ 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
253
296
 
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
297
+ deprecate :unique, :unique?
298
+ deprecate :size, :length
299
+
300
+ # NOTE: PLEASE update OPTIONS in DataMapper::Type when updating
259
301
  # them here
260
- PROPERTY_OPTIONS = [
302
+ OPTIONS = [
261
303
  :accessor, :reader, :writer,
262
304
  :lazy, :default, :nullable, :key, :serial, :field, :size, :length,
263
- :format, :index, :unique_index, :check, :ordinal, :auto_validation,
264
- :validates, :unique, :track, :precision, :scale
305
+ :format, :index, :unique_index, :auto_validation,
306
+ :validates, :unique, :precision, :scale, :min, :max
265
307
  ]
266
308
 
267
- # FIXME: can we pull the keys from
268
- # DataMapper::Adapters::DataObjectsAdapter::TYPES
269
- # for this?
270
- TYPES = [
309
+ PRIMITIVES = [
271
310
  TrueClass,
272
311
  String,
273
- DataMapper::Types::Text,
274
312
  Float,
275
313
  Integer,
276
314
  BigDecimal,
@@ -279,71 +317,163 @@ module DataMapper
279
317
  Time,
280
318
  Object,
281
319
  Class,
282
- DataMapper::Types::Discriminator,
283
- DataMapper::Types::Serial
284
- ]
285
-
286
- IMMUTABLE_TYPES = [ TrueClass, Float, Integer, BigDecimal]
320
+ ].to_set.freeze
287
321
 
288
- VISIBILITY_OPTIONS = [ :public, :protected, :private ]
322
+ # Possible :visibility option values
323
+ VISIBILITY_OPTIONS = [ :public, :protected, :private ].to_set.freeze
289
324
 
290
- DEFAULT_LENGTH = 50
291
- DEFAULT_PRECISION = 10
292
- DEFAULT_SCALE_BIGDECIMAL = 0
293
- DEFAULT_SCALE_FLOAT = nil
325
+ DEFAULT_LENGTH = 50
326
+ DEFAULT_PRECISION = 10
327
+ DEFAULT_SCALE_BIGDECIMAL = 0 # Default scale for BigDecimal type
328
+ DEFAULT_SCALE_FLOAT = nil # Default scale for Float type
329
+ DEFAULT_NUMERIC_MIN = 0
330
+ DEFAULT_NUMERIC_MAX = 2**31-1
294
331
 
295
332
  attr_reader :primitive, :model, :name, :instance_variable_name,
296
- :type, :reader_visibility, :writer_visibility, :getter, :options,
297
- :default, :precision, :scale, :track, :extra_options
333
+ :type, :reader_visibility, :writer_visibility, :options,
334
+ :default, :precision, :scale, :min, :max, :repository_name
298
335
 
299
336
  # Supplies the field in the data-store which the property corresponds to
300
337
  #
301
- # @return <String> name of field in data-store
302
- # -
303
- # @api semi-public
338
+ # @return [String] name of field in data-store
339
+ #
340
+ # @api semipublic
304
341
  def field(repository_name = nil)
305
- @field || @fields[repository_name] ||= self.model.field_naming_convention(repository_name).call(self)
342
+ if repository_name
343
+ warn "Passing in +repository_name+ to #{self.class}#field is deprecated (#{caller[0]})"
344
+
345
+ if repository_name != self.repository_name
346
+ raise ArgumentError, "Mismatching +repository_name+ with #{self.class}#repository_name (#{repository_name.inspect} != #{self.repository_name.inspect})"
347
+ end
348
+ end
349
+
350
+ # defer setting the field with the adapter specific naming
351
+ # conventions until after the adapter has been setup
352
+ @field ||= model.field_naming_convention(self.repository_name).call(self).freeze
306
353
  end
307
354
 
308
- def unique
309
- @unique ||= @options.fetch(:unique, @serial || @key || false)
355
+ # Returns true if property is unique. Serial properties and keys
356
+ # are unique by default.
357
+ #
358
+ # @return [Boolean]
359
+ # true if property has uniq index defined, false otherwise
360
+ #
361
+ # @api public
362
+ def unique?
363
+ @unique
310
364
  end
311
365
 
312
- def hash
313
- if @custom && !@bound
314
- @type.bind(self)
315
- @bound = true
366
+ # Compares another Property for equivalency
367
+ #
368
+ # TODO: needs example
369
+ #
370
+ # @param [Property] other
371
+ # the other Property to compare with
372
+ #
373
+ # @return [Boolean]
374
+ # true if they are equivalent, false if not
375
+ #
376
+ # @api semipublic
377
+ def ==(other)
378
+ if equal?(other)
379
+ return true
316
380
  end
317
381
 
318
- return @model.hash + @name.hash
382
+ unless other.respond_to?(:model)
383
+ return false
384
+ end
385
+
386
+ unless other.respond_to?(:name)
387
+ return false
388
+ end
389
+
390
+ cmp?(other, :==)
319
391
  end
320
392
 
321
- def eql?(o)
322
- if o.is_a?(Property)
323
- return o.model == @model && o.name == @name
324
- else
393
+ # Compares another Property for equality
394
+ #
395
+ # TODO: needs example
396
+ #
397
+ # @param [Property] other
398
+ # the other Property to compare with
399
+ #
400
+ # @return [Boolean]
401
+ # true if they are equal, false if not
402
+ #
403
+ # @api semipublic
404
+ def eql?(other)
405
+ if equal?(other)
406
+ return true
407
+ end
408
+
409
+ unless instance_of?(other.class)
325
410
  return false
326
411
  end
412
+
413
+ cmp?(other, :eql?)
414
+ end
415
+
416
+ # Returns the hash of the property name
417
+ #
418
+ # This is necessary to allow comparisons between different properties
419
+ # in different models, having the same base model
420
+ #
421
+ # @return [Integer]
422
+ # the property name hash
423
+ #
424
+ # @api semipublic
425
+ def hash
426
+ name.hash
327
427
  end
328
428
 
429
+ # Returns maximum property length (if applicable).
430
+ # This usually only makes sense when property is of
431
+ # type Range or custom type.
432
+ #
433
+ # @return [Integer, NilClass]
434
+ # the maximum length of this property
435
+ #
436
+ # @api semipublic
329
437
  def length
330
- @length.is_a?(Range) ? @length.max : @length
438
+ if @length.kind_of?(Range)
439
+ @length.max
440
+ else
441
+ @length
442
+ end
331
443
  end
332
- alias size length
333
444
 
445
+ # Returns index name if property has index.
446
+ #
447
+ # @return [true, Symbol, Array, nil]
448
+ # returns true if property is indexed by itself
449
+ # returns a Symbol if the property is indexed with other properties
450
+ # returns an Array if the property belongs to multiple indexes
451
+ # returns nil if the property does not belong to any indexes
452
+ #
453
+ # @api public
334
454
  def index
335
455
  @index
336
456
  end
337
457
 
458
+ # Returns true if property has unique index. Serial properties and
459
+ # keys are unique by default.
460
+ #
461
+ # @return [true, Symbol, Array, nil]
462
+ # returns true if property is indexed by itself
463
+ # returns a Symbol if the property is indexed with other properties
464
+ # returns an Array if the property belongs to multiple indexes
465
+ # returns nil if the property does not belong to any indexes
466
+ #
467
+ # @api public
338
468
  def unique_index
339
469
  @unique_index
340
470
  end
341
471
 
342
472
  # Returns whether or not the property is to be lazy-loaded
343
473
  #
344
- # @return <TrueClass, FalseClass> whether or not the property is to be
345
- # lazy-loaded
346
- # -
474
+ # @return [Boolean]
475
+ # true if the property is to be lazy-loaded
476
+ #
347
477
  # @api public
348
478
  def lazy?
349
479
  @lazy
@@ -351,9 +481,9 @@ module DataMapper
351
481
 
352
482
  # Returns whether or not the property is a key or a part of a key
353
483
  #
354
- # @return <TrueClass, FalseClass> whether the property is a key or a part of
355
- # a key
356
- #-
484
+ # @return [Boolean]
485
+ # true if the property is a key or a part of a key
486
+ #
357
487
  # @api public
358
488
  def key?
359
489
  @key
@@ -361,8 +491,9 @@ module DataMapper
361
491
 
362
492
  # Returns whether or not the property is "serial" (auto-incrementing)
363
493
  #
364
- # @return <TrueClass, FalseClass> whether or not the property is "serial"
365
- #-
494
+ # @return [Boolean]
495
+ # whether or not the property is "serial"
496
+ #
366
497
  # @api public
367
498
  def serial?
368
499
  @serial
@@ -370,232 +501,360 @@ module DataMapper
370
501
 
371
502
  # Returns whether or not the property can accept 'nil' as it's value
372
503
  #
373
- # @return <TrueClass, FalseClass> whether or not the property can accept 'nil'
374
- #-
504
+ # @return [Boolean]
505
+ # whether or not the property can accept 'nil'
506
+ #
375
507
  # @api public
376
508
  def nullable?
377
509
  @nullable
378
510
  end
379
511
 
512
+ # Returns whether or not the property is custom (not provided by dm-core)
513
+ #
514
+ # @return [Boolean]
515
+ # whether or not the property is custom
516
+ #
517
+ # @api public
380
518
  def custom?
381
519
  @custom
382
520
  end
383
521
 
384
- # Provides a standardized getter method for the property
522
+ # Standardized reader method for the property
523
+ #
524
+ # @param [Resource] resource
525
+ # model instance for which this property is to be loaded
526
+ #
527
+ # @return [Object]
528
+ # the value of this property for the provided instance
529
+ #
530
+ # @raise [ArgumentError] "+resource+ should be a Resource, but was ...."
385
531
  #
386
- # @raise <ArgumentError> "+resource+ should be a DataMapper::Resource, but was ...."
387
- #-
388
532
  # @api private
389
533
  def get(resource)
390
- lazy_load(resource)
391
-
392
- value = get!(resource)
534
+ lazy_load(resource) unless loaded?(resource) || resource.new?
393
535
 
394
- set_original_value(resource, value)
395
-
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)
536
+ if loaded?(resource)
537
+ get!(resource)
538
+ else
539
+ set(resource, default? ? default_for(resource) : nil)
401
540
  end
402
-
403
- value
404
541
  end
405
542
 
543
+ # Fetch the ivar value in the resource
544
+ #
545
+ # @param [Resource] resource
546
+ # model instance for which this property is to be unsafely loaded
547
+ #
548
+ # @return [Object]
549
+ # current @ivar value of this property in +resource+
550
+ #
551
+ # @api private
406
552
  def get!(resource)
407
553
  resource.instance_variable_get(instance_variable_name)
408
554
  end
409
555
 
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
556
+ # Sets original value of the property on given resource.
557
+ # When property is set on DataMapper resource instance,
558
+ # original value is preserved. This makes possible to
559
+ # track dirty attributes and save only those really changed,
560
+ # and avoid extra queries to the data source in certain
561
+ # situations.
562
+ #
563
+ # @param [Resource] resource
564
+ # model instance for which to set the original value
565
+ # @param [Object] original
566
+ # value to set as original value for this property in +resource+
567
+ #
568
+ # @api private
569
+ def set_original_value(resource, original)
570
+ original_attributes = resource.original_attributes
571
+ original = self.value(original)
572
+
573
+ if original_attributes.key?(self)
574
+ # stop tracking the value if it has not changed
575
+ original_attributes.delete(self) if original == original_attributes[self] && resource.saved?
576
+ else
577
+ original_attributes[self] = original
415
578
  end
416
579
  end
417
580
 
418
581
  # Provides a standardized setter method for the property
419
582
  #
420
- # @raise <ArgumentError> "+resource+ should be a DataMapper::Resource, but was ...."
421
- #-
583
+ # @param [Resource] resource
584
+ # the resource to get the value from
585
+ # @param [Object] value
586
+ # the value to set in the resource
587
+ #
588
+ # @return [Object]
589
+ # +value+ after being typecasted according to this property's primitive
590
+ #
591
+ # @raise [ArgumentError] "+resource+ should be a Resource, but was ...."
592
+ #
422
593
  # @api private
423
594
  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)
595
+ loaded = loaded?(resource)
596
+ original = get!(resource) if loaded
597
+ value = typecast(value)
431
598
 
432
- new_value = typecast(value)
433
- old_value = get!(resource)
599
+ if loaded && value == original
600
+ return original
601
+ end
434
602
 
435
- set_original_value(resource, old_value)
603
+ set_original_value(resource, original)
436
604
 
437
- set!(resource, new_value)
605
+ set!(resource, value)
438
606
  end
439
607
 
608
+ # Set the ivar value in the resource
609
+ #
610
+ # @param [Resource] resource
611
+ # the resource to set
612
+ # @param [Object] value
613
+ # the value to set in the resource
614
+ #
615
+ # @return [Object]
616
+ # the value set in the resource
617
+ #
618
+ # @api private
440
619
  def set!(resource, value)
441
620
  resource.instance_variable_set(instance_variable_name, value)
442
621
  end
443
622
 
623
+ # Check if the attribute corresponding to the property is loaded
624
+ #
625
+ # @param [Resource] resource
626
+ # model instance for which the attribute is to be tested
627
+ #
628
+ # @return [Boolean]
629
+ # true if the attribute is loaded in the resource
630
+ #
631
+ # @api private
632
+ def loaded?(resource)
633
+ resource.instance_variable_defined?(instance_variable_name)
634
+ end
635
+
444
636
  # Loads lazy columns when get or set is called.
445
- #-
637
+ #
638
+ # @param [Resource] resource
639
+ # model instance for which lazy loaded attribute are loaded
640
+ #
446
641
  # @api private
447
642
  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
643
  # If we're trying to load a lazy property, load it. Otherwise, lazy-load
452
644
  # any properties that should be eager-loaded but were not included
453
645
  # in the original :fields list
454
- contexts = lazy? ? name : model.eager_properties(resource.repository.name)
455
- resource.send(:lazy_load, contexts)
646
+ property_names = lazy? ? [ name ] : model.properties(resource.repository.name).defaults.map { |property| property.name }
647
+ resource.send(:lazy_load, property_names)
456
648
  end
457
649
 
458
- # typecasts values into a primitive
650
+ # typecasts values into a primitive (Ruby class that backs DataMapper
651
+ # property type). If property type can handle typecasting, it is delegated.
652
+ # How typecasting is perfomed, depends on the primitive of the type.
653
+ #
654
+ # If type's primitive is a TrueClass, values of 1, t and true are casted to true.
655
+ #
656
+ # For String primitive, +to_s+ is called on value.
657
+ #
658
+ # For Float primitive, +to_f+ is called on value but only if value is a number
659
+ # otherwise value is returned.
660
+ #
661
+ # For Integer primitive, +to_i+ is called on value but only if value is a
662
+ # number, otherwise value is returned.
663
+ #
664
+ # For BigDecimal primitive, +to_d+ is called on value but only if value is a
665
+ # number, otherwise value is returned.
666
+ #
667
+ # Casting to DateTime, Time and Date can handle both hashes with keys like :day or
668
+ # :hour and strings in format methods like Time.parse can handle.
669
+ #
670
+ # @param [#to_s, #to_f, #to_i, #to_d, Hash] value
671
+ # the value to typecast
672
+ #
673
+ # @return [rue, String, Float, Integer, BigDecimal, DateTime, Date, Time, Class]
674
+ # The typecasted +value+
459
675
  #
460
- # @return <TrueClass, String, Float, Integer, BigDecimal, DateTime, Date, Time
461
- # Class> the primitive data-type, defaults to TrueClass
462
- #-
463
676
  # @api private
464
677
  def typecast(value)
465
678
  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
679
+ return value if primitive?(value) || value.nil?
680
+
681
+ if primitive == Integer then typecast_to_integer(value)
682
+ elsif primitive == String then typecast_to_string(value)
683
+ elsif primitive == TrueClass then typecast_to_boolean(value)
684
+ elsif primitive == BigDecimal then typecast_to_bigdecimal(value)
685
+ elsif primitive == Float then typecast_to_float(value)
686
+ elsif primitive == DateTime then typecast_to_datetime(value)
687
+ elsif primitive == Time then typecast_to_time(value)
688
+ elsif primitive == Date then typecast_to_date(value)
689
+ elsif primitive == Class then typecast_to_class(value)
690
+ else
499
691
  value
500
692
  end
501
693
  end
502
694
 
695
+ # Returns a default value of the
696
+ # property for given resource.
697
+ #
698
+ # When default value is a callable object,
699
+ # it is called with resource and property passed
700
+ # as arguments.
701
+ #
702
+ # @param [Resource] resource
703
+ # the model instance for which the default is to be set
704
+ #
705
+ # @return [Object]
706
+ # the default value of this property for +resource+
707
+ #
708
+ # @api semipublic
503
709
  def default_for(resource)
504
- @default.respond_to?(:call) ? @default.call(resource, self) : @default
710
+ if @default.respond_to?(:call)
711
+ @default.call(resource, self)
712
+ else
713
+ @default.try_dup
714
+ end
505
715
  end
506
716
 
507
- def value(val)
508
- custom? ? self.type.dump(val, self) : val
717
+ # Returns true if the property has a default value
718
+ #
719
+ # @return [Boolean]
720
+ # true if the property has a default value
721
+ #
722
+ # @api semipublic
723
+ def default?
724
+ @options.key?(:default)
509
725
  end
510
726
 
511
- def inspect
512
- "#<Property:#{@model}:#{@name}>"
727
+ # Returns given value unchanged for core types and
728
+ # uses +dump+ method of the property type for custom types.
729
+ #
730
+ # @param [Object] value
731
+ # the value to be converted into a storeable (ie., primitive) value
732
+ #
733
+ # @return [Object]
734
+ # the primitive value to be stored in the repository for +val+
735
+ #
736
+ # @api semipublic
737
+ def value(value)
738
+ if custom?
739
+ type.dump(value, self)
740
+ else
741
+ value
742
+ end
513
743
  end
514
744
 
515
- # TODO: add docs
516
- # @api private
517
- def _dump(*)
518
- Marshal.dump([ repository, model, name ])
745
+ # Test the value to see if it is a valid value for this Property
746
+ #
747
+ # @param [Object] value
748
+ # the value to be tested
749
+ #
750
+ # @return [Boolean]
751
+ # true if the value is valid
752
+ #
753
+ # @api semipulic
754
+ def valid?(value)
755
+ value = self.value(value)
756
+ primitive?(value) || (value.nil? && nullable?)
519
757
  end
520
758
 
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]
759
+ # Returns a concise string representation of the property instance.
760
+ #
761
+ # @return [String]
762
+ # Concise string representation of the property instance.
763
+ #
764
+ # @api public
765
+ def inspect
766
+ "#<#{self.class.name} @model=#{model.inspect} @name=#{name.inspect}>"
767
+ end
768
+
769
+ # Test a value to see if it matches the primitive type
770
+ #
771
+ # @param [Object] value
772
+ # value to test
773
+ #
774
+ # @return [Boolean]
775
+ # true if the value is the correct type
776
+ #
777
+ # @api semipublic
778
+ def primitive?(value)
779
+ if primitive == TrueClass
780
+ value == true || value == false
781
+ else
782
+ value.kind_of?(primitive)
783
+ end
526
784
  end
527
785
 
528
786
  private
529
787
 
788
+ # TODO: document
789
+ # @api semipublic
530
790
  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
791
+ assert_kind_of 'model', model, Model
792
+ assert_kind_of 'name', name, Symbol
793
+ assert_kind_of 'type', type, Class, Module
794
+ assert_kind_of 'options', options, Hash
795
+
796
+ options = options.dup
797
+
798
+ if TrueClass == type
799
+ warn "#{type} is deprecated, use Boolean instead at #{caller[2]}"
800
+ type = Types::Boolean
801
+ elsif Integer == type && options.delete(:serial)
802
+ warn "#{type} with explicit :serial option is deprecated, use Serial instead (#{caller[2]})"
803
+ type = Types::Serial
804
+ elsif options.key?(:size)
805
+ if String == type
806
+ warn ":size option is deprecated, use #{type} with :length instead (#{caller[2]})"
807
+ length = options.delete(:size)
808
+ options[:length] = length unless options.key?(:length)
809
+ elsif Numeric > type
810
+ warn ":size option is deprecated, specify :min and :max instead (#{caller[2]})"
811
+ end
543
812
  end
544
813
 
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
814
+ assert_valid_options(options)
815
+
816
+ # if the type can be found within Types then
817
+ # use that class rather than the primitive
818
+ unless type.name.blank?
819
+ type = Types.find_const(type.name)
547
820
  end
548
821
 
549
- @extra_options = {}
550
- (options.keys - PROPERTY_OPTIONS).each do |key|
551
- @extra_options[key] = options.delete(key)
822
+ unless PRIMITIVES.include?(type) || (Type > type && PRIMITIVES.include?(type.primitive))
823
+ raise ArgumentError, "+type+ was #{type.inspect}, which is not a supported type"
552
824
  end
553
825
 
826
+ @repository_name = model.repository_name
554
827
  @model = model
555
828
  @name = name.to_s.sub(/\?$/, '').to_sym
556
829
  @type = type
557
- @custom = DataMapper::Type > @type
558
- @options = @custom ? @type.options.merge(options) : options
559
- @instance_variable_name = "@#{@name}"
830
+ @custom = Type > @type
831
+ @options = (@custom ? @type.options.merge(options) : options.dup).freeze
832
+ @instance_variable_name = "@#{@name}".freeze
560
833
 
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)
834
+ @primitive = @type.respond_to?(:primitive) ? @type.primitive : @type
835
+ @field = @options[:field].freeze
836
+ @default = @options[:default]
564
837
 
565
- @getter = TrueClass == @primitive ? "#{@name}?".to_sym : @name
566
- @field = @options.fetch(:field, nil)
567
838
  @serial = @options.fetch(:serial, false)
568
839
  @key = @options.fetch(:key, @serial || false)
569
- @default = @options.fetch(:default, nil)
570
840
  @nullable = @options.fetch(:nullable, @key == false)
571
- @index = @options.fetch(:index, false)
572
- @unique_index = @options.fetch(:unique_index, false)
841
+ @index = @options.fetch(:index, nil)
842
+ @unique_index = @options.fetch(:unique_index, nil)
843
+ @unique = @options.fetch(:unique, @serial || @key || false)
573
844
  @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
845
 
584
846
  # assign attributes per-type
585
847
  if String == @primitive || Class == @primitive
586
- @length = @options.fetch(:length, @options.fetch(:size, DEFAULT_LENGTH))
848
+ @length = @options.fetch(:length, DEFAULT_LENGTH)
587
849
  elsif BigDecimal == @primitive || Float == @primitive
588
850
  @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)
851
+ @scale = @options.fetch(:scale, Float == @primitive ? DEFAULT_SCALE_FLOAT : DEFAULT_SCALE_BIGDECIMAL)
593
852
 
594
853
  unless @precision > 0
595
854
  raise ArgumentError, "precision must be greater than 0, but was #{@precision.inspect}"
596
855
  end
597
856
 
598
- if (BigDecimal == @primitive) || (Float == @primitive && !@scale.nil?)
857
+ unless Float == @primitive && @scale.nil?
599
858
  unless @scale >= 0
600
859
  raise ArgumentError, "scale must be equal to or greater than 0, but was #{@scale.inspect}"
601
860
  end
@@ -606,71 +865,347 @@ module DataMapper
606
865
  end
607
866
  end
608
867
 
868
+ if Numeric > @primitive && (@options.keys & [ :min, :max ]).any?
869
+ @min = @options.fetch(:min, DEFAULT_NUMERIC_MIN)
870
+ @max = @options.fetch(:max, DEFAULT_NUMERIC_MAX)
871
+
872
+ if @max < DEFAULT_NUMERIC_MIN && !@options.key?(:min)
873
+ raise ArgumentError, "min should be specified when the max is less than #{DEFAULT_NUMERIC_MIN}"
874
+ elsif @max < @min
875
+ raise ArgumentError, "max must be less than the min, but was #{@max} while the min was #{@min}"
876
+ end
877
+ end
878
+
609
879
  determine_visibility
610
880
 
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)
881
+ if custom?
882
+ type.bind(self)
883
+ end
884
+
885
+ # comes from dm-validations
886
+ @model.auto_generate_validations(self) if @model.respond_to?(:auto_generate_validations)
613
887
  end
614
888
 
615
- def determine_visibility # :nodoc:
889
+ # TODO: document
890
+ # @api private
891
+ def assert_valid_options(options)
892
+ if (unknown_keys = options.keys - OPTIONS).any?
893
+ raise ArgumentError, "options #{unknown_keys.map { |key| key.inspect }.join(' and ')} are unknown"
894
+ end
895
+
896
+ options.each do |key, value|
897
+ case key
898
+ when :field
899
+ assert_kind_of "options[#{key.inspect}]", value, String
900
+
901
+ when :default
902
+ if value.nil?
903
+ raise ArgumentError, "options[#{key.inspect}] must not be nil"
904
+ end
905
+
906
+ when :serial, :key, :nullable, :unique, :auto_validation
907
+ unless value == true || value == false
908
+ raise ArgumentError, "options[#{key.inspect}] must be either true or false"
909
+ end
910
+
911
+ when :lazy
912
+ unless value == true || value == false || value.kind_of?(Symbol) || (value.kind_of?(Array) && value.all? { |val| val.kind_of?(Symbol) })
913
+ raise ArgumentError, "options[#{key.inspect}] must be either true, false, a Symbol or an Array of Symbols"
914
+ end
915
+
916
+ when :index, :unique_index
917
+ assert_kind_of "options[#{key.inspect}]", value, Symbol, Array, TrueClass
918
+
919
+ when :length
920
+ assert_kind_of "options[#{key.inspect}]", value, Range, Integer
921
+
922
+ when :size, :precision, :scale
923
+ assert_kind_of "options[#{key.inspect}]", value, Integer
924
+
925
+ when :reader, :writer, :accessor
926
+ assert_kind_of "options[#{key.inspect}]", value, Symbol
927
+
928
+ unless VISIBILITY_OPTIONS.include?(value)
929
+ raise ArgumentError, "options[#{key.inspect}] must be #{VISIBILITY_OPTIONS.join(' or ')}"
930
+ end
931
+ end
932
+ end
933
+ end
934
+
935
+ # Assert given visibility value is supported.
936
+ #
937
+ # Will raise ArgumentError if this Property's reader and writer
938
+ # visibilities are not included in VISIBILITY_OPTIONS.
939
+ # @return [NilClass]
940
+ #
941
+ # @raise [ArgumentError] "property visibility must be :public, :protected, or :private"
942
+ #
943
+ # @api private
944
+ def determine_visibility
616
945
  @reader_visibility = @options[:reader] || @options[:accessor] || :public
617
946
  @writer_visibility = @options[:writer] || @options[:accessor] || :public
947
+ end
948
+
618
949
 
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)
950
+ # Typecast a value to an Integer
951
+ #
952
+ # @param [#to_str, #to_i] value
953
+ # value to typecast
954
+ #
955
+ # @return [Integer]
956
+ # Integer constructed from value
957
+ #
958
+ # @api private
959
+ def typecast_to_integer(value)
960
+ typecast_to_numeric(value, :to_i)
961
+ end
962
+
963
+ # Typecast a value to a String
964
+ #
965
+ # @param [#to_s] value
966
+ # value to typecast
967
+ #
968
+ # @return [String]
969
+ # String constructed from value
970
+ #
971
+ # @api private
972
+ def typecast_to_string(value)
973
+ value.to_s
974
+ end
975
+
976
+ # Typecast a value to a true or false
977
+ #
978
+ # @param [Integer, #to_str] value
979
+ # value to typecast
980
+ #
981
+ # @return [Boolean]
982
+ # true or false constructed from value
983
+ #
984
+ # @api private
985
+ def typecast_to_boolean(value)
986
+ if value.kind_of?(Integer)
987
+ return true if value == 1
988
+ return false if value == 0
989
+ elsif value.respond_to?(:to_str)
990
+ return true if %w[ true 1 t ].include?(value.to_str.downcase)
991
+ return false if %w[ false 0 f ].include?(value.to_str.downcase)
992
+ end
993
+
994
+ value
995
+ end
996
+
997
+ # Typecast a value to a BigDecimal
998
+ #
999
+ # @param [#to_str, #to_d, Integer] value
1000
+ # value to typecast
1001
+ #
1002
+ # @return [BigDecimal]
1003
+ # BigDecimal constructed from value
1004
+ #
1005
+ # @api private
1006
+ def typecast_to_bigdecimal(value)
1007
+ if value.kind_of?(Integer)
1008
+ # TODO: remove this case when Integer#to_d added by extlib
1009
+ value.to_s.to_d
1010
+ else
1011
+ typecast_to_numeric(value, :to_d)
621
1012
  end
622
1013
  end
623
1014
 
624
- # Typecasts an arbitrary value to a DateTime
1015
+ # Typecast a value to a Float
1016
+ #
1017
+ # @param [#to_str, #to_f] value
1018
+ # value to typecast
1019
+ #
1020
+ # @return [Float]
1021
+ # Float constructed from value
1022
+ #
1023
+ # @api private
1024
+ def typecast_to_float(value)
1025
+ typecast_to_numeric(value, :to_f)
1026
+ end
1027
+
1028
+ # Match numeric string
1029
+ #
1030
+ # @param [#to_str, Numeric] value
1031
+ # value to typecast
1032
+ # @param [Symbol] method
1033
+ # method to typecast with
1034
+ #
1035
+ # @return [Numeric]
1036
+ # number if matched, value if no match
1037
+ #
1038
+ # @api private
1039
+ def typecast_to_numeric(value, method)
1040
+ if value.respond_to?(:to_str)
1041
+ if value.to_str =~ /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/
1042
+ $1.send(method)
1043
+ else
1044
+ value
1045
+ end
1046
+ elsif value.respond_to?(method)
1047
+ value.send(method)
1048
+ else
1049
+ value
1050
+ end
1051
+ end
1052
+
1053
+ # Typecasts an arbitrary value to a DateTime.
1054
+ # Handles both Hashes and DateTime instances.
1055
+ #
1056
+ # @param [#to_mash, #to_s] value
1057
+ # value to be typecast
1058
+ #
1059
+ # @return [DateTime]
1060
+ # DateTime constructed from value
1061
+ #
1062
+ # @api private
625
1063
  def typecast_to_datetime(value)
626
- case value
627
- when Hash then typecast_hash_to_datetime(value)
628
- else DateTime.parse(value.to_s)
1064
+ if value.respond_to?(:to_mash)
1065
+ typecast_hash_to_datetime(value)
1066
+ else
1067
+ DateTime.parse(value.to_s)
629
1068
  end
1069
+ rescue ArgumentError
1070
+ value
630
1071
  end
631
1072
 
632
1073
  # Typecasts an arbitrary value to a Date
1074
+ # Handles both Hashes and Date instances.
1075
+ #
1076
+ # @param [#to_mash, #to_s] value
1077
+ # value to be typecast
1078
+ #
1079
+ # @return [Date]
1080
+ # Date constructed from value
1081
+ #
1082
+ # @api private
633
1083
  def typecast_to_date(value)
634
- case value
635
- when Hash then typecast_hash_to_date(value)
636
- else Date.parse(value.to_s)
1084
+ if value.respond_to?(:to_mash)
1085
+ typecast_hash_to_date(value)
1086
+ else
1087
+ Date.parse(value.to_s)
637
1088
  end
1089
+ rescue ArgumentError
1090
+ value
638
1091
  end
639
1092
 
640
1093
  # Typecasts an arbitrary value to a Time
1094
+ # Handles both Hashes and Time instances.
1095
+ #
1096
+ # @param [#to_mash, #to_s] value
1097
+ # value to be typecast
1098
+ #
1099
+ # @return [Time]
1100
+ # Time constructed from value
1101
+ #
1102
+ # @api private
641
1103
  def typecast_to_time(value)
642
- case value
643
- when Hash then typecast_hash_to_time(value)
644
- else Time.parse(value.to_s)
1104
+ if value.respond_to?(:to_mash)
1105
+ typecast_hash_to_time(value)
1106
+ else
1107
+ Time.parse(value.to_s)
645
1108
  end
1109
+ rescue ArgumentError
1110
+ value
646
1111
  end
647
1112
 
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)
1113
+ # Creates a DateTime instance from a Hash with keys :year, :month, :day,
1114
+ # :hour, :min, :sec
1115
+ #
1116
+ # @param [#to_mash] value
1117
+ # value to be typecast
1118
+ #
1119
+ # @return [DateTime]
1120
+ # DateTime constructed from hash
1121
+ #
1122
+ # @api private
1123
+ def typecast_hash_to_datetime(value)
1124
+ DateTime.new(*extract_time(value))
654
1125
  end
655
1126
 
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)
1127
+ # Creates a Date instance from a Hash with keys :year, :month, :day
1128
+ #
1129
+ # @param [#to_mash] value
1130
+ # value to be typecast
1131
+ #
1132
+ # @return [Date]
1133
+ # Date constructed from hash
1134
+ #
1135
+ # @api private
1136
+ def typecast_hash_to_date(value)
1137
+ Date.new(*extract_time(value)[0, 3])
662
1138
  end
663
1139
 
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)
1140
+ # Creates a Time instance from a Hash with keys :year, :month, :day,
1141
+ # :hour, :min, :sec
1142
+ #
1143
+ # @param [#to_mash] value
1144
+ # value to be typecast
1145
+ #
1146
+ # @return [Time]
1147
+ # Time constructed from hash
1148
+ #
1149
+ # @api private
1150
+ def typecast_hash_to_time(value)
1151
+ Time.local(*extract_time(value))
667
1152
  end
668
1153
 
669
1154
  # 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) }
1155
+ # uses the value of Time.now.
1156
+ #
1157
+ # @param [#to_mash] value
1158
+ # value to extract time args from
1159
+ #
1160
+ # @return [Array]
1161
+ # Extracted values
1162
+ #
1163
+ # @api private
1164
+ def extract_time(value)
1165
+ mash = value.to_mash
1166
+ now = Time.now
1167
+
1168
+ [ :year, :month, :day, :hour, :min, :sec ].map do |segment|
1169
+ typecast_to_numeric(mash.fetch(segment, now.send(segment)), :to_i)
1170
+ end
1171
+ end
1172
+
1173
+ # Typecast a value to a Class
1174
+ #
1175
+ # @param [#to_s] value
1176
+ # value to typecast
1177
+ #
1178
+ # @return [Class]
1179
+ # Class constructed from value
1180
+ #
1181
+ # @api private
1182
+ def typecast_to_class(value)
1183
+ model.find_const(value.to_s)
1184
+ rescue NameError
1185
+ value
1186
+ end
1187
+
1188
+ # Return true if +other+'s is equivalent or equal to +self+'s
1189
+ #
1190
+ # @param [Property] other
1191
+ # The Property whose attributes are to be compared with +self+'s
1192
+ # @param [Symbol] operator
1193
+ # The comparison operator to use to compare the attributes
1194
+ #
1195
+ # @return [Boolean]
1196
+ # The result of the comparison of +other+'s attributes with +self+'s
1197
+ #
1198
+ # @api private
1199
+ def cmp?(other, operator)
1200
+ unless model.base_model.send(operator, other.model.base_model)
1201
+ return false
1202
+ end
1203
+
1204
+ unless name.send(operator, other.name)
1205
+ return false
1206
+ end
1207
+
1208
+ true
674
1209
  end
675
1210
  end # class Property
676
1211
  end # module DataMapper