-1

In my Rails 4, I have a Post model I need to implement a custom validation on.

Following the recommendation in this question and in the documentation here, I have implemented the following code:

#app/validators/link_validator.rb

class LinkValidator < ActiveModel::Validator
  def validate(record)
    if record.format == "Link"
      unless facebook_copy_link(record.copy)
        record.errors[:copy] << 'Please make sure the copy of this post includes a link.'
      end
    end
  end
end

#post.rb
class Post < ActiveRecord::Base
  [...]
  include ActiveModel::Validations
  validates_with LinkValidator
  [...]
end

—————

UPDATE: The facebook_copy_link method is defined as follows:

class ApplicationController < ActionController::Base
  [...]
  def facebook_copy_link(string)
    require "uri"
    array = URI.extract(string.to_s)
    array.select { |item| item.include? ( "http" || "www") }.first
  end
  [...]
end

—————

When I run the app, I get the following error:

NameError at /posts/74/edit
uninitialized constant Post::LinkValidator
validates_with LinkValidator

Any idea what is wrong here?

—————

UPDATE 2: I had forgotten to restart the server.

Now, I get a new error:

NoMethodError at /posts/74
undefined method `facebook_copy_link' for #<LinkValidator:0x007fdbc717ba60 @options={}>
unless facebook_copy_link(record.copy)

Is there a way to include this method in the validator?

Community
  • 1
  • 1
Thibaud Clement
  • 6,607
  • 10
  • 50
  • 103

1 Answers1

1

Aside from being a Rails validator class, LinkValidator is also a Ruby class. So you can define pretty much any method on it.

The facebook_copy_link doesn't seem to be using the controller instance's state, so you could just easily move the method into the validator class:

require "uri"

class LinkValidator < ActiveModel::Validator
  def validate(record)
    if record.format == "Link"
      unless facebook_copy_link(record.copy)
        record.errors[:copy] << 'Please make sure the copy of this post includes a link.'
      end
    end
  end

  private

  def facebook_copy_link(string)
    array = URI.extract(string.to_s)
    array.select { |item| item.include? ( "http" || "www") }.first
  end
end

Note how I made the facebook_copy_link method private. This is simply good practice, as the only method being accessed by other objects is validate.

As a sidenote, it's not necessary to put include ActiveModel::Validations in an ActiveRecord subclass. Validations are already available in ActiveRecord classes.

fivedigit
  • 18,464
  • 6
  • 54
  • 58
  • Thanks for this very useful answer. Two quick questions if you don't mind: 1. the `facebook_copy_link` is actually used twice in the PostsController, which is the reason why it is currently located in the ApplicationController: given this new piece of information, would you still recommend to move it to the validator class? 2. I see that you placed the `require "uri"` code before `class LinkValidator < ActiveModel::Validator`: is there a reason for that? – Thibaud Clement Dec 18 '15 at 00:56
  • You *could* move the method into the model, but then every model requiring that validation has to define the method. Another option is to move it into a class other than the validator and the controller. As for the `require`, I prefer to have all dependencies listed at the top of a source file for clarity. :) – fivedigit Dec 18 '15 at 21:35