dm-core 0.9.2

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 (101) hide show
  1. data/CHANGELOG +144 -0
  2. data/FAQ +74 -0
  3. data/MIT-LICENSE +22 -0
  4. data/QUICKLINKS +12 -0
  5. data/README +143 -0
  6. data/lib/dm-core.rb +213 -0
  7. data/lib/dm-core/adapters.rb +4 -0
  8. data/lib/dm-core/adapters/abstract_adapter.rb +202 -0
  9. data/lib/dm-core/adapters/data_objects_adapter.rb +701 -0
  10. data/lib/dm-core/adapters/mysql_adapter.rb +132 -0
  11. data/lib/dm-core/adapters/postgres_adapter.rb +179 -0
  12. data/lib/dm-core/adapters/sqlite3_adapter.rb +105 -0
  13. data/lib/dm-core/associations.rb +172 -0
  14. data/lib/dm-core/associations/many_to_many.rb +138 -0
  15. data/lib/dm-core/associations/many_to_one.rb +101 -0
  16. data/lib/dm-core/associations/one_to_many.rb +275 -0
  17. data/lib/dm-core/associations/one_to_one.rb +61 -0
  18. data/lib/dm-core/associations/relationship.rb +116 -0
  19. data/lib/dm-core/associations/relationship_chain.rb +74 -0
  20. data/lib/dm-core/auto_migrations.rb +64 -0
  21. data/lib/dm-core/collection.rb +604 -0
  22. data/lib/dm-core/hook.rb +11 -0
  23. data/lib/dm-core/identity_map.rb +45 -0
  24. data/lib/dm-core/is.rb +16 -0
  25. data/lib/dm-core/logger.rb +233 -0
  26. data/lib/dm-core/migrations/destructive_migrations.rb +17 -0
  27. data/lib/dm-core/migrator.rb +29 -0
  28. data/lib/dm-core/model.rb +399 -0
  29. data/lib/dm-core/naming_conventions.rb +52 -0
  30. data/lib/dm-core/property.rb +611 -0
  31. data/lib/dm-core/property_set.rb +158 -0
  32. data/lib/dm-core/query.rb +590 -0
  33. data/lib/dm-core/repository.rb +159 -0
  34. data/lib/dm-core/resource.rb +618 -0
  35. data/lib/dm-core/scope.rb +35 -0
  36. data/lib/dm-core/support.rb +7 -0
  37. data/lib/dm-core/support/array.rb +13 -0
  38. data/lib/dm-core/support/assertions.rb +8 -0
  39. data/lib/dm-core/support/errors.rb +23 -0
  40. data/lib/dm-core/support/kernel.rb +7 -0
  41. data/lib/dm-core/support/symbol.rb +41 -0
  42. data/lib/dm-core/transaction.rb +267 -0
  43. data/lib/dm-core/type.rb +160 -0
  44. data/lib/dm-core/type_map.rb +80 -0
  45. data/lib/dm-core/types.rb +19 -0
  46. data/lib/dm-core/types/boolean.rb +7 -0
  47. data/lib/dm-core/types/discriminator.rb +32 -0
  48. data/lib/dm-core/types/object.rb +20 -0
  49. data/lib/dm-core/types/paranoid_boolean.rb +23 -0
  50. data/lib/dm-core/types/paranoid_datetime.rb +22 -0
  51. data/lib/dm-core/types/serial.rb +9 -0
  52. data/lib/dm-core/types/text.rb +10 -0
  53. data/spec/integration/association_spec.rb +1215 -0
  54. data/spec/integration/association_through_spec.rb +150 -0
  55. data/spec/integration/associations/many_to_many_spec.rb +171 -0
  56. data/spec/integration/associations/many_to_one_spec.rb +123 -0
  57. data/spec/integration/associations/one_to_many_spec.rb +66 -0
  58. data/spec/integration/auto_migrations_spec.rb +398 -0
  59. data/spec/integration/collection_spec.rb +1015 -0
  60. data/spec/integration/data_objects_adapter_spec.rb +32 -0
  61. data/spec/integration/model_spec.rb +68 -0
  62. data/spec/integration/mysql_adapter_spec.rb +85 -0
  63. data/spec/integration/postgres_adapter_spec.rb +732 -0
  64. data/spec/integration/property_spec.rb +224 -0
  65. data/spec/integration/query_spec.rb +376 -0
  66. data/spec/integration/repository_spec.rb +57 -0
  67. data/spec/integration/resource_spec.rb +324 -0
  68. data/spec/integration/sqlite3_adapter_spec.rb +352 -0
  69. data/spec/integration/sti_spec.rb +185 -0
  70. data/spec/integration/transaction_spec.rb +75 -0
  71. data/spec/integration/type_spec.rb +149 -0
  72. data/spec/lib/mock_adapter.rb +27 -0
  73. data/spec/spec_helper.rb +112 -0
  74. data/spec/unit/adapters/abstract_adapter_spec.rb +133 -0
  75. data/spec/unit/adapters/adapter_shared_spec.rb +15 -0
  76. data/spec/unit/adapters/data_objects_adapter_spec.rb +627 -0
  77. data/spec/unit/adapters/postgres_adapter_spec.rb +125 -0
  78. data/spec/unit/associations/many_to_many_spec.rb +14 -0
  79. data/spec/unit/associations/many_to_one_spec.rb +138 -0
  80. data/spec/unit/associations/one_to_many_spec.rb +385 -0
  81. data/spec/unit/associations/one_to_one_spec.rb +7 -0
  82. data/spec/unit/associations/relationship_spec.rb +67 -0
  83. data/spec/unit/associations_spec.rb +205 -0
  84. data/spec/unit/auto_migrations_spec.rb +110 -0
  85. data/spec/unit/collection_spec.rb +174 -0
  86. data/spec/unit/data_mapper_spec.rb +21 -0
  87. data/spec/unit/identity_map_spec.rb +126 -0
  88. data/spec/unit/is_spec.rb +80 -0
  89. data/spec/unit/migrator_spec.rb +33 -0
  90. data/spec/unit/model_spec.rb +339 -0
  91. data/spec/unit/naming_conventions_spec.rb +28 -0
  92. data/spec/unit/property_set_spec.rb +96 -0
  93. data/spec/unit/property_spec.rb +447 -0
  94. data/spec/unit/query_spec.rb +485 -0
  95. data/spec/unit/repository_spec.rb +93 -0
  96. data/spec/unit/resource_spec.rb +557 -0
  97. data/spec/unit/scope_spec.rb +131 -0
  98. data/spec/unit/transaction_spec.rb +493 -0
  99. data/spec/unit/type_map_spec.rb +114 -0
  100. data/spec/unit/type_spec.rb +119 -0
  101. metadata +187 -0
