sam-dm-core 0.9.6

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 (126) hide show
  1. data/.autotest +26 -0
  2. data/CONTRIBUTING +51 -0
  3. data/FAQ +92 -0
  4. data/History.txt +145 -0
  5. data/MIT-LICENSE +22 -0
  6. data/Manifest.txt +125 -0
  7. data/QUICKLINKS +12 -0
  8. data/README.txt +143 -0
  9. data/Rakefile +30 -0
  10. data/SPECS +63 -0
  11. data/TODO +1 -0
  12. data/lib/dm-core.rb +224 -0
  13. data/lib/dm-core/adapters.rb +4 -0
  14. data/lib/dm-core/adapters/abstract_adapter.rb +202 -0
  15. data/lib/dm-core/adapters/data_objects_adapter.rb +707 -0
  16. data/lib/dm-core/adapters/mysql_adapter.rb +136 -0
  17. data/lib/dm-core/adapters/postgres_adapter.rb +188 -0
  18. data/lib/dm-core/adapters/sqlite3_adapter.rb +105 -0
  19. data/lib/dm-core/associations.rb +199 -0
  20. data/lib/dm-core/associations/many_to_many.rb +147 -0
  21. data/lib/dm-core/associations/many_to_one.rb +107 -0
  22. data/lib/dm-core/associations/one_to_many.rb +309 -0
  23. data/lib/dm-core/associations/one_to_one.rb +61 -0
  24. data/lib/dm-core/associations/relationship.rb +218 -0
  25. data/lib/dm-core/associations/relationship_chain.rb +81 -0
  26. data/lib/dm-core/auto_migrations.rb +113 -0
  27. data/lib/dm-core/collection.rb +638 -0
  28. data/lib/dm-core/dependency_queue.rb +31 -0
  29. data/lib/dm-core/hook.rb +11 -0
  30. data/lib/dm-core/identity_map.rb +45 -0
  31. data/lib/dm-core/is.rb +16 -0
  32. data/lib/dm-core/logger.rb +232 -0
  33. data/lib/dm-core/migrations/destructive_migrations.rb +17 -0
  34. data/lib/dm-core/migrator.rb +29 -0
  35. data/lib/dm-core/model.rb +471 -0
  36. data/lib/dm-core/naming_conventions.rb +84 -0
  37. data/lib/dm-core/property.rb +673 -0
  38. data/lib/dm-core/property_set.rb +162 -0
  39. data/lib/dm-core/query.rb +625 -0
  40. data/lib/dm-core/repository.rb +159 -0
  41. data/lib/dm-core/resource.rb +637 -0
  42. data/lib/dm-core/scope.rb +58 -0
  43. data/lib/dm-core/support.rb +7 -0
  44. data/lib/dm-core/support/array.rb +13 -0
  45. data/lib/dm-core/support/assertions.rb +8 -0
  46. data/lib/dm-core/support/errors.rb +23 -0
  47. data/lib/dm-core/support/kernel.rb +7 -0
  48. data/lib/dm-core/support/symbol.rb +41 -0
  49. data/lib/dm-core/transaction.rb +267 -0
  50. data/lib/dm-core/type.rb +160 -0
  51. data/lib/dm-core/type_map.rb +80 -0
  52. data/lib/dm-core/types.rb +19 -0
  53. data/lib/dm-core/types/boolean.rb +7 -0
  54. data/lib/dm-core/types/discriminator.rb +34 -0
  55. data/lib/dm-core/types/object.rb +24 -0
  56. data/lib/dm-core/types/paranoid_boolean.rb +34 -0
  57. data/lib/dm-core/types/paranoid_datetime.rb +33 -0
  58. data/lib/dm-core/types/serial.rb +9 -0
  59. data/lib/dm-core/types/text.rb +10 -0
  60. data/lib/dm-core/version.rb +3 -0
  61. data/script/all +5 -0
  62. data/script/performance.rb +203 -0
  63. data/script/profile.rb +87 -0
  64. data/spec/integration/association_spec.rb +1371 -0
  65. data/spec/integration/association_through_spec.rb +203 -0
  66. data/spec/integration/associations/many_to_many_spec.rb +449 -0
  67. data/spec/integration/associations/many_to_one_spec.rb +163 -0
  68. data/spec/integration/associations/one_to_many_spec.rb +151 -0
  69. data/spec/integration/auto_migrations_spec.rb +398 -0
  70. data/spec/integration/collection_spec.rb +1069 -0
  71. data/spec/integration/data_objects_adapter_spec.rb +32 -0
  72. data/spec/integration/dependency_queue_spec.rb +58 -0
  73. data/spec/integration/model_spec.rb +127 -0
  74. data/spec/integration/mysql_adapter_spec.rb +85 -0
  75. data/spec/integration/postgres_adapter_spec.rb +731 -0
  76. data/spec/integration/property_spec.rb +233 -0
  77. data/spec/integration/query_spec.rb +506 -0
  78. data/spec/integration/repository_spec.rb +57 -0
  79. data/spec/integration/resource_spec.rb +475 -0
  80. data/spec/integration/sqlite3_adapter_spec.rb +352 -0
  81. data/spec/integration/sti_spec.rb +208 -0
  82. data/spec/integration/strategic_eager_loading_spec.rb +138 -0
  83. data/spec/integration/transaction_spec.rb +75 -0
  84. data/spec/integration/type_spec.rb +271 -0
  85. data/spec/lib/logging_helper.rb +18 -0
  86. data/spec/lib/mock_adapter.rb +27 -0
  87. data/spec/lib/model_loader.rb +91 -0
  88. data/spec/lib/publicize_methods.rb +28 -0
  89. data/spec/models/vehicles.rb +34 -0
  90. data/spec/models/zoo.rb +47 -0
  91. data/spec/spec.opts +3 -0
  92. data/spec/spec_helper.rb +86 -0
  93. data/spec/unit/adapters/abstract_adapter_spec.rb +133 -0
  94. data/spec/unit/adapters/adapter_shared_spec.rb +15 -0
  95. data/spec/unit/adapters/data_objects_adapter_spec.rb +628 -0
  96. data/spec/unit/adapters/postgres_adapter_spec.rb +133 -0
  97. data/spec/unit/associations/many_to_many_spec.rb +17 -0
  98. data/spec/unit/associations/many_to_one_spec.rb +152 -0
  99. data/spec/unit/associations/one_to_many_spec.rb +393 -0
  100. data/spec/unit/associations/one_to_one_spec.rb +7 -0
  101. data/spec/unit/associations/relationship_spec.rb +71 -0
  102. data/spec/unit/associations_spec.rb +242 -0
  103. data/spec/unit/auto_migrations_spec.rb +111 -0
  104. data/spec/unit/collection_spec.rb +182 -0
  105. data/spec/unit/data_mapper_spec.rb +35 -0
  106. data/spec/unit/identity_map_spec.rb +126 -0
  107. data/spec/unit/is_spec.rb +80 -0
  108. data/spec/unit/migrator_spec.rb +33 -0
  109. data/spec/unit/model_spec.rb +339 -0
  110. data/spec/unit/naming_conventions_spec.rb +36 -0
  111. data/spec/unit/property_set_spec.rb +83 -0
  112. data/spec/unit/property_spec.rb +753 -0
  113. data/spec/unit/query_spec.rb +530 -0
  114. data/spec/unit/repository_spec.rb +93 -0
  115. data/spec/unit/resource_spec.rb +626 -0
  116. data/spec/unit/scope_spec.rb +142 -0
  117. data/spec/unit/transaction_spec.rb +493 -0
  118. data/spec/unit/type_map_spec.rb +114 -0
  119. data/spec/unit/type_spec.rb +119 -0
  120. data/tasks/ci.rb +68 -0
  121. data/tasks/dm.rb +63 -0
  122. data/tasks/doc.rb +20 -0
  123. data/tasks/gemspec.rb +23 -0
  124. data/tasks/hoe.rb +46 -0
  125. data/tasks/install.rb +20 -0
  126. metadata +216 -0
