How can I dynamically add custom validators in rails? - ruby-on-rails

I have a case where I need to add and remove custom validators for different instances of the same object.
For example...
class MyCustomValidator < ActiveModel::Validator
...
end
class Foo
include ActiveModel::Validations
validates_with MyCustomValidator
end
Context A:
Foo.new.valid? #=> This should use the custom validator
Context B:
f = Foo.new
f.class.clear_validators!
f.valid? #=> This should no longer call the custom validator
Context C:
f = Foo.new
f.class.clear_validators!
f.valid? #=> This should no longer call the custom validator
# This is where I need to do something to bring the validator back so I can run
f.valid? #=> This should use the custom validator
Is there a way to accomplish this?

You could do something like:
class Foo
include ActiveModel::Validations
validates_with MyCustomValidator, if: :use_custom_validators?
def use_custom_validators?
!#custom_validator_disabled
end
def without_custom_validators(&block)
prev_disabled, #custom_validator_disabled = #custom_validator_disabled, true
#custom_validator_disabled = true
instance_eval(&block)
#custom_validator_disabled = prev_disabled
end
end
f = Foo.new
f.valid? # Custom validator used
f.without_custom_validators do
valid? # Custom validator not used
end
f.valid? # Custom validator used

If the necessity to use a validator is defined by certain state of the object, you can use a state_machine gem and explicitly define a state flag. After that, you can define state-specific validations.
For that to work, you need to define two states with an extra validation only defined on one of them. Then you need to make an event that transitions from the "clean" to "validated" state (or whatever you call them).
Example:
class LockableSwitch < ActiveRecord::Base
state_machine initial: :off
state :off # It's probably redundant. I dunno.
# if you're not storing state as strings, you'd specify
# state :off, value: 0 # if `state` column was an integer
state :on do
# Can only be switched on if a key is inserted
validates :key, presence: true
end
event :switch do # An event that causes the apple to become ripe
transition :off => :on # `=>` justifies the use of that old syntax
transition :on => :off # The first matched transition is used
end
end
end
#l = LockableSwitch.new # :off by default
#l.switch! # fails, bang-method throws an error on failed transition
#l.key = "something"
#l.switch! # works
It's yet another library and extra memory/complexity (even though not too much), but it makes your code much clearer in "what it does".

Related

How to create a Rails 4 Concern that takes an argument

I have an ActiveRecord class called User. I'm trying to create a concern called Restrictable which takes in some arguments like this:
class User < ActiveRecord::Base
include Restrictable # Would be nice to not need this line
restrictable except: [:id, :name, :email]
end
I want to then provide an instance method called restricted_data which can perform some operation on those arguments and return some data. Example:
user = User.find(1)
user.restricted_data # Returns all columns except :id, :name, :email
How would I go about doing that?
If I understand your question correctly this is about how to write such a concern, and not about the actual return value of restricted_data. I would implement the concern skeleton as such:
require "active_support/concern"
module Restrictable
extend ActiveSupport::Concern
module ClassMethods
attr_reader :restricted
private
def restrictable(except: []) # Alternatively `options = {}`
#restricted = except # Alternatively `options[:except] || []`
end
end
def restricted_data
"This is forbidden: #{self.class.restricted}"
end
end
Then you can:
class C
include Restrictable
restrictable except: [:this, :that, :the_other]
end
c = C.new
c.restricted_data #=> "This is forbidden: [:this, :that, :the_other]"
That would comply with the interface you designed, but the except key is a bit strange because it's actually restricting those values instead of allowing them.
I'd suggest starting with this blog post: https://signalvnoise.com/posts/3372-put-chubby-models-on-a-diet-with-concerns Checkout the second example.
Think of concerns as a module you are mixing in. Not too complicated.
module Restrictable
extend ActiveSupport::Concern
module ClassMethods
def restricted_data(user)
# Do your stuff
end
end
end

Method variables in rails models