@@ -0,0 +1,52 @@
1
+ module DataMapper
2
+
3
+ # Use these modules to establish naming conventions.
4
+ # The default is UnderscoredAndPluralized.
5
+ # You assign a naming convention like so:
6
+ #
7
+ # repository(:default).adapter.resource_naming_convention = NamingConventions::Underscored
8
+ #
9
+ # You can also easily assign a custom convention with a Proc:
10
+ #
11
+ # repository(:default).adapter.resource_naming_convention = lambda do |value|
12
+ # 'tbl' + value.camelize(true)
13
+ # end
14
+ #
15
+ # Or by simply defining your own module in NamingConventions that responds to
16
+ # ::call.
17
+ #
18
+ # NOTE: It's important to set the convention before accessing your models
19
+ # since the resource_names are cached after first accessed.
20
+ # DataMapper.setup(name, uri) returns the Adapter for convenience, so you can
21
+ # use code like this:
22
+ #
23
+ # adapter = DataMapper.setup(:default, "mock://localhost/mock")
24
+ # adapter.resource_naming_convention = DataMapper::NamingConventions::Underscored
25
+ module NamingConventions
26
+
27
+ module UnderscoredAndPluralized
28
+ def self.call(value)
29
+ Extlib::Inflection.pluralize(Extlib::Inflection.underscore(value)).gsub('/','_')
30
+ end
31
+ end # module UnderscoredAndPluralized
32
+
33
+ module UnderscoredAndPluralizedWithoutModule
34
+ def self.call(value)
35
+ Extlib::Inflection.pluralize(Extlib::Inflection.underscore(Extlib::Inflection.demodulize(value)))
36
+ end
37
+ end # module UnderscoredAndPluralizedWithoutModule
38
+
39
+ module Underscored
40
+ def self.call(value)
41
+ Extlib::Inflection.underscore(value)
42
+ end
43
+ end # module Underscored
44
+
45
+ module Yaml
46
+ def self.call(value)
47
+ Extlib::Inflection.pluralize(Extlib::Inflection.underscore(value)) + ".yaml"
48
+ end
49
+ end # module Yaml
50
+
51
+ end # module NamingConventions
52
+ end # module DataMapper
@@ -0,0 +1,611 @@
1
+ require 'date'
2
+ require 'time'
3
+ require 'bigdecimal'
4
+
5
+ module DataMapper
6
+
7
+ # :include:QUICKLINKS
8
+ #
9
+ # = Properties
10
+ # Properties for a model are not derived from a database structure, but
11
+ # instead explicitly declared inside your model class definitions. These
12
+ # properties then map (or, if using automigrate, generate) fields in your
13
+ # repository/database.
14
+ #
15
+ # 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:
18
+ #
19
+ # * information about your model is centralized in one place: rather than
20
+ # having to dig out migrations, xml or other configuration files.
21
+ # * having information centralized in your models, encourages you and the
22
+ # developers on your team to take a model-centric view of development.
23
+ # * it provides the ability to use Ruby's access control functions.
24
+ # * and, because DataMapper only cares about properties explicitly defined in
25
+ # your models, DataMapper plays well with legacy databases, and shares
26
+ # databases easily with other applications.
27
+ #
28
+ # == Declaring Properties
29
+ # Inside your class, you call the property method for each property you want
30
+ # to add. The only two required arguments are the name and type, everything
31
+ # else is optional.
32
+ #
33
+ # class Post
34
+ # 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
39
+ # end
40
+ #
41
+ # By default, DataMapper supports the following primitive types:
42
+ #
43
+ # * TrueClass, Boolean
44
+ # * String
45
+ # * Text (limit of 65k characters by default)
46
+ # * Float
47
+ # * Integer
48
+ # * BigDecimal
49
+ # * DateTime
50
+ # * Date
51
+ # * Time
52
+ # * Object (marshalled out during serialization)
53
+ # * Class (datastore primitive is the same as String. Used for Inheritance)
54
+ #
55
+ # For more information about available Types, see DataMapper::Type
56
+ #
57
+ # == Limiting Access
58
+ # Property access control is uses the same terminology Ruby does. Properties
59
+ # are public by default, but can also be declared private or protected as
60
+ # needed (via the :accessor option).
61
+ #
62
+ # class Post
63
+ # 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
68
+ # end
69
+ #
70
+ # Access control is also analogous to Ruby accessors and mutators, and can
71
+ # be declared using :reader and :writer, in addition to :accessor.
72
+ #
73
+ # class Post
74
+ # include DataMapper::Resource
75
+ #
76
+ # property :title, String, :writer => :private
77
+ # # Only writer is private
78
+ #
79
+ # property :tags, String, :reader => :protected
80
+ # # Only reader is protected
81
+ # end
82
+ #
83
+ # == 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:
87
+ #
88
+ # class Post
89
+ # include DataMapper::Resource
90
+ # property :title, String
91
+ #
92
+ # def title=(new_title)
93
+ # raise ArgumentError if new_title != 'Luke is Awesome'
94
+ # @title = new_title
95
+ # end
96
+ # end
97
+ #
98
+ # == Lazy Loading
99
+ # By default, some properties are not loaded when an object is fetched in
100
+ # DataMapper. These lazily loaded properties are fetched on demand when their
101
+ # accessor is called for the first time (as it is often unnecessary to
102
+ # instantiate -every- property -every- time an object is loaded). For
103
+ # instance, DataMapper::Types::Text fields are lazy loading by default,
104
+ # although you can over-ride this behavior if you wish:
105
+ #
106
+ # Example:
107
+ #
108
+ # class Post
109
+ # include DataMapper::Resource
110
+ # property :title, String # Loads normally
111
+ # property :body, DataMapper::Types::Text # Is lazily loaded by default
112
+ # end
113
+ #
114
+ # If you want to over-ride the lazy loading on any field you can set it to a
115
+ # context or false to disable it with the :lazy option. Contexts allow
116
+ # multipule lazy properties to be loaded at one time. If you set :lazy to
117
+ # true, it is placed in the :default context
118
+ #
119
+ # class Post
120
+ # include DataMapper::Resource
121
+ #
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
133
+ # end
134
+ #
135
+ # Delaying the request for lazy-loaded attributes even applies to objects
136
+ # accessed through associations. In a sense, DataMapper anticipates that
137
+ # you will likely be iterating over objects in associations and rolls all
138
+ # of the load commands for lazy-loaded properties into one request from
139
+ # the database.
140
+ #
141
+ # Example:
142
+ #
143
+ # Widget[1].components
144
+ # # loads when the post object is pulled from database, by default
145
+ #
146
+ # Widget[1].components.first.body
147
+ # # loads the values for the body property on all objects in the
148
+ # # association, rather than just this one.
149
+ #
150
+ # Widget[1].components.first.comment
151
+ # # loads both comment and author for all objects in the association
152
+ # # since they are both in the :detailed context
153
+ #
154
+ # == Keys
155
+ # Properties can be declared as primary or natural keys on a table.
156
+ # You should a property as the primary key of the table:
157
+ #
158
+ # Examples:
159
+ #
160
+ # property :id, Serial # auto-incrementing key
161
+ # property :legacy_pk, String, :key => true # 'natural' key
162
+ #
163
+ # This is roughly equivalent to ActiveRecord's <tt>set_primary_key</tt>,
164
+ # though non-integer data types may be used, thus DataMapper supports natural
165
+ # keys. When a property is declared as a natural key, accessing the object
166
+ # using the indexer syntax <tt>Class[key]</tt> remains valid.
167
+ #
168
+ # User[1]
169
+ # # when :id is the primary key on the users table
170
+ # User['bill']
171
+ # # when :name is the primary (natural) key on the users table
172
+ #
173
+ # == Indeces
174
+ # You can add indeces for your properties by using the <tt>:index</tt>
175
+ # option. If you use <tt>true</tt> as the option value, the index will be
176
+ # automatically named. If you want to name the index yourself, use a symbol
177
+ # as the value.
178
+ #
179
+ # property :last_name, String, :index => true
180
+ # property :first_name, String, :index => :name
181
+ #
182
+ # You can create multi-column composite indeces by using the same symbol in
183
+ # all the columns belonging to the index. The columns will appear in the
184
+ # index in the order they are declared.
185
+ #
186
+ # property :last_name, String, :index => :name
187
+ # property :first_name, String, :index => :name
188
+ # # => index on (last_name, first_name)
189
+ #
190
+ # If you want to make the indeces unique, use <tt>:unique_index</tt> instead
191
+ # of <tt>:index</tt>
192
+ #
193
+ # == Inferred Validations
194
+ # If you require the dm-validations plugin, auto-validations will
195
+ # automatically be mixed-in in to your model classes:
196
+ # validation rules that are inferred when properties are declared with
197
+ # specific column restrictions.
198
+ #
199
+ # class Post
200
+ # include DataMapper::Resource
201
+ #
202
+ # property :title, String, :length => 250
203
+ # # => infers 'validates_length :title,
204
+ # :minimum => 0, :maximum => 250'
205
+ #
206
+ # property :title, String, :nullable => false
207
+ # # => infers 'validates_present :title
208
+ #
209
+ # property :email, String, :format => :email_address
210
+ # # => infers 'validates_format :email, :with => :email_address
211
+ #
212
+ # property :title, String, :length => 255, :nullable => false
213
+ # # => infers both 'validates_length' as well as
214
+ # # 'validates_present'
215
+ # # better: property :title, String, :length => 1..255
216
+ #
217
+ # end
218
+ #
219
+ # This functionality is available with the dm-validations gem, part of the
220
+ # dm-more bundle. For more information about validations, check the
221
+ # documentation for dm-validations.
222
+ #
223
+ # == Embedded Values
224
+ # As an alternative to extraneous has_one relationships, consider using an
225
+ # EmbeddedValue.
226
+ #
227
+ # == Misc. Notes
228
+ # * Properties declared as strings will default to a length of 50, rather than
229
+ # 255 (typical max varchar column size). To overload the default, pass
230
+ # <tt>:length => 255</tt> or <tt>:length => 0..255</tt>. Since DataMapper
231
+ # does not introspect for properties, this means that legacy database tables
232
+ # may need their <tt>String</tt> columns defined with a <tt>:length</tt> so
233
+ # that DM does not apply an un-needed length validation, or allow overflow.
234
+ # * You may declare a Property with the data-type of <tt>Class</tt>.
235
+ # see SingleTableInheritance for more on how to use <tt>Class</tt> columns.
236
+ class Property
237
+ include Assertions
238
+
239
+ # NOTE: check is only for psql, so maybe the postgres adapter should
240
+ # define its own property options. currently it will produce a warning tho
241
+ # since PROPERTY_OPTIONS is a constant
242
+ #
243
+ # NOTE: PLEASE update PROPERTY_OPTIONS in DataMapper::Type when updating
244
+ # them here
245
+ PROPERTY_OPTIONS = [
246
+ :public, :protected, :private, :accessor, :reader, :writer,
247
+ :lazy, :default, :nullable, :key, :serial, :field, :size, :length,
248
+ :format, :index, :unique_index, :check, :ordinal, :auto_validation,
249
+ :validates, :unique, :track, :precision, :scale
250
+ ]
251
+
252
+ # FIXME: can we pull the keys from
253
+ # DataMapper::Adapters::DataObjectsAdapter::TYPES
254
+ # for this?
255
+ TYPES = [
256
+ TrueClass,
257
+ String,
258
+ DataMapper::Types::Text,
259
+ Float,
260
+ Integer,
261
+ BigDecimal,
262
+ DateTime,
263
+ Date,
264
+ Time,
265
+ Object,
266
+ Class,
267
+ DataMapper::Types::Discriminator
268
+ ]
269
+
270
+ IMMUTABLE_TYPES = [ TrueClass, Float, Integer, BigDecimal]
271
+
272
+ VISIBILITY_OPTIONS = [ :public, :protected, :private ]
273
+
274
+ DEFAULT_LENGTH = 50
275
+ DEFAULT_PRECISION = 10
276
+ DEFAULT_SCALE = 0
277
+
278
+ attr_reader :primitive, :model, :name, :instance_variable_name,
279
+ :type, :reader_visibility, :writer_visibility, :getter, :options,
280
+ :default, :precision, :scale, :track
281
+
282
+ # Supplies the field in the data-store which the property corresponds to
283
+ #
284
+ # @return <String> name of field in data-store
285
+ # -
286
+ # @api semi-public
287
+ def field(*args)
288
+ @options.fetch(:field, repository(*args).adapter.field_naming_convention.call(name))
289
+ end
290
+
291
+ def unique
292
+ @unique ||= @options.fetch(:unique, @serial || @key || false)
293
+ end
294
+
295
+ def repository(*args)
296
+ @model.repository(*args)
297
+ end
298
+
299
+ def hash
300
+ if @custom && !@bound
301
+ @type.bind(self)
302
+ @bound = true
303
+ end
304
+
305
+ return @model.hash + @name.hash
306
+ end
307
+
308
+ def eql?(o)
309
+ if o.is_a?(Property)
310
+ return o.model == @model && o.name == @name
311
+ else
312
+ return false
313
+ end
314
+ end
315
+
316
+ def length
317
+ @length.is_a?(Range) ? @length.max : @length
318
+ end
319
+ alias size length
320
+
321
+ def index
322
+ @index
323
+ end
324
+
325
+ def unique_index
326
+ @unique_index
327
+ end
328
+
329
+ # Returns whether or not the property is to be lazy-loaded
330
+ #
331
+ # @return <TrueClass, FalseClass> whether or not the property is to be
332
+ # lazy-loaded
333
+ # -
334
+ # @api public
335
+ def lazy?
336
+ @lazy
337
+ end
338
+
339
+
340
+ # Returns whether or not the property is a key or a part of a key
341
+ #
342
+ # @return <TrueClass, FalseClass> whether the property is a key or a part of
343
+ # a key
344
+ #-
345
+ # @api public
346
+ def key?
347
+ @key
348
+ end
349
+
350
+ # Returns whether or not the property is "serial" (auto-incrementing)
351
+ #
352
+ # @return <TrueClass, FalseClass> whether or not the property is "serial"
353
+ #-
354
+ # @api public
355
+ def serial?
356
+ @serial
357
+ end
358
+
359
+ # Returns whether or not the property can accept 'nil' as it's value
360
+ #
361
+ # @return <TrueClass, FalseClass> whether or not the property can accept 'nil'
362
+ #-
363
+ # @api public
364
+ def nullable?
365
+ @nullable
366
+ end
367
+
368
+ def custom?
369
+ @custom
370
+ end
371
+
372
+ # Provides a standardized getter method for the property
373
+ #
374
+ # @raise <ArgumentError> "+resource+ should be a DataMapper::Resource, but was ...."
375
+ #-
376
+ # @api private
377
+ def get(resource)
378
+ new_record = resource.new_record?
379
+
380
+ unless new_record || resource.attribute_loaded?(name)
381
+ # TODO: refactor this section
382
+ contexts = if lazy?
383
+ name
384
+ else
385
+ model.properties(resource.repository.name).reject do |property|
386
+ property.lazy? || resource.attribute_loaded?(property.name)
387
+ end
388
+ end
389
+ resource.send(:lazy_load, contexts)
390
+ end
391
+
392
+ value = get!(resource)
393
+
394
+ case track
395
+ when :hash
396
+ resource.original_values[name] = value.dup.hash unless resource.original_values.has_key?(name) rescue value.hash
397
+ when :get
398
+ resource.original_values[name] = value.dup unless resource.original_values.has_key?(name) rescue value
399
+ end
400
+
401
+ if value.nil? && new_record && !options[:default].nil? && !resource.attribute_loaded?(name)
402
+ value = default_for(resource)
403
+ set(resource, value)
404
+ end
405
+
406
+ value
407
+ end
408
+
409
+ def get!(resource)
410
+ resource.instance_variable_get(instance_variable_name)
411
+ end
412
+
413
+ # Provides a standardized setter method for the property
414
+ #
415
+ # @raise <ArgumentError> "+resource+ should be a DataMapper::Resource, but was ...."
416
+ #-
417
+ # @api private
418
+ def set(resource, value)
419
+ new_value = typecast(value)
420
+ old_value = get!(resource)
421
+
422
+ # skip setting the property if the new value is equal
423
+ # to the old value, and the old value was defined
424
+ return if new_value == old_value && resource.attribute_loaded?(name)
425
+
426
+ resource.original_values[name] = old_value unless resource.original_values.has_key?(name)
427
+
428
+ set!(resource, new_value)
429
+ end
430
+
431
+ def set!(resource, value)
432
+ resource.instance_variable_set(instance_variable_name, value)
433
+ end
434
+
435
+ # typecasts values into a primitive
436
+ #
437
+ # @return <TrueClass, String, Float, Integer, BigDecimal, DateTime, Date, Time
438
+ # Class> the primitive data-type, defaults to TrueClass
439
+ #-
440
+ # @api private
441
+ def typecast(value)
442
+ return value if value.kind_of?(type) || value.nil?
443
+ begin
444
+ if type == TrueClass then %w[ true 1 t ].include?(value.to_s.downcase)
445
+ elsif type == String then value.to_s
446
+ elsif type == Float then value.to_f
447
+ elsif type == Integer then value.to_i
448
+ elsif type == BigDecimal then BigDecimal(value.to_s)
449
+ elsif type == DateTime then typecast_to_datetime(value)
450
+ elsif type == Date then typecast_to_date(value)
451
+ elsif type == Time then typecast_to_time(value)
452
+ elsif type == Class then self.class.find_const(value)
453
+ else
454
+ value
455
+ end
456
+ rescue
457
+ value
458
+ end
459
+ end
460
+
461
+ def default_for(resource)
462
+ @default.respond_to?(:call) ? @default.call(resource, self) : @default
463
+ end
464
+
465
+ def inspect
466
+ "#<Property:#{@model}:#{@name}>"
467
+ end
468
+
469
+ private
470
+
471
+ def initialize(model, name, type, options = {})
472
+ assert_kind_of 'model', model, Model
473
+ assert_kind_of 'name', name, Symbol
474
+ assert_kind_of 'type', type, Class
475
+
476
+ if Fixnum == type
477
+ # It was decided that Integer is a more expressively names class to
478
+ # use instead of Fixnum. Fixnum only represents smaller numbers,
479
+ # so there was some confusion over whether or not it would also
480
+ # work with Bignum too (it will). Any Integer, which includes
481
+ # Fixnum and Bignum, can be stored in this property.
482
+ warn "#{type} properties are deprecated. Please use Integer instead"
483
+ type = Integer
484
+ end
485
+
486
+ unless TYPES.include?(type) || (DataMapper::Type > type && TYPES.include?(type.primitive))
487
+ raise ArgumentError, "+type+ was #{type.inspect}, which is not a supported type: #{TYPES * ', '}", caller
488
+ end
489
+
490
+ if (unknown_options = options.keys - PROPERTY_OPTIONS).any?
491
+ raise ArgumentError, "+options+ contained unknown keys: #{unknown_options * ', '}", caller
492
+ end
493
+
494
+ @model = model
495
+ @name = name.to_s.sub(/\?$/, '').to_sym
496
+ @type = type
497
+ @custom = DataMapper::Type > @type
498
+ @options = @custom ? @type.options.merge(options) : options
499
+ @instance_variable_name = "@#{@name}"
500
+
501
+ # TODO: This default should move to a DataMapper::Types::Text
502
+ # Custom-Type and out of Property.
503
+ @primitive = @options.fetch(:primitive, @type.respond_to?(:primitive) ? @type.primitive : @type)
504
+
505
+ @getter = TrueClass == @primitive ? "#{@name}?".to_sym : @name
506
+ @serial = @options.fetch(:serial, false)
507
+ @key = @options.fetch(:key, @serial || false)
508
+ @default = @options.fetch(:default, nil)
509
+ @nullable = @options.fetch(:nullable, @key == false)
510
+ @index = @options.fetch(:index, false)
511
+ @unique_index = @options.fetch(:unique_index, false)
512
+ @lazy = @options.fetch(:lazy, @type.respond_to?(:lazy) ? @type.lazy : false) && !@key
513
+
514
+ @track = @options.fetch(:track) do
515
+ if @custom && @type.respond_to?(:track) && @type.track
516
+ @type.track
517
+ else
518
+ IMMUTABLE_TYPES.include?(@primitive) ? :set : :get
519
+ end
520
+ end
521
+
522
+ # assign attributes per-type
523
+ if String == @primitive || Class == @primitive
524
+ @length = @options.fetch(:length, @options.fetch(:size, DEFAULT_LENGTH))
525
+ elsif BigDecimal == @primitive || Float == @primitive
526
+ @precision = @options.fetch(:precision, DEFAULT_PRECISION)
527
+ @scale = @options.fetch(:scale, DEFAULT_SCALE)
528
+
529
+ unless @precision > 0
530
+ raise ArgumentError, "precision must be greater than 0, but was #{@precision.inspect}"
531
+ end
532
+
533
+ unless @scale >= 0
534
+ raise ArgumentError, "scale must be equal to or greater than 0, but was #{@scale.inspect}"
535
+ end
536
+
537
+ unless @precision >= @scale
538
+ raise ArgumentError, "precision must be equal to or greater than scale, but was #{@precision.inspect} and scale was #{@scale.inspect}"
539
+ end
540
+ end
541
+
542
+ determine_visibility
543
+
544
+ @model.auto_generate_validations(self) if @model.respond_to?(:auto_generate_validations)
545
+ @model.property_serialization_setup(self) if @model.respond_to?(:property_serialization_setup)
546
+ end
547
+
548
+ def determine_visibility # :nodoc:
549
+ @reader_visibility = @options[:reader] || @options[:accessor] || :public
550
+ @writer_visibility = @options[:writer] || @options[:accessor] || :public
551
+ @writer_visibility = :protected if @options[:protected]
552
+ @writer_visibility = :private if @options[:private]
553
+
554
+ unless VISIBILITY_OPTIONS.include?(@reader_visibility) && VISIBILITY_OPTIONS.include?(@writer_visibility)
555
+ raise ArgumentError, 'property visibility must be :public, :protected, or :private', caller(2)
556
+ end
557
+ end
558
+
559
+ # Typecasts an arbitrary value to a DateTime
560
+ def typecast_to_datetime(value)
561
+ case value
562
+ when Hash then typecast_hash_to_datetime(value)
563
+ else DateTime.parse(value.to_s)
564
+ end
565
+ end
566
+
567
+ # Typecasts an arbitrary value to a Date
568
+ def typecast_to_date(value)
569
+ case value
570
+ when Hash then typecast_hash_to_date(value)
571
+ else Date.parse(value.to_s)
572
+ end
573
+ end
574
+
575
+ # Typecasts an arbitrary value to a Time
576
+ def typecast_to_time(value)
577
+ case value
578
+ when Hash then typecast_hash_to_time(value)
579
+ else Time.parse(value.to_s)
580
+ end
581
+ end
582
+
583
+ def typecast_hash_to_datetime(hash)
584
+ args = extract_time_args_from_hash(hash, :year, :month, :day, :hour, :min, :sec)
585
+ DateTime.new(*args)
586
+ rescue ArgumentError
587
+ t = typecast_hash_to_time(hash)
588
+ DateTime.new(t.year, t.month, t.day, t.hour, t.min, t.sec)
589
+ end
590
+
591
+ def typecast_hash_to_date(hash)
592
+ args = extract_time_args_from_hash(hash, :year, :month, :day)
593
+ Date.new(*args)
594
+ rescue ArgumentError
595
+ t = typecast_hash_to_time(hash)
596
+ Date.new(t.year, t.month, t.day)
597
+ end
598
+
599
+ def typecast_hash_to_time(hash)
600
+ args = extract_time_args_from_hash(hash, :year, :month, :day, :hour, :min, :sec)
601
+ Time.local(*args)
602
+ end
603
+
604
+ # Extracts the given args from the hash. If a value does not exist, it
605
+ # uses the value of Time.now
606
+ def extract_time_args_from_hash(hash, *args)
607
+ now = Time.now
608
+ args.map { |arg| hash[arg] || hash[arg.to_s] || now.send(arg) }
609
+ end
610
+ end # class Property
611
+ end # module DataMapper