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 (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