datamapper-shim 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify dependencies in datamapper-shim.gemspec
4
+ gemspec
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 &block; 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
+