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