Brain damaged assertions chain

2009
05.21

After a while, when I am coding and have already lost any notion of time, I realize that I was debugging or changing my code all over again; repeating myself really badly for several hours.

The assertions in my code were the parts that I repeated the most and lost a lot of time fine tuning how they should behave, reports errors. And way too much time spend asserting that the assertions asserted correctly.


I though; what a waste of time it was and decided to spend an afternoon in the search from a solution that complies with this humble wish list:

  1. default raise behavior
  2. one place to fine tune the raise behavior
  3. compact one liner syntax for short assert
  4. long complicated assert should be possible
  5. use the existing methods of the objects
  6. correct code more import than wrong fast code

Example 1

In a method I had the following assertions:

	def some_method(*args)
		if args.empty?
			# do something
		elseif args.length == 1 and
			args[0] == :read or
			args[0] == :write or
			args[0] == :readwrite then

			# do something
		else
			raise "Zero or one argument expected"
		end
	end

Example 2

another example I was repeating quite often:

	def foo(*args)
		if args.empty?
			return something
		elseif args.length == 1 and arg[0].kind_of?(Numeric)
			#do something
		else
			raise "Wrong arg"
		end
	end

You can try and move stuff arround but personnaly I don’t find that the situation improves much:

	def foo(*args)
		return something if args.empty?

		if args.length == 1 and arg[0].kind_of?(Numeric)
			#do something
		else
			raise "Wrong arg"
		end
	end

Solution

The fact is that the method performs two distinct tasks. The first is to check if the passed arguments can be processed and second do the actual work. I wanted to split the two task and here is the result of the two examples above:

Example 1: with Assertion

	def some_method(*args)
		# assert we can handle *args
		assert(arg).empty?.or.length.eql?(1).and.at(0)._{
			eql?(:read).eql?(:write).eql?(:readwrite)
		}.end

		# do the work
		if arg.empty?
			#do something
		else
			#do something
		end
	end

So the syntax is assert(object_to_test).methods_chain.end the ‘end’ methods triggers the exception if the chain is not fulfilled.

Example 2: with Assertion

The second example contains less specification, as such the assertions will be shorter and readable:

	def foo(*args)
		assert(args).empty?.or { length.eql?(1).and.at(0).kind_of?(Numeric).end

		if args.empty?
			return something
		else
			#do somtehing
		end

More Usage examples

	a = [1,2,3]
	assert(a).kind_of?(Array).not.empty?.each { kind_of?(Numeric) }.end
	assert(a).kind_of?(Array).empty?.or.each {
		kind_of?(Numeric) }.last.eql?(3).end

	a =[:foo, 1, 2]
	assert(a).nil?.or {
		kind_of?(Array).and.not.empty?.and {
			at(0).kind_of(Symbol).slice(1..-1).each {
				kind_of?(Numeric)
			}
		}
	}.end

	a = [1, 1.2, 1.3, 3]
	assert(a).let(:intorfloat?) {
		kind_of?(Fixnum).or.kind_of?(Float)
		}.each { intorfloat? }.end

	def foo(x,y)
		assert(x).kind_of?(Symbol).and(y).kind_of?(Numeric).end

		#or

		assert(x).kind_of?(Fixnum).or(y).kind_of?(Fixnum).end
	end

assert

Here is the source code for the assert method and the associated class.

  class Assert
    alias __class__ class

    Object.new.methods.each { |m|
      unless m == '__id__' or m == '__send__'
         undef_method m
      end
    }

    def initialize(obj)
      @origin = obj
      @obj = @origin
      @result = true
      @op = :and
    end

    def result
      @result
    end

    def or(*obj, &block)
      @obj = obj[0] unless obj.empty?
      @op = :o r
      self._(&block) unless block.nil?
      self
    end

    def and(*obj, &block)
      @obj = obj[0] unless obj.empty?
      @op = :and
      self._(&block) unless block.nil?
      self
    end

    def not
      @op = case @op
      when :and then :not_and
      when :o r then :not_or
      when :not_and then :and
      when :not_or then :o r
      end
      self
    end

    def end(should_raise = true, &block)
      if @result == false
        if block.nil?
          raise StandardError, "Assertions failed", caller(0).last if  should_raise
        else
          block.call(self)
        end
      end
    end

    def push_result(v)
      if v.kind_of?(TrueClass) or v.kind_of?(FalseClass)
        @result = case @op
        when :and then @result and v
        when :o r then @result or v
        when :not_and then self.not; @result and !v
        when :not_or then self.not; @result or !v
        end
        @obj = @origin
      else
        @obj = v
      end
      self
    end

    def eval_block(ob, &block)
      raise "Block missing" if block.nil?
      local = Assert.new(ob)
      local.instance_eval(&block)
      push_result(local.result)
    end

    def _(&block)
      eval_block(@obj, &block) unless block.nil?
    end

    def each(&block)
      raise "Method missing each" if @obj.respond_to?(:each)

      @obj.each { |e| eval_block(e, &block) }
      self
    end

    def let(sym, &block)
      self.__class__.class_eval do
        define_method(sym) { eval_block(@obj, &block) }
      end

      self
    end      

    def method_missing(m, *args, &block)
      if @obj.respond_to?(m)
        push_result @obj.send(m, *args, &block)
      else
        raise "Method missing #{m}"
      end
      self
    end
  end

def assert(obj)
  Assert.new(obj)
end

Ruby Inox on GitHub

Although not it was not a strict requirement. This file is part of the Ruby Inox project. Inox is a work in progress and this file might not be up to date. The status and latest files are available on GitHub

Related posts:
  1. Ruby Inox Part 2: A Point class Alright, the journey begins. I received the first specification! If...
  2. Ruby Inox Part 3: Actions Here is the pursuit of a series of related articles...
  3. Ruby Inox Part 3.1: Actions Here is a short update for the Ruby Inox Part3:...
  4. Ruby Inox Part 6: Widget We are steering toward a graphical user interface. In this...
  5. Aspect the Ruby way The main purpose of this file is to be able...

Tags: , , , , ,

One Response to “Brain damaged assertions chain”

  1. William says:

    In the code above there is one error that prevents the execution of chained block codes! Change line 4 to 8 with the this code:

    __nu = “__id__:__send__:instance_eval”.split(‘:’)
    Object.new.methods.each { |m|
    undef_method m unless __nu.include?(m)
    }

Your Reply


Ironic Wolf is Digg proof thanks to caching by WP Super Cache