When I write ActiveRecord models in rails I want to make sure that local variables I use in my method will remain local to the scope of the method I write, and I will not be accidentally overwriting model property in future.
For example:
class User < ActiveRecord::Base
after_save :do_something
def do_something
# ... some logic
group = "some value"
# ... more logic
end
end
How do I make sure that if I happen to add group property to my model in future - my method will not accidentally overwrite it?
I thought about class variables, but it's not what they are for, and I think using them will be a bad practice.
In ruby you can define local variables and instance variables with the same name.
class Example
#instance = 4
def example_method
#instance += 1
end
def example_2
instance = 0
10.times { instance += 1 }
end
end
e = Example.new
e.example_2 # => 10
e.instance # => 4
e.example_method # => 5
e.instance # => 5
these will not conflict
example_2
is returning the local variable which is then destroyed if not used
example_method
is setting the instance variable which will save per instance of the class or in this case for active record will save to database.
Also note you can be more explicit when referring to instance variables and say
self.instance
EDIT
your group variable inside your do_something is a local variable and will not cause conflict with #group which is a instance variable of a higher scope
EDIT2
I have a projects model in my application
class Project < ActiveRecord::Base
VALID_NAME_REGEX = /(\A[\w ]+)/
VALID_URL_REGEX = URI::regexp(["http", "https"])
validates :name, :presence => true,
:uniqueness => true,
:length => {
:maximum => 24
},
:format => {
:with => VALID_NAME_REGEX,
:message => "Only allow numbers letters hyphens and underscores"
}
after_save :test
def test
name = 'bob'
end
end
after setting my model to this and creating a new instance of it on my website I can confirm this will not set my projects name to bob
Using a local variable, that is a variable not prefixed by # or ## will not cause a namespace collision with future model attributes. Those attributes would have to be referenced in such a way similar to self.group. You could run into trouble if your code in your model tweaks the class by adding fields or members to the object at runtime, or if you mess with the class definition, but just declaring a local variable in a method will not collide with an instance variable that corresponds to an ORM field.
Additionally, if you've got methods to mutate fields in your model and you try to access them by saying
def getgroup
group
end
this won't work, either. group in this context is undefined, because it doesn't refer to anything else in the local scope

