Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Inject Validations #6014

Closed
wants to merge 1 commit into from

4 participants

@jenshz

A proposal to dynamically add context specific (as in DCI context) validations to individual ActiveModel instances.

@jenshz jenshz Inject Validations
A proposal to dynamically add context specific (as in DCI context) validations to individual ActiveModel instances.
b25170e
@jenshz

This is our proposal for context-specific validations

We have specced it, but no unit tests have been written.

@josevalim
Owner

I don't like this approach, injecting the validations seems very wrong to me. What I would like to see instead is the ability to have validations contexts. So I could create a new validation context and ask my module to use it:

class OnPurchaseContext
  include ActiveModel::Validations

  validates_presence_of :name
end

OnPurchaseContext.new.validates(@cart)
@Aupajo

I agree with @josevalim.

Injecting validations in a way that isn't necessarily transparent to someone else could be a big source of headaches. For instance, how do they know when a model has been injected?

Validation contexts could be interesting. If, on the other hand, we're not talking about contexts so much as just looking for a nice way to extract and share validations, why not use an ActiveSupport::Concern? With the example in the PR:

# Autoload model concerns
module YourApp
  class Application < Rails::Application
    config.autoload_paths += %W(#{config.root}/app/models/concerns)
  end
end

# app/models/concerns/price_concerns.rb
module PriceConcerns
  extend ActiveSupport::Concern

  included do
    validates_presence_of :price
  end
end

# app/models/offer.rb
class Offer < ActiveRecord::Base
  include PriceConcerns
end

<3 concerns.

@josevalim
Owner

@Aupajo I believe the issue here is not sharing validations across different models but a model with different validations scenarios.

In any case, I am closing this pull request since we can agree that injecting validations wouldn't be the best way to implement this. Thanks.

@josevalim josevalim closed this
@anderslemke

Thank you guys for your input. We've been discussing this a lot here on autobutler.dk.

We are exploring DCI and are trying to nail how we could keep a set of validations in the same file as the DCI context.

An approach that would work "out of the box", is to define the validations on the model itself like this:

class Offer < ActiveRecord::Base
  with_options :on => :purchase do |context|
    context.validates_presence_of :price
  end
end

and then make sure that you use the context :purchase when you save your objects. But then you would have to look two places to figure out what is going on on a purchase.

@anderslemke

@Aupajo, we've been trying out something like what you propose.
We just tried extending instances with the module, instead of including it in the class.

We thought it would be nice, if you could offer.extend(PriceConcerns), and then that specific instance of Offer would use those validations included.

This doesn't work, though, for reasons I haven't fully grasped, but @jenshz has. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Apr 27, 2012
  1. @jenshz

    Inject Validations

    jenshz authored
    A proposal to dynamically add context specific (as in DCI context) validations to individual ActiveModel instances.
This page is out of date. Refresh to see the latest.
Showing with 69 additions and 0 deletions.
  1. +69 −0 activemodel/lib/active_model/inject_validations.rb
View
69 activemodel/lib/active_model/inject_validations.rb
@@ -0,0 +1,69 @@
+module ActiveModel
+ # When you have a module
+ #
+ # module Foo
+ # extend ActiveModel::InjectValidations
+ #
+ # inject_validations do |c|
+ # c.validates_presence_of :price
+ # end
+ # end
+ #
+ # and a model
+ #
+ # class Offer < ActiveRecord::Base ; end
+ #
+ # and you extend an instance of +Offer+ like this
+ #
+ # offer = Offer.new
+ # offer.extend(Foo)
+ #
+ # The validation behaviour of +offer+, and only that instance,
+ # will be the same as if you had
+ #
+ # class Offer < ActiveRecord::base
+ # validates_presence_of :price
+ # end
+ #
+ # It assumes that all validation is done with the valid? method on the object,
+ # which is the case for the current version of ActiveModel (3.2.3).
+ module InjectValidations
+
+ def inject_validations(&block)
+ @validations = block
+ end
+
+ private
+
+ def self.extended(base)
+ base.module_eval do
+ def self.extended(real_base)
+ super(real_base)
+ context_sym = (self.name.gsub(":","").to_s + "Context").underscore.to_sym
+
+ # Do not redefine validations on base class, if already included once.
+ sym = ("@@" + context_sym.to_s).to_sym
+ unless real_base.class.class_variable_defined?(sym)
+ real_base.class.class_variable_set(sym, true)
+ vs = @validations
+ real_base.class.instance_eval do
+ with_options :on => context_sym, &vs
+ end
+ end
+
+ real_base.define_singleton_method(:valid?) do |context=nil|
+ valid = super(context_sym)
+ unless context.nil?
+ _errors = errors.to_hash
+ valid &= super(context)
+
+ # Merge errors from both contexts
+ _errors.each { |k,v| errors.set(k, (_errors[k] + v).uniq) }
+ end
+ valid
+ end
+ end
+ end
+ end
+ end
+end
Something went wrong with that request. Please try again.