Tyshawn
Tyshawn

Reputation: 39

Ruby: Custom Implementation of attribute accessors with validation

I'm trying to Implementation of custom attribute accessors with validation.

Let say attr_validated. Now this attr_validated

1: Should have same setter and getter methods as with attr_accessor. ## this part is done.

2: It Should validate the given block.

attr_validated :num_legs do |v|
v <= 4
end

This question might be look like any other question but its not. While googled i got

1: Ist Part

class Class
     def attr_validated(*args)
    args.each do |arg|
      # getter
      self.class_eval("def #{arg};@#{arg};end")
      # setter
      self.class_eval("def #{arg}=(val);@#{arg}=val;end")
    end
  end
end

class Dog
  attr_validated :num_legs ## Instead of this i need to validate a block also attr_validated :num_legs do |v|
v <= 4
end

dog = Dog.new
p dog.num_legs
p dog.num_legs = 'Stack'

2: How might we can Implement second part.

Any help would be greatly appreciated !!!

Upvotes: 2

Views: 1551

Answers (2)

Paweł Dawczak
Paweł Dawczak

Reputation: 9639

How about something like this:

class Class
  def attr_validated(*args, &validator)
    args.each do |name|
      define_method("#{name}=") do |value|
        if block_given?
          raise ArgumentError, "Value '#{value}' is invalid" unless validator.call(value)
        end

        instance_variable_set("@#{name}", value)
      end

      define_method(name) do
        instance_variable_get("@#{name}")
      end
    end
  end
end

class Person
  attr_validated(:name, :occupation) { |name| name.length > 3 }
end

p1 = Person.new
p1.name = "John The Tester"
p1.occupation = "Software developer"
p "#{p1.name} - #{p1.occupation}"

p2 = Person.new
p2.name = "test"
p2.occupation = "Tester"
p "#{p2.name} - #{p2.occupation}"

Which would generate output like:

"John The Tester - Software developer"
app.rb:6:in `block (2 levels) in attr_validated': Value 'test' is invalid (ArgumentError)
        from app.rb:28:in `<main>'

Hope that helps!

Good luck!

UPDATE

You can add another method, that will apply validation for first argument like this:

class Class
  def attr_validated_first(*args, &validator)
    args.each_with_index do |name, index|
      define_method("#{name}=") do |value|
        if block_given? && index == 0
          raise ArgumentError, "Value '#{value}' is invalid" unless validator.call(value)
        end

        instance_variable_set("@#{name}", value)
      end

      define_method(name) do
        instance_variable_get("@#{name}")
      end
    end
  end
end

However I wouldn't recommend this approach, which would be confusing! if you want to register couple attributes with different validation rules... You can use attr_validated from first example multiple times, like this:

class Person
  attr_validated(:name)       { |name| name.length > 3 }
  attr_validated(:occupation) { |occupation| occupation == "Ruby Developer" }
end

Upvotes: 4

J&#246;rg W Mittag
J&#246;rg W Mittag

Reputation: 369458

That's easy. Just raise an ArgumentError in the setter if the block says the argument is invalid:

class Person
  attr_reader :name 

  def name=(name)
    raise ArgumentError, "'#{name}' is not a valid name!" if rejector.(name)
    @name = name
  end

  private

  attr_accessor :rejector

  def initialize(&rejector)
    self.rejector = rejector
  end
end

artist = Person.new do |person|
  person.length > 6
end

artist.name = 'The Artist Formerly Known As Prince'
# ArgumentError: 'The Artist Formerly Known As Prince' is not a valid name!
artist.name = 'ruby'
# => 'ruby'

Upvotes: 3

Related Questions