@@ -0,0 +1,84 @@
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::Resource::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::Resource::Underscored
25
+ module NamingConventions
26
+
27
+ module Resource
28
+
29
+ module UnderscoredAndPluralized
30
+ def self.call(name)
31
+ Extlib::Inflection.pluralize(Extlib::Inflection.underscore(name)).gsub('/','_')
32
+ end
33
+ end # module UnderscoredAndPluralized
34
+
35
+ module UnderscoredAndPluralizedWithoutModule
36
+ def self.call(name)
37
+ Extlib::Inflection.pluralize(Extlib::Inflection.underscore(Extlib::Inflection.demodulize(name)))
38
+ end
39
+ end # module UnderscoredAndPluralizedWithoutModule
40
+
41
+ module Underscored
42
+ def self.call(name)
43
+ Extlib::Inflection.underscore(name)
44
+ end
45
+ end # module Underscored
46
+
47
+ module Yaml
48
+ def self.call(name)
49
+ Extlib::Inflection.pluralize(Extlib::Inflection.underscore(name)) + ".yaml"
50
+ end
51
+ end # module Yaml
52
+
53
+ end # module Resource
54
+
55
+ module Field
56
+
57
+ module UnderscoredAndPluralized
58
+ def self.call(property)
59
+ Extlib::Inflection.pluralize(Extlib::Inflection.underscore(property.name.to_s)).gsub('/','_')
60
+ end
61
+ end # module UnderscoredAndPluralized
62
+
63
+ module UnderscoredAndPluralizedWithoutModule
64
+ def self.call(property)
65
+ Extlib::Inflection.pluralize(Extlib::Inflection.underscore(Extlib::Inflection.demodulize(property.name.to_s)))
66
+ end
67
+ end # module UnderscoredAndPluralizedWithoutModule
68
+
69
+ module Underscored
70
+ def self.call(property)
71
+ Extlib::Inflection.underscore(property.name.to_s)
72
+ end
73
+ end # module Underscored
74
+
75
+ module Yaml
76
+ def self.call(property)
77
+ Extlib::Inflection.pluralize(Extlib::Inflection.underscore(property.name.to_s)) + ".yaml"
78
+ end
79
+ end # module Yaml
80
+
81
+ end # module Field
82
+
83
+ end # module NamingConventions
84
+ end # module DataMapper
@@ -0,0 +1,673 @@
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
+ # == Default Values
224
+ # To set a default for a property, use the <tt>:default</tt> key. The
225
+ # property will be set to the value associated with that key the first time
226
+ # it is accessed, or when the resource is saved if it hasn't been set with
227
+ # another value already. This value can be a static value, such as 'hello'
228
+ # but it can also be a proc that will be evaluated when the property is read
229
+ # before its value has been set. The property is set to the return of the
230
+ # proc. The proc is passed two values, the resource the property is being set
231
+ # for and the property itself.
232
+ #
233
+ # property :display_name, String, :default => { |r, p| r.login }
234
+ #
235
+ # Word of warning. Don't try to read the value of the property you're setting
236
+ # the default for in the proc. An infinite loop will ensue.
237
+ #
238
+ # == Embedded Values
239
+ # As an alternative to extraneous has_one relationships, consider using an
240
+ # EmbeddedValue.
241
+ #
242
+ # == Misc. Notes
243
+ # * Properties declared as strings will default to a length of 50, rather than
244
+ # 255 (typical max varchar column size). To overload the default, pass
245
+ # <tt>:length => 255</tt> or <tt>:length => 0..255</tt>. Since DataMapper
246
+ # does not introspect for properties, this means that legacy database tables
247
+ # may need their <tt>String</tt> columns defined with a <tt>:length</tt> so
248
+ # that DM does not apply an un-needed length validation, or allow overflow.
249
+ # * You may declare a Property with the data-type of <tt>Class</tt>.
250
+ # see SingleTableInheritance for more on how to use <tt>Class</tt> columns.
251
+ class Property
252
+ include Assertions
253
+
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
259
+ # them here
260
+ PROPERTY_OPTIONS = [
261
+ :accessor, :reader, :writer,
262
+ :lazy, :default, :nullable, :key, :serial, :field, :size, :length,
263
+ :format, :index, :unique_index, :check, :ordinal, :auto_validation,
264
+ :validates, :unique, :track, :precision, :scale
265
+ ]
266
+
267
+ # FIXME: can we pull the keys from
268
+ # DataMapper::Adapters::DataObjectsAdapter::TYPES
269
+ # for this?
270
+ TYPES = [
271
+ TrueClass,
272
+ String,
273
+ DataMapper::Types::Text,
274
+ Float,
275
+ Integer,
276
+ BigDecimal,
277
+ DateTime,
278
+ Date,
279
+ Time,
280
+ Object,
281
+ Class,
282
+ DataMapper::Types::Discriminator,
283
+ DataMapper::Types::Serial
284
+ ]
285
+
286
+ IMMUTABLE_TYPES = [ TrueClass, Float, Integer, BigDecimal]
287
+
288
+ VISIBILITY_OPTIONS = [ :public, :protected, :private ]
289
+
290
+ DEFAULT_LENGTH = 50
291
+ DEFAULT_PRECISION = 10
292
+ DEFAULT_SCALE_BIGDECIMAL = 0
293
+ DEFAULT_SCALE_FLOAT = nil
294
+
295
+ attr_reader :primitive, :model, :name, :instance_variable_name,
296
+ :type, :reader_visibility, :writer_visibility, :getter, :options,
297
+ :default, :precision, :scale, :track, :extra_options
298
+
299
+ # Supplies the field in the data-store which the property corresponds to
300
+ #
301
+ # @return <String> name of field in data-store
302
+ # -
303
+ # @api semi-public
304
+ def field(repository_name = nil)
305
+ @field || fields[repository_name]
306
+ end
307
+
308
+ def unique
309
+ @unique ||= @options.fetch(:unique, @serial || @key || false)
310
+ end
311
+
312
+ def hash
313
+ if @custom && !@bound
314
+ @type.bind(self)
315
+ @bound = true
316
+ end
317
+
318
+ return @model.hash + @name.hash
319
+ end
320
+
321
+ def eql?(o)
322
+ if o.is_a?(Property)
323
+ return o.model == @model && o.name == @name
324
+ else
325
+ return false
326
+ end
327
+ end
328
+
329
+ def length
330
+ @length.is_a?(Range) ? @length.max : @length
331
+ end
332
+ alias size length
333
+
334
+ def index
335
+ @index
336
+ end
337
+
338
+ def unique_index
339
+ @unique_index
340
+ end
341
+
342
+ # Returns whether or not the property is to be lazy-loaded
343
+ #
344
+ # @return <TrueClass, FalseClass> whether or not the property is to be
345
+ # lazy-loaded
346
+ # -
347
+ # @api public
348
+ def lazy?
349
+ @lazy
350
+ end
351
+
352
+ # Returns whether or not the property is a key or a part of a key
353
+ #
354
+ # @return <TrueClass, FalseClass> whether the property is a key or a part of
355
+ # a key
356
+ #-
357
+ # @api public
358
+ def key?
359
+ @key
360
+ end
361
+
362
+ # Returns whether or not the property is "serial" (auto-incrementing)
363
+ #
364
+ # @return <TrueClass, FalseClass> whether or not the property is "serial"
365
+ #-
366
+ # @api public
367
+ def serial?
368
+ @serial
369
+ end
370
+
371
+ # Returns whether or not the property can accept 'nil' as it's value
372
+ #
373
+ # @return <TrueClass, FalseClass> whether or not the property can accept 'nil'
374
+ #-
375
+ # @api public
376
+ def nullable?
377
+ @nullable
378
+ end
379
+
380
+ def custom?
381
+ @custom
382
+ end
383
+
384
+ # Provides a standardized getter method for the property
385
+ #
386
+ # @raise <ArgumentError> "+resource+ should be a DataMapper::Resource, but was ...."
387
+ #-
388
+ # @api private
389
+ def get(resource)
390
+ lazy_load(resource)
391
+
392
+ value = get!(resource)
393
+
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)
401
+ end
402
+
403
+ value
404
+ end
405
+
406
+ def get!(resource)
407
+ resource.instance_variable_get(instance_variable_name)
408
+ end
409
+
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
415
+ end
416
+ end
417
+
418
+ # Provides a standardized setter method for the property
419
+ #
420
+ # @raise <ArgumentError> "+resource+ should be a DataMapper::Resource, but was ...."
421
+ #-
422
+ # @api private
423
+ 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)
431
+
432
+ new_value = typecast(value)
433
+ old_value = get!(resource)
434
+
435
+ # skip setting the property if the new value is equal
436
+ # to the old value, and the old value was defined
437
+ # ---
438
+ # [YK] Why bother? Does this change the result at all?
439
+ # ---
440
+ # return if new_value == old_value && resource.attribute_loaded?(name)
441
+
442
+ set_original_value(resource, old_value)
443
+
444
+ set!(resource, new_value)
445
+ end
446
+
447
+ def set!(resource, value)
448
+ resource.instance_variable_set(instance_variable_name, value)
449
+ end
450
+
451
+ # Loads lazy columns when get or set is called.
452
+ #-
453
+ # @api private
454
+ def lazy_load(resource)
455
+ # It is faster to bail out at at a new_record? rather than to process
456
+ # which properties would be loaded and then not load them.
457
+ return if resource.new_record? || resource.attribute_loaded?(name)
458
+ # If we're trying to load a lazy property, load it. Otherwise, lazy-load
459
+ # any properties that should be eager-loaded but were not included
460
+ # in the original :fields list
461
+ contexts = lazy? ? name : model.eager_properties(resource.repository.name).map {|property| property.name}
462
+ resource.send(:lazy_load, contexts)
463
+ end
464
+
465
+ # typecasts values into a primitive
466
+ #
467
+ # @return <TrueClass, String, Float, Integer, BigDecimal, DateTime, Date, Time
468
+ # Class> the primitive data-type, defaults to TrueClass
469
+ #-
470
+ # @api private
471
+ def typecast(value)
472
+ return type.typecast(value, self) if type.respond_to?(:typecast)
473
+ return value if value.kind_of?(primitive) || value.nil?
474
+ begin
475
+ if primitive == TrueClass then %w[ true 1 t ].include?(value.to_s.downcase)
476
+ elsif primitive == String then value.to_s
477
+ elsif primitive == Float then value.to_f
478
+ elsif primitive == Integer
479
+ # The simplest possible implementation, i.e. value.to_i, is not
480
+ # desirable because "junk".to_i gives "0". We want nil instead,
481
+ # because this makes it clear that the typecast failed.
482
+ #
483
+ # After benchmarking, we preferred the current implementation over
484
+ # these two alternatives:
485
+ # * Integer(value) rescue nil
486
+ # * Integer(value_to_s =~ /(\d+)/ ? $1 : value_to_s) rescue nil
487
+ #
488
+ # [YK] The previous implementation used a rescue. Why use a rescue
489
+ # when the list of cases where a valid string other than "0" could
490
+ # produce 0 is known?
491
+ value_to_i = value.to_i
492
+ if value_to_i == 0
493
+ value.to_s =~ /^(0x|0b)?0+/ ? 0 : nil
494
+ else
495
+ value_to_i
496
+ end
497
+ elsif primitive == BigDecimal then BigDecimal(value.to_s)
498
+ elsif primitive == DateTime then typecast_to_datetime(value)
499
+ elsif primitive == Date then typecast_to_date(value)
500
+ elsif primitive == Time then typecast_to_time(value)
501
+ elsif primitive == Class then self.class.find_const(value)
502
+ else
503
+ value
504
+ end
505
+ rescue
506
+ value
507
+ end
508
+ end
509
+
510
+ def default_for(resource)
511
+ @default.respond_to?(:call) ? @default.call(resource, self) : @default
512
+ end
513
+
514
+ def value(val)
515
+ custom? ? self.type.dump(val, self) : val
516
+ end
517
+
518
+ def inspect
519
+ "#<Property:#{@model}:#{@name}>"
520
+ end
521
+
522
+ private
523
+
524
+ def initialize(model, name, type, options = {})
525
+ assert_kind_of 'model', model, Model
526
+ assert_kind_of 'name', name, Symbol
527
+ assert_kind_of 'type', type, Class
528
+
529
+ if Fixnum == type
530
+ # It was decided that Integer is a more expressively names class to
531
+ # use instead of Fixnum. Fixnum only represents smaller numbers,
532
+ # so there was some confusion over whether or not it would also
533
+ # work with Bignum too (it will). Any Integer, which includes
534
+ # Fixnum and Bignum, can be stored in this property.
535
+ warn "#{type} properties are deprecated. Please use Integer instead"
536
+ type = Integer
537
+ end
538
+
539
+ unless TYPES.include?(type) || (DataMapper::Type > type && TYPES.include?(type.primitive))
540
+ raise ArgumentError, "+type+ was #{type.inspect}, which is not a supported type: #{TYPES * ', '}", caller
541
+ end
542
+
543
+ @extra_options = {}
544
+ (options.keys - PROPERTY_OPTIONS).each do |key|
545
+ @extra_options[key] = options.delete(key)
546
+ end
547
+
548
+ @model = model
549
+ @name = name.to_s.sub(/\?$/, '').to_sym
550
+ @type = type
551
+ @custom = DataMapper::Type > @type
552
+ @options = @custom ? @type.options.merge(options) : options
553
+ @instance_variable_name = "@#{@name}"
554
+
555
+ # TODO: This default should move to a DataMapper::Types::Text
556
+ # Custom-Type and out of Property.
557
+ @primitive = @options.fetch(:primitive, @type.respond_to?(:primitive) ? @type.primitive : @type)
558
+
559
+ @getter = TrueClass == @primitive ? "#{@name}?".to_sym : @name
560
+ @field = @options.fetch(:field, nil)
561
+ @serial = @options.fetch(:serial, false)
562
+ @key = @options.fetch(:key, @serial || false)
563
+ @default = @options.fetch(:default, nil)
564
+ @nullable = @options.fetch(:nullable, @key == false)
565
+ @index = @options.fetch(:index, false)
566
+ @unique_index = @options.fetch(:unique_index, false)
567
+ @lazy = @options.fetch(:lazy, @type.respond_to?(:lazy) ? @type.lazy : false) && !@key
568
+
569
+ @track = @options.fetch(:track) do
570
+ if @custom && @type.respond_to?(:track) && @type.track
571
+ @type.track
572
+ else
573
+ IMMUTABLE_TYPES.include?(@primitive) ? :set : :get
574
+ end
575
+ end
576
+
577
+ # assign attributes per-type
578
+ if String == @primitive || Class == @primitive
579
+ @length = @options.fetch(:length, @options.fetch(:size, DEFAULT_LENGTH))
580
+ elsif BigDecimal == @primitive || Float == @primitive
581
+ @precision = @options.fetch(:precision, DEFAULT_PRECISION)
582
+
583
+ default_scale = (Float == @primitive) ? DEFAULT_SCALE_FLOAT : DEFAULT_SCALE_BIGDECIMAL
584
+ @scale = @options.fetch(:scale, default_scale)
585
+ # @scale = @options.fetch(:scale, DEFAULT_SCALE_BIGDECIMAL)
586
+
587
+ unless @precision > 0
588
+ raise ArgumentError, "precision must be greater than 0, but was #{@precision.inspect}"
589
+ end
590
+
591
+ if (BigDecimal == @primitive) || (Float == @primitive && !@scale.nil?)
592
+ unless @scale >= 0
593
+ raise ArgumentError, "scale must be equal to or greater than 0, but was #{@scale.inspect}"
594
+ end
595
+
596
+ unless @precision >= @scale
597
+ raise ArgumentError, "precision must be equal to or greater than scale, but was #{@precision.inspect} and scale was #{@scale.inspect}"
598
+ end
599
+ end
600
+ end
601
+
602
+ determine_visibility
603
+
604
+ @model.auto_generate_validations(self) if @model.respond_to?(:auto_generate_validations)
605
+ @model.property_serialization_setup(self) if @model.respond_to?(:property_serialization_setup)
606
+ end
607
+
608
+ def fields
609
+ @fields ||= Hash.new { |h,k| h[k] = self.model.field_naming_conventions[k].call(self) }
610
+ end
611
+
612
+ def determine_visibility # :nodoc:
613
+ @reader_visibility = @options[:reader] || @options[:accessor] || :public
614
+ @writer_visibility = @options[:writer] || @options[:accessor] || :public
615
+
616
+ unless VISIBILITY_OPTIONS.include?(@reader_visibility) && VISIBILITY_OPTIONS.include?(@writer_visibility)
617
+ raise ArgumentError, 'property visibility must be :public, :protected, or :private', caller(2)
618
+ end
619
+ end
620
+
621
+ # Typecasts an arbitrary value to a DateTime
622
+ def typecast_to_datetime(value)
623
+ case value
624
+ when Hash then typecast_hash_to_datetime(value)
625
+ else DateTime.parse(value.to_s)
626
+ end
627
+ end
628
+
629
+ # Typecasts an arbitrary value to a Date
630
+ def typecast_to_date(value)
631
+ case value
632
+ when Hash then typecast_hash_to_date(value)
633
+ else Date.parse(value.to_s)
634
+ end
635
+ end
636
+
637
+ # Typecasts an arbitrary value to a Time
638
+ def typecast_to_time(value)
639
+ case value
640
+ when Hash then typecast_hash_to_time(value)
641
+ else Time.parse(value.to_s)
642
+ end
643
+ end
644
+
645
+ def typecast_hash_to_datetime(hash)
646
+ args = extract_time_args_from_hash(hash, :year, :month, :day, :hour, :min, :sec)
647
+ DateTime.new(*args)
648
+ rescue ArgumentError => e
649
+ t = typecast_hash_to_time(hash)
650
+ DateTime.new(t.year, t.month, t.day, t.hour, t.min, t.sec)
651
+ end
652
+
653
+ def typecast_hash_to_date(hash)
654
+ args = extract_time_args_from_hash(hash, :year, :month, :day)
655
+ Date.new(*args)
656
+ rescue ArgumentError
657
+ t = typecast_hash_to_time(hash)
658
+ Date.new(t.year, t.month, t.day)
659
+ end
660
+
661
+ def typecast_hash_to_time(hash)
662
+ args = extract_time_args_from_hash(hash, :year, :month, :day, :hour, :min, :sec)
663
+ Time.local(*args)
664
+ end
665
+
666
+ # Extracts the given args from the hash. If a value does not exist, it
667
+ # uses the value of Time.now
668
+ def extract_time_args_from_hash(hash, *args)
669
+ now = Time.now
670
+ args.map { |arg| hash[arg] || hash[arg.to_s] || now.send(arg) }
671
+ end
672
+ end # class Property
673
+ end # module DataMapper