Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Decorating a method in Ruby

I have method that takes as a parameter an array of integer numbers, and I would like to use the decorator pattern to validate first if each of the numbers in the array is in a specified range.

I've seen decorator in ruby that extend class or include modules, but this seems a little excesive to me, is there a way to just decorate a method without relaying on a class or module ?

EDIT I have several methods that takes an array as parameter, and need to validate the range of the items. Instead of inline coding the validation in each of this method, I want a decorator for all of this methods, but I was wondering if decorator classes/modules are the only ones that exist in Ruby ?

class MyClass
 ..some code here ...
 def method(array)
   ..some code here...
 end
end
like image 612
Crixx93 Avatar asked Nov 02 '25 17:11

Crixx93


1 Answers

Here's a simple example of how to wrap methods with a validator:

class Foo
  def foo(a, b, c)
    p a, b, c
  end

  def bar(a, b)
    p a, b
  end

  validate(:foo, :bar) {|*args, &blk| args.reduce(:+) == 6 } 
end

The Module#validate method takes a list of method names, and a block with the validation logic for those methods.

foo = Foo.new

foo.foo(1, 2, 3)
# 1
# 2
# 3

foo.bar(2, 4)
# 2
# 4

foo.foo(2, 3, 4)
# [2, 3, 4] (Validator::ValidationError)

foo.bar(2, 3)
# [2, 3] (Validator::ValidationError)

As you can see, the validator rejected the argument list in the last two calls because they didn't pass the condition in the block.

This is the "magic" which makes it all happen, which isn't actually that magic. We generate a module which overrides the methods we want to validate with a version that raises an exception if the validation fails and then simply calls super. Then we prepend that module to the class/module that we are currently in, i.e. the one that the call to the validate method is in. And that's it, basically.

I chose to also be a good Ruby citizen and wrap the whole thing in a Refinement, so you need to say

using Validator

to actually activate it.

module Validator
  class ValidationError < ArgumentError; end

  refine Module do
    def validate(*methods, &validator)
      prepend(Module.new do
        methods.each do |method|
          define_method(method) do |*args, &blk|
            raise ValidationError, args.inspect unless yield *args
            super(*args, &blk)
          end
        end
      end)
    end
  end
end
like image 135
Jörg W Mittag Avatar answered Nov 04 '25 15:11

Jörg W Mittag