How to initialize 'attr_accessor' attribute values? [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
attr_accessor default values
I am using Ruby on Rails 3.0.9 and I would like to initialize some attr_accessor attribute values in my class\model that inherits from ActiveRecord::Base. That is,
... in my module I have:
class User < ActiveRecord::Base
attr_accessor :attribute_name1,
:attribute_name2,
:attribute_name3,
...
end
and I would like to set to true all attr_accessor attribute values. How can I do that?
P.S.: Of course I would like to solve the above issue approaching "à la Ruby on Rails Way". I know about the after_initialize callback but by using that method I should repeat each attribute_name<N> statement for which I would like to set the value to true inside that after_initialize statement (... and this is not DRY - Don't Repeat Yourself). Maybe there is a better way to achieve this. Is there a way to set attr_accessor attribute values "on the fly" when you state those attributes? That is, I expect to declare and set attr_accessor attributes at once!
Did you try:
class User < ActiveRecord::Base
attr_accessor :attribute_name1,
:attribute_name2,
:attribute_name3,
...
after_initialize :set_attr
def set_attr
#attribute_name1 = true
...
end
end
For Rails 3.2 or earlier, you could use attr_accessor_with_default:
class User < ActiveRecord::Base
attr_accessor_with_default :attribute_name1, true
attr_accessor_with_default :attribute_name2, true
attr_accessor_with_default :attribute_name3, true
...
end
Since your default value is an immutable type (boolean), this form of the method is safe to use here. But don't use it if the default value is a mutable object, including an array or string, because all of your new model objects will share the exact same instance, which is probably not what you want.
Instead, attr_accessor_with_default will accept a block, where you can return a new instance each time:
attr_accessor_with_default(:attribute_name) { FizzBuzz.new }
I would just define a getter that lazily loads the value you are interested in, and use attr_writer to define the setter. For instance,
class Cat
attr_writer :amount_of_feet
def amount_of_feet; #amount_of_feet ||= 4; end # usually true
end
Which, if you really mean it, can be rewritten with some meta-programming:
class Cat
# The below method could be defined in Module directly
def self.defaulted_attributes(attributes)
attributes.each do |attr, default|
attr_writer attr
define_method(attr) do
instance_variable_get("##{attr}") ||
instance_variable_set("##{attr}", default)
end
end
end
defaulted_attributes :amount_of_feet => 4
end
calin = Cat.new
print "calin had #{calin.amount_of_feet} feet... "
calin.amount_of_feet -= 1
puts "but I cut one of them, he now has #{calin.amount_of_feet}"
This works because, usually, computing the default value won't have any side effect making the order matter and it won't be needed to compute the value until you first try to access it.
(Câlin is my cat; he's doing well, and still has the four of his feet)
Brutal solution
class User < ActiveRecord::Base
##attr_accessible = [:attribute_name1, :attribute_name2, :attribute_name3]
attr_accessor *##attr_accessible
after_initialize :set_them_all
def set_them_all
##attr_accessible.each do |a|
instance_variable_set "##{a}", true
end
end
end
or little more conceptual: Ruby: attr_accessor generated methods - how to iterate them (in to_s - custom format)?

Rails callback for the equivalent of "after_new"

Right now I cant find a way to generate a callback between lines 1 and 2 here:
f = Foo.new
f.some_call
f.save!
Is there any way to simulate what would be effectively an after_new callback? Right now I'm using after_initialize but there are potential performance problems with using that since it fires for a lot of different events.
You can use the after_initialize callback
# app/models/foo.rb
class Foo < ActiveRecord::Base
after_initialize :some_call
def some_call
puts "Who you gonna call?"
end
end
# rails console
> foo = Foo.new # => "Who you gonna call?"
> foo = Foo.first # => "Who you gonna call?"
Beware after_initialize is triggered every time ActiveRecord do a Foo.new (calls like new, find, first and so on) see the Rails Guide
So you probably want something like this after_initialize :some_call, :if => :new_record?
# app/models/foo.rb
class Foo < ActiveRecord::Base
after_initialize :some_call, :if => :new_record?
def some_call
puts "Who you gonna call?"
end
end
# rails console
> foo = Foo.new # => "Who you gonna call?"
> foo = Foo.first
Define a constructor for Foo and do what you need to do there.
An alternate solution would be to look into using after_initialize but that may not do quite what you expect.
If this is a Active Record model, then after_initialize is the correct way to handle callbacks after object creation. The framework itself makes certain assumptions about the way objects will be created from the database. Performance should not be a concern unless you have some long-running task being triggered in the callback, in which case you probably need to rethink the strategy.
If not an AR model, you can create an initialize method in your object and place the code there.
There are a number of other callbacks available depending on want you want to do, including after_create (called when a new record is created).

Rails ActiveRecord: Locking down attributes when record enters a particular state

Wondering if there’s a plugin or best way of setting up an ActiveRecord class so that, for example, when a record enter the "published" state, certain attributes are frozen so that they could not be tampered with.
Editing attributes which shouldn't be edited is a validation error:
class Post < ActiveRecord::Base
validate :lock_down_attributes_when_published
private
def lock_down_attributes_when_published
return unless published?
message = "must not change when published"
errors.add(:title, message) if title_changed?
errors.add(:published_at, message) if published_at_changed?
end
end
This uses the ActiveRecord::Dirty extensions introduced in 2.2 or so.
You can freeze an entire AR::B object by setting #readonly to true (in a method), but that will lock out all attributes.
The way I would recommend is by defining attribute setter methods that check for the current state before passing to super:
class Post < ActiveRecord::Base
def author=(author)
super unless self.published?
end
def content=(content)
super unless self.published?
end
end
[EDIT] Or for a large amount of attributes:
class Post < ActiveRecord::Base
%w(author content comments others).each do |method|
class_eval <<-"end_eval", binding, __FILE__, __LINE__
def #{method}=(val)
super unless self.published?
end
end_eval
end
end
Which of course I would advocate pulling into a plugin to share with others, and add a nice DSL for accessing like: disable_attributes :author, :content, :comments, :when => :published?
You could add a custom validation to block changes to attributes if you're in a certain state. You could hard code things directly into the validation. But I prefer the slightly more robust approach using constants defining a whitelist (list of attributes that are allowed to change in a state) or a blacklist (list of attributes not allowed to change in a state).
Here's an example of both approaches. Each approach assumes there is a state method in your model that returns the current/new state as a string.
White List Approach
WhiteListStateLockMap = {
"state_1" => [
"first_attribute_allowed_to_change_in_state_1",
"second_attribute_allowed_to_change_in_state_1",
...
],
"state_2" => [
"first_attribute_allowed_to_change_in_state_2",
"second_attribute_allowed_to_change_in_state_2",
...
],
...
}
validates :state_lock
def state_lock
# ensure that all changed elements are on the white list for this state.
unless changed & WhiteListStateLockMap[state] == changed
# add an error for each changed attribute absent from the white list for this state.
(changed - WhiteListStateLockMap[state]).each do |attr|
errors.add attr, "Locked while #{state}"
end
end
end
Black List Approach
BlackListStateLockMap = {
"state_1" => [
"first_attribute_not_allowed_to_change_in_state_1,
"second_attribute_not_allowed_to_change_in_state_1,
...
],
"state_2" => [
"first_attribute_not_allowed_to_change_in_state_2",
"second_attribute_not_allowed_to_change_in_state_2",
...
],
...
}
validates :state_lock
def state_lock
# ensure that no changed attributes are on the black list for this state.
unless (changed & BlackListStateLockMap[state]).empty?
# add an error for all changed attributes on the black list for this state.
(BlackListStateLockMap[state] & changed).each do |attr|
errors.add attr, "Locked while #{state}"
end
end
end
If the particular state is merely persisted?, then attr_readonly is the best option.
attr_readonly(*attributes) public
Attributes listed as readonly will be used to create a new record but update operations will ignore these fields.
To test (courtesy of THAiSi):
class MyModel < ActiveRecord::Base
attr_readonly :important_type_thingie
end
#RSpec
describe MyModel do
its('class.readonly_attributes') { should include "important_type_thingie" }
it "should not update the thingie" do
m = create :my_model, :important_type_thingie => 'foo'
m.update_attributes :important_type_thingie => 'bar'
m.reload.important_type_thingie.should eql 'foo'
end
end

Resources