datamapper-shim 0.0.1
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.
- data/Gemfile +4 -0
- data/Gemfile.lock +45 -0
- data/datamapper-shim.gemspec +30 -0
- data/lib/data_mapper/shim.rb +724 -0
- data/test/data_mapper/shim_test.rb +1258 -0
- metadata +211 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activemodel (3.2.16)
|
5
|
+
activesupport (= 3.2.16)
|
6
|
+
builder (~> 3.0.0)
|
7
|
+
activerecord (3.2.16)
|
8
|
+
activemodel (= 3.2.16)
|
9
|
+
activesupport (= 3.2.16)
|
10
|
+
arel (~> 3.0.2)
|
11
|
+
tzinfo (~> 0.3.29)
|
12
|
+
activesupport (3.2.16)
|
13
|
+
i18n (~> 0.6, >= 0.6.4)
|
14
|
+
multi_json (~> 1.0)
|
15
|
+
arel (3.0.3)
|
16
|
+
attribute-defaults (0.6.0)
|
17
|
+
activerecord (>= 3.0.0)
|
18
|
+
builder (3.0.4)
|
19
|
+
coderay (1.1.0)
|
20
|
+
composite_primary_keys (5.0.13)
|
21
|
+
activerecord (~> 3.2.0, >= 3.2.9)
|
22
|
+
i18n (0.6.9)
|
23
|
+
json (1.8.1)
|
24
|
+
method_source (0.8.2)
|
25
|
+
minitest (5.2.3)
|
26
|
+
multi_json (1.8.4)
|
27
|
+
pry (0.9.12.4)
|
28
|
+
coderay (~> 1.0)
|
29
|
+
method_source (~> 0.8)
|
30
|
+
slop (~> 3.4)
|
31
|
+
slop (3.4.7)
|
32
|
+
sqlite3 (1.3.8)
|
33
|
+
tzinfo (0.3.38)
|
34
|
+
|
35
|
+
PLATFORMS
|
36
|
+
ruby
|
37
|
+
|
38
|
+
DEPENDENCIES
|
39
|
+
activerecord (~> 3.2.0)
|
40
|
+
attribute-defaults (~> 0.6.0)
|
41
|
+
composite_primary_keys (~> 5.0.0)
|
42
|
+
json (~> 1.4)
|
43
|
+
minitest (~> 5.2.3)
|
44
|
+
pry
|
45
|
+
sqlite3 (~> 1.3.8)
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "datamapper-shim"
|
7
|
+
spec.version = "0.0.1"
|
8
|
+
spec.authors = ["ecin"]
|
9
|
+
spec.email = ["ecin@copypastel.com"]
|
10
|
+
spec.description = "Dress ActiveRecord models in DataMapper robes."
|
11
|
+
spec.summary = "Migrate DataMapper models to ActiveRecord."
|
12
|
+
spec.homepage = "https://github.com/typekit/datamapper-shim"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split($/)
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_dependency "json", "~> 1.4"
|
21
|
+
spec.add_dependency "activerecord", "~> 3.2.0"
|
22
|
+
spec.add_dependency "attribute-defaults", "~> 0.6.0"
|
23
|
+
spec.add_dependency "composite_primary_keys", "~> 5.0.0"
|
24
|
+
spec.add_dependency "json", "~> 1.4"
|
25
|
+
spec.add_dependency "sqlite3", "~> 1.3.8"
|
26
|
+
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
28
|
+
spec.add_development_dependency "minitest", "~> 5.2.3"
|
29
|
+
spec.add_development_dependency "pry", "~> 0.9.12.4"
|
30
|
+
end
|
@@ -0,0 +1,724 @@
|
|
1
|
+
require "active_record"
|
2
|
+
require "active_support/concern"
|
3
|
+
require "attribute_defaults"
|
4
|
+
require "composite_primary_keys"
|
5
|
+
require "json"
|
6
|
+
|
7
|
+
module DataMapper
|
8
|
+
module Shim
|
9
|
+
RAISE_ON_UNHANDLED_OPTIONS = true
|
10
|
+
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
# Placeholders for DataMapper types
|
14
|
+
BigInt = Class.new
|
15
|
+
Boolean = Class.new
|
16
|
+
Discriminator = Class.new
|
17
|
+
ParanoidDateTime = Class.new
|
18
|
+
Resource = Class.new
|
19
|
+
Serial = Class.new
|
20
|
+
Text = Class.new
|
21
|
+
|
22
|
+
# Serializable classes
|
23
|
+
Yaml = Class.new
|
24
|
+
Json = Class.new
|
25
|
+
|
26
|
+
class Enum
|
27
|
+
attr_accessor :values
|
28
|
+
|
29
|
+
def self.[](*values)
|
30
|
+
self.new(values)
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(values)
|
34
|
+
# [:active, :inactive] => { :active => 1, :inactive => 2, 1 => :active, 2 => :inactive }
|
35
|
+
self.values = values.each_with_index.inject({}) do |hash, (val, i)|
|
36
|
+
hash[val] = i + 1
|
37
|
+
hash[i + 1] = val
|
38
|
+
hash
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def [](index)
|
43
|
+
self.values[index]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class List
|
48
|
+
def load(value)
|
49
|
+
value ? value.to_s.split(",") : nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def dump(value)
|
53
|
+
value ? value.collect { |x| x.to_s.strip }.join(',') : nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Mischellaneous helpers
|
58
|
+
|
59
|
+
def attribute_get(name)
|
60
|
+
read_attribute(name)
|
61
|
+
end
|
62
|
+
|
63
|
+
def attribute_set(name, value)
|
64
|
+
write_attribute(name, value)
|
65
|
+
end
|
66
|
+
|
67
|
+
def attribute_dirty?(attribute)
|
68
|
+
changed_attributes.has_key?(attribute.to_s)
|
69
|
+
end
|
70
|
+
|
71
|
+
def dirty?
|
72
|
+
changed?
|
73
|
+
end
|
74
|
+
|
75
|
+
class OriginalValues
|
76
|
+
def initialize(model)
|
77
|
+
@model = model
|
78
|
+
end
|
79
|
+
|
80
|
+
def [](attribute)
|
81
|
+
@model.__send__ :"#{attribute}_was"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class ActiveRecord::Relation
|
86
|
+
def destroy!
|
87
|
+
destroy_all
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def original_values
|
92
|
+
OriginalValues.new(self)
|
93
|
+
end
|
94
|
+
|
95
|
+
module ClassMethods
|
96
|
+
|
97
|
+
# RFC2822 (No attribution reference available)
|
98
|
+
EmailAddress = begin
|
99
|
+
alpha = "a-zA-Z"
|
100
|
+
digit = "0-9"
|
101
|
+
atext = "[#{alpha}#{digit}\!\#\$\%\&\'\*+\/\=\?\^\_\`\{\|\}\~\-]"
|
102
|
+
dot_atom_text = "#{atext}+([.]#{atext}*)*"
|
103
|
+
dot_atom = "#{dot_atom_text}"
|
104
|
+
qtext = '[^\\x0d\\x22\\x5c\\x8 0-\\xff]'
|
105
|
+
text = "[\\x01-\\x09\\x11\\x12\\x14-\\x7f]"
|
106
|
+
quoted_pair = "(\\x5c#{text})"
|
107
|
+
qcontent = "(?:#{qtext}|#{quoted_pair})"
|
108
|
+
quoted_string = "[\"]#{qcontent}+[\"]"
|
109
|
+
atom = "#{atext}+"
|
110
|
+
word = "(?:#{atom}|#{quoted_string})"
|
111
|
+
obs_local_part = "#{word}([.]#{word})*"
|
112
|
+
local_part = "(?:#{dot_atom}|#{quoted_string}|#{obs_local_part})"
|
113
|
+
no_ws_ctl = "\\x01-\\x08\\x11\\x12\\x14-\\x1f\\x7f"
|
114
|
+
dtext = "[#{no_ws_ctl}\\x21-\\x5a\\x5e-\\x7e]"
|
115
|
+
dcontent = "(?:#{dtext}|#{quoted_pair})"
|
116
|
+
domain_literal = "\\[#{dcontent}+\\]"
|
117
|
+
obs_domain = "#{atom}([.]#{atom})*"
|
118
|
+
domain = "(?:#{dot_atom}|#{domain_literal}|#{obs_domain})"
|
119
|
+
addr_spec = "#{local_part}\@#{domain}"
|
120
|
+
pattern = /^#{addr_spec}$/
|
121
|
+
end
|
122
|
+
|
123
|
+
# Helpers
|
124
|
+
|
125
|
+
# As in 'has n, :messages'
|
126
|
+
def n
|
127
|
+
1/0.0
|
128
|
+
end
|
129
|
+
|
130
|
+
class Property
|
131
|
+
attr_reader :name, :type
|
132
|
+
|
133
|
+
def initialize(name, type)
|
134
|
+
@name = name
|
135
|
+
@type = type
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def properties
|
140
|
+
@__datamapper_shim_properties ||= Set.new
|
141
|
+
end
|
142
|
+
|
143
|
+
def storage_names
|
144
|
+
storage = Object.new
|
145
|
+
storage.instance_variable_set :@model, self
|
146
|
+
|
147
|
+
def storage.[]=(repo, name)
|
148
|
+
@model.table_name = name
|
149
|
+
end
|
150
|
+
storage
|
151
|
+
end
|
152
|
+
|
153
|
+
def timestamps(suffix)
|
154
|
+
# ActiveRecord handles timestamps (created_at, updated_at)
|
155
|
+
# automatically if the columns exist.
|
156
|
+
end
|
157
|
+
|
158
|
+
# Finders
|
159
|
+
|
160
|
+
def all(*args)
|
161
|
+
if args.empty?
|
162
|
+
scoped
|
163
|
+
else
|
164
|
+
where(*args)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def first(*args)
|
169
|
+
if args.empty?
|
170
|
+
scoped.first
|
171
|
+
else
|
172
|
+
where(*args).first
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def get(*args)
|
177
|
+
find_by_id(*args)
|
178
|
+
end
|
179
|
+
# Model definition
|
180
|
+
|
181
|
+
def property(name, type, options = {})
|
182
|
+
properties << Property.new(name, type)
|
183
|
+
|
184
|
+
case
|
185
|
+
when type == Boolean
|
186
|
+
# property :trial, Boolean
|
187
|
+
#
|
188
|
+
# If the :trial column in the DB is created with TINYINT(4),
|
189
|
+
# Rails doesn't map it to a boolean type,
|
190
|
+
# so we need to coerce it here if necessary.
|
191
|
+
define_method name do
|
192
|
+
value = read_attribute(name)
|
193
|
+
if value.is_a? Integer
|
194
|
+
value.zero? ? false : true
|
195
|
+
else
|
196
|
+
value
|
197
|
+
end
|
198
|
+
end
|
199
|
+
when type.is_a?(Enum)
|
200
|
+
warn "Rails 4 handles enums out of the box."
|
201
|
+
|
202
|
+
# Attribute accessors for the property
|
203
|
+
self.instance_eval do
|
204
|
+
define_method(name) do
|
205
|
+
type[read_attribute(name)]
|
206
|
+
end
|
207
|
+
|
208
|
+
define_method("#{name}=") do |value|
|
209
|
+
value = value.is_a?(Integer) ? value : type[value]
|
210
|
+
write_attribute(name, value)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
@__datamapper_shim_enums ||= {}
|
215
|
+
@__datamapper_shim_enums[name] = type
|
216
|
+
|
217
|
+
# Substitute enum values in where clauses
|
218
|
+
# Note, we don't define this on ActiveRecord::Relation
|
219
|
+
def self.where(opts, *rest)
|
220
|
+
@__datamapper_shim_enums.each do |name, enum|
|
221
|
+
if opts.has_key?(name) && !opts[name].is_a?(Integer)
|
222
|
+
opts[name] = enum[opts[name]]
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
super
|
227
|
+
end
|
228
|
+
when type == Json
|
229
|
+
serialize name, JSON
|
230
|
+
when type == List
|
231
|
+
serialize name, List.new
|
232
|
+
when type == ParanoidDateTime
|
233
|
+
self.instance_eval do
|
234
|
+
define_method :destroy do
|
235
|
+
update_attribute(name, Time.zone.try(:now) || Time.now)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
default_scope where(:deleted_at => nil)
|
240
|
+
|
241
|
+
def self.with_deleted
|
242
|
+
self.unscoped.where("`#{self.table_name}`.deleted_at IS NOT NULL").scoping do
|
243
|
+
yield if block_given?
|
244
|
+
end
|
245
|
+
end
|
246
|
+
when type == Yaml
|
247
|
+
serialize name
|
248
|
+
end
|
249
|
+
|
250
|
+
if type == DateTime || type == ParanoidDateTime
|
251
|
+
# ActiveRecord maps datetime columns to TimeWithZone,
|
252
|
+
# which can have some slight inconsistencies with DateTime
|
253
|
+
self.instance_eval do
|
254
|
+
define_method "#{name}" do
|
255
|
+
read_attribute(name).try(:to_datetime)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
if options.has_key?(:default)
|
261
|
+
default = options.delete(:default)
|
262
|
+
# :if => :class guarantees that the default value is always returned
|
263
|
+
attr_default(name, :if => :class, :persisted => false) do |record|
|
264
|
+
if record.new_record? && !record.__send__(:"#{name}_changed?")
|
265
|
+
default
|
266
|
+
else
|
267
|
+
record.instance_eval do
|
268
|
+
read_attribute(name)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
if reader = options.delete(:reader)
|
275
|
+
self.instance_eval do
|
276
|
+
define_method name do
|
277
|
+
read_attribute(name)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
case reader
|
282
|
+
when :private
|
283
|
+
private name
|
284
|
+
when :protected
|
285
|
+
protected name
|
286
|
+
else
|
287
|
+
raise "option :#{reader} for :reader not handled in shim"
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
if writer = options.delete(:writer)
|
292
|
+
self.instance_eval do
|
293
|
+
define_method(:"#{name}=") do |value|
|
294
|
+
write_attribute(name, value)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
case writer
|
299
|
+
when :protected
|
300
|
+
protected :"#{name}="
|
301
|
+
attr_protected name
|
302
|
+
when :private
|
303
|
+
private :"#{name}="
|
304
|
+
attr_protected name
|
305
|
+
else
|
306
|
+
raise "option :#{writer} for :writer not handled in shim"
|
307
|
+
end
|
308
|
+
else
|
309
|
+
attr_accessible name
|
310
|
+
end
|
311
|
+
|
312
|
+
# http://rubydoc.info/github/datamapper/dm-core/master/DataMapper/Property
|
313
|
+
# No validation necessary here, as these just create a database index.
|
314
|
+
options.delete(:index)
|
315
|
+
options.delete(:unique_index)
|
316
|
+
|
317
|
+
# DataMapper does some default validation on certain datatypes, e.g.
|
318
|
+
# Strings can't have a length longer than 50 chars. ActiveRecord doesn't
|
319
|
+
# do any such validations, so we can ignore this option.
|
320
|
+
options.delete(:auto_validation)
|
321
|
+
|
322
|
+
# http://stackoverflow.com/questions/95061/stop-activerecord-from-loading-blob-column
|
323
|
+
# No obvious way to lazily load an attribute
|
324
|
+
options.delete(:lazy)
|
325
|
+
|
326
|
+
# Primary keys
|
327
|
+
|
328
|
+
if options.delete(:key)
|
329
|
+
@__datamapper_shim_primary_keys ||= []
|
330
|
+
@__datamapper_shim_primary_keys << name
|
331
|
+
|
332
|
+
self.primary_keys = *@__datamapper_shim_primary_keys
|
333
|
+
validates_presence_of name
|
334
|
+
end
|
335
|
+
|
336
|
+
# Validations
|
337
|
+
|
338
|
+
validation_options = {}
|
339
|
+
|
340
|
+
if length = options.delete(:length) || options.delete(:size)
|
341
|
+
validation_options.merge! :length => { :maximum => length }
|
342
|
+
end
|
343
|
+
|
344
|
+
if unique = options.delete(:unique)
|
345
|
+
validation_options.merge! :uniqueness => true
|
346
|
+
end
|
347
|
+
|
348
|
+
if format = options.delete(:format)
|
349
|
+
if format == :email_address
|
350
|
+
validation_options.merge! :format => { :with => EmailAddress }
|
351
|
+
else
|
352
|
+
validation_options.merge! :format => { :with => format }
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
# This needs to be checked last due to how
|
357
|
+
# :allow_nil works.
|
358
|
+
if options.has_key?(:nullable)
|
359
|
+
if options.delete(:nullable) == false
|
360
|
+
if type == Boolean
|
361
|
+
# http://stackoverflow.com/questions/7781174/rspec-validation-failed-attribute-cant-be-blank-but-it-isnt-blank
|
362
|
+
validation_options.merge! :inclusion => { :in => [true, false, 0, 1] }
|
363
|
+
else
|
364
|
+
validation_options.merge! :presence => true
|
365
|
+
end
|
366
|
+
else
|
367
|
+
# Can't add an :allow_nil option unless there's an actual
|
368
|
+
# validation happening.
|
369
|
+
unless validation_options.empty?
|
370
|
+
validation_options.merge! :allow_nil => true
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
validates(name, validation_options) unless validation_options.empty?
|
376
|
+
handle_leftover_options(options)
|
377
|
+
end
|
378
|
+
|
379
|
+
# Relationships
|
380
|
+
|
381
|
+
def has(cardinality, name, options = {})
|
382
|
+
relationship_options = {}
|
383
|
+
|
384
|
+
if class_name = options.delete(:class_name)
|
385
|
+
relationship_options.merge! :class_name => class_name
|
386
|
+
end
|
387
|
+
|
388
|
+
if foreign_key = options.delete(:child_key)
|
389
|
+
relationship_options.merge! :foreign_key => foreign_key
|
390
|
+
end
|
391
|
+
|
392
|
+
if key = options.delete(:association_foreign_key)
|
393
|
+
relationship_options.merge! :association_foreign_key => key
|
394
|
+
end
|
395
|
+
|
396
|
+
if order = options.delete(:order)
|
397
|
+
relationship_options.merge! :order => order
|
398
|
+
end
|
399
|
+
|
400
|
+
if inverse_of = options.delete(:inverse_of)
|
401
|
+
relationship_options.merge! :inverse_of => inverse_of
|
402
|
+
end
|
403
|
+
|
404
|
+
if mutable = options.delete(:mutable)
|
405
|
+
# no-op, ActiveRecord relationships are mutable by default
|
406
|
+
end
|
407
|
+
|
408
|
+
if constraint = options.delete(:constraint)
|
409
|
+
# https://github.com/datamapper/dm-constraints for more information on
|
410
|
+
# all possible values for :constraint
|
411
|
+
case constraint
|
412
|
+
when :protect
|
413
|
+
relationship_options[:dependent] = :restrict
|
414
|
+
|
415
|
+
# DataMapper returns false if a model can't be destroyed.
|
416
|
+
# Here, we capture the exception AR raises if a model
|
417
|
+
# can't be destroyed due to an association.
|
418
|
+
self.instance_eval do
|
419
|
+
define_method(:destroy) do
|
420
|
+
begin
|
421
|
+
super
|
422
|
+
rescue ActiveRecord::DeleteRestrictionError => e
|
423
|
+
false
|
424
|
+
end
|
425
|
+
end
|
426
|
+
end
|
427
|
+
when :destroy
|
428
|
+
relationship_options[:dependent] = :destroy
|
429
|
+
when :destroy!
|
430
|
+
relationship_options[:dependent] = :delete_all
|
431
|
+
when :set_nil
|
432
|
+
relationship_options[:dependent] = :nullify
|
433
|
+
when :skip
|
434
|
+
# no-op
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
if through = options.delete(:through)
|
439
|
+
if through == Resource
|
440
|
+
cardinality = -1
|
441
|
+
else
|
442
|
+
relationship_options.merge! :through => through
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
case cardinality
|
447
|
+
when -1
|
448
|
+
if relationship_options.key?(:dependent)
|
449
|
+
case dependent = relationship_options.delete(:dependent)
|
450
|
+
when :destroy
|
451
|
+
before_destroy do
|
452
|
+
self.__send__(name).each do |record|
|
453
|
+
record.destroy
|
454
|
+
end
|
455
|
+
end
|
456
|
+
when :restrict
|
457
|
+
warn "ActiveRecord's HABTM doesn't support :dependent => :restrict; implement relationship as a has_many :through instead"
|
458
|
+
else
|
459
|
+
raise "Missing implementation for :#{dependent} constraint on #{name} for #{self}"
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
has_and_belongs_to_many name, relationship_options
|
464
|
+
when 1
|
465
|
+
has_one name, relationship_options
|
466
|
+
when n
|
467
|
+
has_many name, relationship_options
|
468
|
+
end
|
469
|
+
|
470
|
+
attr_accessible name
|
471
|
+
handle_leftover_options(options)
|
472
|
+
end
|
473
|
+
|
474
|
+
def belongs_to(name, options = {})
|
475
|
+
relationship_options = {}
|
476
|
+
|
477
|
+
if options.has_key?(:nullable) && options.delete(:nullable) == false
|
478
|
+
validates_presence_of name
|
479
|
+
end
|
480
|
+
|
481
|
+
if foreign_key = options.delete(:child_key)
|
482
|
+
if foreign_key.is_a?(Enumerable) && foreign_key.length == 1
|
483
|
+
foreign_key = foreign_key.first
|
484
|
+
end
|
485
|
+
|
486
|
+
relationship_options.merge! :foreign_key => foreign_key
|
487
|
+
end
|
488
|
+
|
489
|
+
if class_name = options.delete(:class_name)
|
490
|
+
relationship_options.merge! :class_name => class_name
|
491
|
+
end
|
492
|
+
|
493
|
+
if foreign_key = options.delete(:parent_key)
|
494
|
+
warn "Not handling parent_key options in belongs_to"
|
495
|
+
end
|
496
|
+
|
497
|
+
if inverse_of = options.delete(:inverse_of)
|
498
|
+
relationship_options.merge! :inverse_of => inverse_of
|
499
|
+
end
|
500
|
+
|
501
|
+
if autosave = options.delete(:autosave)
|
502
|
+
relationship_options.merge! :autosave => autosave
|
503
|
+
end
|
504
|
+
|
505
|
+
handle_leftover_options(options)
|
506
|
+
attr_accessible name
|
507
|
+
super name, relationship_options
|
508
|
+
end
|
509
|
+
|
510
|
+
# Callbacks
|
511
|
+
|
512
|
+
def after(event, callback = nil, &block)
|
513
|
+
case event
|
514
|
+
when :create
|
515
|
+
callback ? after_create(callback, &block) : after_create(&block)
|
516
|
+
when :save
|
517
|
+
callback ? after_save(callback, &block) : after_save(&block)
|
518
|
+
when :destroy
|
519
|
+
callback ? after_destroy(callback, &block) : after_destroy(&block)
|
520
|
+
else
|
521
|
+
raise "Implement `after #{event.inspect}` in DataMapper::Shim"
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
def before(event, callback = nil, &block)
|
526
|
+
case event
|
527
|
+
when :create
|
528
|
+
callback ? before_create(callback, &block) : before_create(&block)
|
529
|
+
when :save
|
530
|
+
callback ? before_save(callback, &block) : before_save(&block)
|
531
|
+
when :destroy
|
532
|
+
callback ? before_destroy(callback, &block) : before_destroy(&block)
|
533
|
+
when :valid?
|
534
|
+
# Per http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html,
|
535
|
+
# before_validation callbacks in AR cause a #save call to fail if they
|
536
|
+
# return false. Since this wasn't the case in DM, we need to wrap
|
537
|
+
# before_validation callbacks in a proc that always returns true.
|
538
|
+
if callback
|
539
|
+
raise "Need to handle before_validation with callback AND block" unless block.nil?
|
540
|
+
callback_with_true_return = proc do |*args|
|
541
|
+
self.__send__ callback
|
542
|
+
true
|
543
|
+
end
|
544
|
+
before_validation &callback_with_true_return
|
545
|
+
else
|
546
|
+
block_with_true_return = block && proc { |*args| self.instance_eval █ true }
|
547
|
+
before_validation &block_with_true_return
|
548
|
+
end
|
549
|
+
when :update
|
550
|
+
callback ? before_update(callback, &block) : before_update(&block)
|
551
|
+
else
|
552
|
+
raise "Add before case statement for #{event} event"
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
# Validations
|
557
|
+
|
558
|
+
def validates_present(name, options = {})
|
559
|
+
validation_options = {}
|
560
|
+
|
561
|
+
if message = options.delete(:message)
|
562
|
+
validation_options.merge! :message => message
|
563
|
+
end
|
564
|
+
|
565
|
+
if condition = options.delete(:if)
|
566
|
+
validation_options.merge! :if => condition
|
567
|
+
end
|
568
|
+
|
569
|
+
if on = options.delete(:when)
|
570
|
+
if on.length > 1
|
571
|
+
raise "Can't handle :when option with more than one value (had #{on})"
|
572
|
+
else
|
573
|
+
validation_options.merge! :on => on.first
|
574
|
+
end
|
575
|
+
end
|
576
|
+
|
577
|
+
validates name, :presence => validation_options
|
578
|
+
handle_leftover_options(options)
|
579
|
+
end
|
580
|
+
|
581
|
+
def validates_is_unique(name, options = {})
|
582
|
+
validation_options = { :uniqueness => true }
|
583
|
+
|
584
|
+
if options.delete(:allow_nil)
|
585
|
+
validation_options.merge! :allow_nil => true
|
586
|
+
end
|
587
|
+
|
588
|
+
if scope = options.delete(:scope)
|
589
|
+
validation_options.merge! :uniqueness => { :scope => scope }
|
590
|
+
end
|
591
|
+
|
592
|
+
if condition = options.delete(:if)
|
593
|
+
validation_options.merge! :if => condition
|
594
|
+
end
|
595
|
+
|
596
|
+
validates name, validation_options
|
597
|
+
handle_leftover_options(options)
|
598
|
+
end
|
599
|
+
|
600
|
+
def validates_length(name, options)
|
601
|
+
validation_options = {}
|
602
|
+
|
603
|
+
if max = options.delete(:max)
|
604
|
+
validation_options.merge! :maximum => max
|
605
|
+
end
|
606
|
+
|
607
|
+
if max = options.delete(:maximum)
|
608
|
+
validation_options.merge! :maximum => max
|
609
|
+
end
|
610
|
+
|
611
|
+
if min = options.delete(:min)
|
612
|
+
validation_options.merge! :minimum => min
|
613
|
+
end
|
614
|
+
|
615
|
+
if condition = options.delete(:if)
|
616
|
+
validation_options.merge! :if => condition
|
617
|
+
end
|
618
|
+
|
619
|
+
if message = options.delete(:message)
|
620
|
+
validation_options.merge! :message => message
|
621
|
+
end
|
622
|
+
|
623
|
+
validates name, :length => validation_options
|
624
|
+
handle_leftover_options(options)
|
625
|
+
end
|
626
|
+
|
627
|
+
def validates_is_confirmed(name, options = {})
|
628
|
+
confirmation_options = { }
|
629
|
+
|
630
|
+
if message = options.delete(:message)
|
631
|
+
confirmation_options.merge! :message => message
|
632
|
+
end
|
633
|
+
|
634
|
+
if condition = options.delete(:if)
|
635
|
+
confirmation_options.merge! :if => condition
|
636
|
+
end
|
637
|
+
|
638
|
+
validates name, :confirmation => confirmation_options
|
639
|
+
validates :"#{name}_confirmation", :presence => true, :if => confirmation_options[:if]
|
640
|
+
|
641
|
+
handle_leftover_options(options)
|
642
|
+
end
|
643
|
+
|
644
|
+
def validates_with_block(name, &block)
|
645
|
+
validates_each name do |record, property_name|
|
646
|
+
valid, error_message = record.instance_exec(record.__send__(property_name), &block)
|
647
|
+
unless valid
|
648
|
+
record.errors.add(name, error_message)
|
649
|
+
end
|
650
|
+
end
|
651
|
+
end
|
652
|
+
|
653
|
+
def validates_format(name, options = {})
|
654
|
+
validation_options = {}
|
655
|
+
|
656
|
+
if with = options.delete(:with)
|
657
|
+
validation_options.merge! :format => { :with => with }
|
658
|
+
end
|
659
|
+
|
660
|
+
validates name, validation_options
|
661
|
+
handle_leftover_options(options)
|
662
|
+
end
|
663
|
+
|
664
|
+
def validates_within(name, options = {})
|
665
|
+
validation_options = {}
|
666
|
+
|
667
|
+
if set = options.delete(:set)
|
668
|
+
validation_options.merge! :inclusion => { :in => set }
|
669
|
+
end
|
670
|
+
|
671
|
+
validates name, validation_options
|
672
|
+
handle_leftover_options(options)
|
673
|
+
end
|
674
|
+
|
675
|
+
def validates_with_method(name, options)
|
676
|
+
validation_options = {}
|
677
|
+
|
678
|
+
if on = options.delete(:when)
|
679
|
+
case on
|
680
|
+
when Array
|
681
|
+
if on.length > 1
|
682
|
+
raise "Can't handle :when option with more than one value (had #{on})"
|
683
|
+
else
|
684
|
+
validation_options.merge! :on => on.first
|
685
|
+
end
|
686
|
+
else
|
687
|
+
validation_options.merge! :on => on
|
688
|
+
end
|
689
|
+
end
|
690
|
+
|
691
|
+
method = options.delete(:method)
|
692
|
+
|
693
|
+
# Validation methods made for DM return either true if the model validation passed,
|
694
|
+
# or [false, "error message"] if validation failed. Here we add the error message to
|
695
|
+
# the model's ActiveModel::Errors object.
|
696
|
+
block = proc do
|
697
|
+
valid, error_message = self.__send__ method
|
698
|
+
unless valid
|
699
|
+
self.errors.add(name, error_message)
|
700
|
+
end
|
701
|
+
end
|
702
|
+
|
703
|
+
validate validation_options, &block
|
704
|
+
handle_leftover_options(options)
|
705
|
+
end
|
706
|
+
|
707
|
+
protected
|
708
|
+
|
709
|
+
def handle_leftover_options(options)
|
710
|
+
return if options.empty?
|
711
|
+
|
712
|
+
method = caller[0][/`.*'/][1..-2]
|
713
|
+
message = "DataMapper::Shim##{method} doesn't yet handle #{options.inspect} (#{name})"
|
714
|
+
|
715
|
+
if RAISE_ON_UNHANDLED_OPTIONS
|
716
|
+
raise NotImplementedError, message
|
717
|
+
else
|
718
|
+
warn message
|
719
|
+
end
|
720
|
+
end
|
721
|
+
end
|
722
|
+
end
|
723
|
+
end
|
724
|
+
|