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:
- default raise behavior
- one place to fine tune the raise behavior
- compact one liner syntax for short assert
- long complicated assert should be possible
- use the existing methods of the objects
- 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 =
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
r then :not_or
when :not_and then :and
when :not_or then
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
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:
- Ruby Inox Part 2: A Point class Alright, the journey begins. I received the first specification! If...
- Ruby Inox Part 3: Actions Here is the pursuit of a series of related articles...
- Ruby Inox Part 3.1: Actions Here is a short update for the Ruby Inox Part3:...
- Ruby Inox Part 6: Widget We are steering toward a graphical user interface. In this...
- Aspect the Ruby way The main purpose of this file is to be able...
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: