Must

add Object#must method to constrain its origin and conversions

# can write like this
  num = params[:num].to_i.must.match(1..300) {10}

# rather than
  num = params[:num].to_i
  num = 10 unless (1..300) === num

and has duck-type features

  1.must.duck?(:to_s)  # => true
  io.must.duck(:write) { io.extend Writable }

and has struct assetions

  pages = [{:name=>"...", :url=>"...",} ...]
  pages.must.struct([Hash])

You want Boolean class? try this!

  flag = hash["flag"].must(true, false)

Asking Methods

be      : check whether object equals to the argument
kind_of : check whether object is a kind of the arguments
coerced : check whether object can be coerced to the argument
blank   : check whether object is blank?
exist   : check whether object is not nil (NOTE: false is ok)

Logical Methods

not : logical NOT

Nop Methods

a  : return self
an : return self

These effect nothing but exist only for English grammar.

Duck Methods

duck("foo")  : check whether object responds to "foo" method.
duck(:foo)   : same above
duck(".foo") : same above
duck("#foo") : check whether object has "foo" instance method. (tested only in class/module)
duck?(...)   : acts same as "duck", but this returns a just boolean
duck!("foo") : if foo exists, call it. otherwise raises Invalid

Struct Methods

struct(...)  : check whether object has a same struct with ...
struct?(...) : acts same as "struct", but this returns a just boolean

Basic Examples

# test its value exactly

1.must.be 1              # => 1
[1,2,3].must.be [1,2,3]  # => [1,2,3]

# exceptions

1.must.be []             # Must::Invalid exception
1.must.be([]) {:ng}      # => :ng
1.must.be(1) {:ng}       # => 1

# as default value

name = params[:name].must.not.be.blank{ "No name" }

# existing test

1.must.exist             # => 1
nil.must.exist           # Must::Invalid exception
false.must.exist         # => false

# test class : ensures that a class of the object is one of given arguments

1.must.be.kind_of(Integer)         # => 1
1.must.be.kind_of(Integer, Array)  # => 1
[].must.be.kind_of(Integer, Array) # => []
1.must.be.kind_of(String, Array)   # Must::Invalid: expected String/Array but got Fixnum

# must(*args) is a syntax sugar for kind_of

1.must(Integer)          # same as "1.must.be.kind_of(Integer)"

# coercing : looks like kind_of except converting its value if possible

1.must.be.coerced(Integer, String => proc{|val| val.to_i})    # => 1
"1".must.be.coerced(Integer, String => proc{|val| val.to_i})  # => 1
"1".must.be.coerced(Integer, String => :to_i)                 # => 1     (NOTE: inline Symbol means sending the method)
"1".must.be.coerced(Integer, Symbol, String => proc{:to_i})   # => :to_i (NOTE: use proc to return Symbol itself)

# struct assertions

uris = build_uris		# ex) [{:host=>"...", :port=>"..."}, ...]
uris.must.struct([Hash])

Actual Examples

1)

normal code:

  def set_reader(reader)
    if reader.is_a?(CSV::Reader)
      @reader = reader
    elsif file.is_a?(String)
      @reader = CSV::Reader.create(i)
    elsif file.is_a?(Pathname)
      @reader = CSV::Reader.create(reader.read)
    else
      raise 'invalid reader'
    end
  end

refactor above code with must plugin

  def set_reader(reader)
    @reader = reader.must.be.coerced(CSV::Reader, Pathname=>:read, String=>{|i| CSV::Reader.create(i)}) {raise 'invalid reader'}
  end

2)

class DateFolder
  def initialize(date)
    @date = date.must.be.coerced(Date, String=>proc{|i| Date.new(*i.scan(/\d+/).map{|i|i.to_i})})
  end
end

# this can accept both formats

DateFolder.new Date.today
DateFolder.new "2008-12-9"

NOTE

“must(*args)” is a shortcut for not “be(*args)” but “kind_of(*args)” and “struct(*args)”.

1.must(1)        # => 1
1.must(Fixnum)   # => 1
1.must(2)        # => 1    # NOTE
1.must.be(2)     # Invalid
Fixnum.must(1)   # => Fixnum

Bundled Class

struct = Must::StructInfo.new({"1.1" => {"jp"=>[{:a=>0},{:b=>2}]}})
struct.types   # => [Hash, String, Array, Symbol, Fixnum]
struct.compact # => {String=>{String=>[{Symbol=>Fixnum}]}}
struct.inspect # => "{String=>{String=>[{Symbol=>Fixnum}]}}"

TODO

* add proper error messages

Install

gem install must

% irb -r rubygems -r must
irb(main):001:0> 1.must.be 1
=> 1

Github

http://github.com/maiha/must

Author

maiha@wota.jp