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 = :or 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 :or then :not_or when :not_and then :and when :not_or then :or 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 :or 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:...
- Aspect the Ruby way The main purpose of this file is to be able...
- Ruby Inox Part 6: Widget We are steering toward a graphical user interface. In this...
- Ruby Inox Part 4: Property Last time, we took a look at Actions. A standard...



ShareThis
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: