sam-dm-core 0.9.6
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +26 -0
- data/CONTRIBUTING +51 -0
- data/FAQ +92 -0
- data/History.txt +145 -0
- data/MIT-LICENSE +22 -0
- data/Manifest.txt +125 -0
- data/QUICKLINKS +12 -0
- data/README.txt +143 -0
- data/Rakefile +30 -0
- data/SPECS +63 -0
- data/TODO +1 -0
- data/lib/dm-core.rb +224 -0
- data/lib/dm-core/adapters.rb +4 -0
- data/lib/dm-core/adapters/abstract_adapter.rb +202 -0
- data/lib/dm-core/adapters/data_objects_adapter.rb +707 -0
- data/lib/dm-core/adapters/mysql_adapter.rb +136 -0
- data/lib/dm-core/adapters/postgres_adapter.rb +188 -0
- data/lib/dm-core/adapters/sqlite3_adapter.rb +105 -0
- data/lib/dm-core/associations.rb +199 -0
- data/lib/dm-core/associations/many_to_many.rb +147 -0
- data/lib/dm-core/associations/many_to_one.rb +107 -0
- data/lib/dm-core/associations/one_to_many.rb +309 -0
- data/lib/dm-core/associations/one_to_one.rb +61 -0
- data/lib/dm-core/associations/relationship.rb +218 -0
- data/lib/dm-core/associations/relationship_chain.rb +81 -0
- data/lib/dm-core/auto_migrations.rb +113 -0
- data/lib/dm-core/collection.rb +638 -0
- data/lib/dm-core/dependency_queue.rb +31 -0
- data/lib/dm-core/hook.rb +11 -0
- data/lib/dm-core/identity_map.rb +45 -0
- data/lib/dm-core/is.rb +16 -0
- data/lib/dm-core/logger.rb +232 -0
- data/lib/dm-core/migrations/destructive_migrations.rb +17 -0
- data/lib/dm-core/migrator.rb +29 -0
- data/lib/dm-core/model.rb +471 -0
- data/lib/dm-core/naming_conventions.rb +84 -0
- data/lib/dm-core/property.rb +673 -0
- data/lib/dm-core/property_set.rb +162 -0
- data/lib/dm-core/query.rb +625 -0
- data/lib/dm-core/repository.rb +159 -0
- data/lib/dm-core/resource.rb +637 -0
- data/lib/dm-core/scope.rb +58 -0
- data/lib/dm-core/support.rb +7 -0
- data/lib/dm-core/support/array.rb +13 -0
- data/lib/dm-core/support/assertions.rb +8 -0
- data/lib/dm-core/support/errors.rb +23 -0
- data/lib/dm-core/support/kernel.rb +7 -0
- data/lib/dm-core/support/symbol.rb +41 -0
- data/lib/dm-core/transaction.rb +267 -0
- data/lib/dm-core/type.rb +160 -0
- data/lib/dm-core/type_map.rb +80 -0
- data/lib/dm-core/types.rb +19 -0
- data/lib/dm-core/types/boolean.rb +7 -0
- data/lib/dm-core/types/discriminator.rb +34 -0
- data/lib/dm-core/types/object.rb +24 -0
- data/lib/dm-core/types/paranoid_boolean.rb +34 -0
- data/lib/dm-core/types/paranoid_datetime.rb +33 -0
- data/lib/dm-core/types/serial.rb +9 -0
- data/lib/dm-core/types/text.rb +10 -0
- data/lib/dm-core/version.rb +3 -0
- data/script/all +5 -0
- data/script/performance.rb +203 -0
- data/script/profile.rb +87 -0
- data/spec/integration/association_spec.rb +1371 -0
- data/spec/integration/association_through_spec.rb +203 -0
- data/spec/integration/associations/many_to_many_spec.rb +449 -0
- data/spec/integration/associations/many_to_one_spec.rb +163 -0
- data/spec/integration/associations/one_to_many_spec.rb +151 -0
- data/spec/integration/auto_migrations_spec.rb +398 -0
- data/spec/integration/collection_spec.rb +1069 -0
- data/spec/integration/data_objects_adapter_spec.rb +32 -0
- data/spec/integration/dependency_queue_spec.rb +58 -0
- data/spec/integration/model_spec.rb +127 -0
- data/spec/integration/mysql_adapter_spec.rb +85 -0
- data/spec/integration/postgres_adapter_spec.rb +731 -0
- data/spec/integration/property_spec.rb +233 -0
- data/spec/integration/query_spec.rb +506 -0
- data/spec/integration/repository_spec.rb +57 -0
- data/spec/integration/resource_spec.rb +475 -0
- data/spec/integration/sqlite3_adapter_spec.rb +352 -0
- data/spec/integration/sti_spec.rb +208 -0
- data/spec/integration/strategic_eager_loading_spec.rb +138 -0
- data/spec/integration/transaction_spec.rb +75 -0
- data/spec/integration/type_spec.rb +271 -0
- data/spec/lib/logging_helper.rb +18 -0
- data/spec/lib/mock_adapter.rb +27 -0
- data/spec/lib/model_loader.rb +91 -0
- data/spec/lib/publicize_methods.rb +28 -0
- data/spec/models/vehicles.rb +34 -0
- data/spec/models/zoo.rb +47 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +86 -0
- data/spec/unit/adapters/abstract_adapter_spec.rb +133 -0
- data/spec/unit/adapters/adapter_shared_spec.rb +15 -0
- data/spec/unit/adapters/data_objects_adapter_spec.rb +628 -0
- data/spec/unit/adapters/postgres_adapter_spec.rb +133 -0
- data/spec/unit/associations/many_to_many_spec.rb +17 -0
- data/spec/unit/associations/many_to_one_spec.rb +152 -0
- data/spec/unit/associations/one_to_many_spec.rb +393 -0
- data/spec/unit/associations/one_to_one_spec.rb +7 -0
- data/spec/unit/associations/relationship_spec.rb +71 -0
- data/spec/unit/associations_spec.rb +242 -0
- data/spec/unit/auto_migrations_spec.rb +111 -0
- data/spec/unit/collection_spec.rb +182 -0
- data/spec/unit/data_mapper_spec.rb +35 -0
- data/spec/unit/identity_map_spec.rb +126 -0
- data/spec/unit/is_spec.rb +80 -0
- data/spec/unit/migrator_spec.rb +33 -0
- data/spec/unit/model_spec.rb +339 -0
- data/spec/unit/naming_conventions_spec.rb +36 -0
- data/spec/unit/property_set_spec.rb +83 -0
- data/spec/unit/property_spec.rb +753 -0
- data/spec/unit/query_spec.rb +530 -0
- data/spec/unit/repository_spec.rb +93 -0
- data/spec/unit/resource_spec.rb +626 -0
- data/spec/unit/scope_spec.rb +142 -0
- data/spec/unit/transaction_spec.rb +493 -0
- data/spec/unit/type_map_spec.rb +114 -0
- data/spec/unit/type_spec.rb +119 -0
- data/tasks/ci.rb +68 -0
- data/tasks/dm.rb +63 -0
- data/tasks/doc.rb +20 -0
- data/tasks/gemspec.rb +23 -0
- data/tasks/hoe.rb +46 -0
- data/tasks/install.rb +20 -0
- 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
|