Project

must_be

0.02
No commit activity in last 3 years
No release in over 3 years
must_be provides runtime assertions which can easily be disabled in production environments. Likewise, the notifier can be customized to raise errors, log failure, enter the debugger, or anything else.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 1.4
~> 1.2
 Project Readme

must_be Runtime Assertions

Ruby doesn't have a static type system. Tests and specs are well and good. But at the end of the day, Cthulhu reigns and sanity needs checking. must_be provides runtime assertions in all kinds of delicious Ruby flavors.

You can configure must_be to notify you of trouble in any way imaginable. Just set MustBe.notifier to any proc or other call worthy object. By default must_be raises an error, but logging and debugging notifiers come included. Furthermore, you can disable must_be at any time if its performance penalty proves unsatisfactory.

Examples

After installing:

> sudo gem install must_be

Begin with customary oblations:

require 'rubygems'
require 'must_be'

Now Object has a number of must* and must_not* methods. Here are several examples of each. When an example notifies, the message corresponding to its MustBe::Note error is listed on the following comment line (#=> or #~> for a regexp match).

Basic

For your everyday, average sanity:

87.must_be_a(Float, Integer)
87.must_be_a(Symbol, String)
#=> 87.must_be_a(Symbol, String), but is a Integer

87.must_not_be_a(Symbol, String, "message")
87.must_not_be_a(Float, Integer, "message")
#=> 87.must_not_be_a(Float, Integer, "message"), but is a Integer

must_be_in uses include? to check. Multiple arguments are grouped together as an array:

2.must_be_in(1, 2, 3)
2.must_be_in([1, 2, 3])
2.must_be_in(1 => 3, 2 => 4)
2.must_be_in(1..3)
2.must_be_in
#=> 2.must_be_in

2.must_not_be_in
2.must_not_be_in(4, 5)
2.must_not_be_in(1 => 3, 2 => 4)
#=> 2.must_not_be_in({1=>3, 2=>4})

must_be_boolean ensures the value is true or false:

true.must_be_boolean
false.must_be_boolean
nil.must_be_boolean
#=> nil.must_be_boolean

Sometimes close is good enough:

2.must_be_close(2.0)
2.must_be_close(2.01)
2.must_be_close(2.1)
#=> 2.must_be_close(2.1, 0.1), difference is 0.1

2.must_be_close(2.1, 6)
2.must_be_close(9.0, 6)
#=> 2.must_be_close(9.0, 6), difference is 7.0

must_be uses case (===) equality:

34.must_be(Integer, :all)
:all.must_be(Integer, :all)
:none.must_be(Integer, :all)
#=> :none.must_be(Integer, :all), but matches Symbol

5.must_be(1..5)
5.must_be(1...5)
#=> 5.must_be(1...5), but matches Integer

5.must_not_be(1...5)
3.must_not_be(1...5)
#=> 3.must_not_be(1...5), but matches Integer

true.must_be
nil.must_be
#=> nil.must_be, but matches NilClass
false.must_be
#=> false.must_be, but matches FalseClass

nil.must_not_be
:hello.must_not_be
#=> :hello.must_not_be, but matches Symbol

:yep.must_be(lambda {|v| v == :yep})
:nope.must_be(lambda {|v| v == :yep})
#~> :nope.must_be\(#<Proc:0x[^.]+?>\), but matches Symbol

:yep.must_not_be(lambda {|v| v == :nope})
:nope.must_not_be(lambda {|v| v == :nope})
#~> :nope.must_not_be\(#<Proc:0x[^.]+?>\), but matches Symbol

Proxy

must either takes a block, or it returns a proxy for checking predicates:

7.must {|x| x > 3}
7.must {|x| x < 3}
#=> 7.must {}

7.must_not {|x| x < 3}
7.must_not {|x| x > 3}
#=> 7.must_not {}

7.must == 7
7.must > 3
7.must.even?
#=> 7.must.even?

7.must_not == 8
7.must_not < 3
7.must_not.odd?
#=> 7.must_not.odd?

Module#attr_typed

attr_typed is like attr_accessor but with type checking:

class Typed
  attr_typed :v, Symbol
  attr_typed :n, String, Integer
  attr_typed :o, &:odd?
end

t = Typed.new
t.v = :hello
t.v = "world"
#=> attribute `v' must be a Symbol, but value "world" is a String

t.n = 411
t.n = 4.1
#=> attribute `n' must be a String or Integer, but value 4.1 is a Float

t.o = 7
t.o = 8
#=> attribute `o' cannot be 8

Containers

It's good to be sure that an array or hash contains the right stuff:

["okay", :ready, "go"].must_only_contain(Symbol, String)
["okay", :ready, 4].must_only_contain(Symbol, String)
#=> must_only_contain: 4.must_be(Symbol, String), but matches Integer in container ["okay", :ready, 4]

["okay", :ready, "go"].must_not_contain(Numeric)
["okay", :ready, 4].must_not_contain(Numeric)
#=> must_not_contain: 4.must_not_be(Numeric), but matches Integer in container ["okay", :ready, 4]

[].must_only_contain(:yes, :no)
[:yep].must_only_contain(:yes, :no)
#=> must_only_contain: :yep.must_be(:yes, :no), but matches Symbol in container [:yep]

[].must_not_contain(:yes, :no)
[:yes, :no].must_not_contain(:yes, :no)
#=> must_not_contain: :yes.must_not_be(:yes, :no), but matches Symbol in container [:yes, :no]

[0, [], ""].must_only_contain
[nil].must_only_contain
#=> must_only_contain: nil.must_be, but matches NilClass in container [nil]

[nil, false].must_not_contain
[0].must_not_contain
#=> must_not_contain: 0.must_not_be, but matches Integer in container [0]

{:welcome => :home}.must_only_contain(Symbol => Symbol)
{:symbol => :s, :Integer => 5}.must_only_contain(Symbol => [Symbol, Integer])
{5 => :s, 6 => 5, :t => 5, :s => :s}.must_only_contain([Symbol, Integer] => [Symbol, Integer])
{6 => 5}.must_only_contain(Symbol => Integer, Integer => Symbol)
#=> must_only_contain: pair {6=>5} does not match [{Symbol=>Integer, Integer=>Symbol}] in container {6=>5}

{:welcome => nil}.must_not_contain(nil => Object)
{nil => :welcome}.must_not_contain(nil => Object)
#=> must_not_contain: pair {nil=>:welcome} matches [{nil=>Object}] in container {nil=>:welcome}

{:welcome => :home}.must_only_contain
{:welcome => nil}.must_only_contain
#=> must_only_contain: pair {:welcome=>nil} does not match [] in container {:welcome=>nil}

{nil => false, false => nil}.must_not_contain
{nil => 0}.must_not_contain
#=> must_not_contain: pair {nil=>0} matches [] in container {nil=>0}

It's better to be sure that a array or hash always contains the right stuff:

numbers = [].must_only_ever_contain(Numeric)
numbers << 3
numbers << :float
#=> must_only_ever_contain: Array#<<(:float): :float.must_be(Numeric), but matches Symbol in container [3, :float]

financials = [1, 4, 9].must_never_ever_contain(Float)
financials.map!{|x| Math.sqrt(x)}
#=> must_never_ever_contain: Array#map! {}: 3.0.must_not_be(Float), but matches Float in container [1.0, 2.0, 3.0]

MustBe::MustOnlyEverContain.register

It's best to be sure that your custom container contains the right stuff:

class Box  
  attr_accessor :contents
  
  def self.[](contents = nil)
    new(contents)
  end
  
  def initialize(contents = nil)
    @contents = nil
  end
  
  def each
    yield(contents) unless contents.nil?
  end
  
  def empty!
    self.contents = nil
  end
  
  def inspect
    "Box[#{contents.inspect}]"
  end
end

MustOnlyEverContain.register(Box) do
  def contents=(contents)
    must_check_member(contents)
    super
  end
  
  must_check_contents_after :empty!
end

box = Box[:hello].must_only_ever_contain(Symbol)
box.contents = :world
box.contents = 987
#=> must_only_ever_contain: Box#contents=(987): 987.must_be(Symbol), but matches Integer in container Box[987]

box = Box[2].must_never_ever_contain(nil)
box.contents = 64
box.empty!
#=> must_never_ever_contain: Box#empty!: nil.must_not_be(nil), but matches NilClass in container Box[nil]

Core

Define new must-assertions by using must_notify:

must_notify("message")
#=> message

must_notify(:receiver, :method, [:args], nil, ", additional message")
#=> :receiver.method(:args), additional message

note = MustBe::Note.new("message")
must_notify(note)
#=> message

Use must_check to temporarily override MustBe.notify usually as part of a bigger, better must-assertion.

note = must_check {}
note.must == nil

note = must_check { :it.must_not_be }
must_notify(note)
#=> :it.must_not_be, but matches Symbol

def add_more_if_notifies
  must_check(lambda do
    yield
  end) do |note|
    MustBe::Note.new(note.message+" more")
  end
end

result = add_more_if_notifies { 5 }
result.must == 5

add_more_if_notifies { must_notify("learn") }
#=> learn more

Nonstandard Control Flow

Last, but not least, we have must-assertions for raising and throwing.

def rescuing
  yield
rescue
  raise if $!.is_a? MustBe::Note
end

rescuing { must_raise(/match/) { raise ArgumentError, "should match" } }
must_raise { "But it doesn't!" }
#=> main.must_raise {}, but nothing was raised

must_not_raise { "I'm fine."}
rescuing { must_not_raise { raise } }
#=> main.must_not_raise {}, but raised RuntimeError

catch(:ball) { must_throw { throw :ball } }
catch(:ball) { must_throw(:party) { throw :ball } }
#=> main.must_throw(:party) {}, but threw :ball

catch(:ball) { must_not_throw(:ball, :fiercely) { throw :ball, :gently } }
catch(:ball) { must_not_throw { throw :ball } }
#=> main.must_not_throw {}, but threw :ball

Configuration

Set MustBe.notifier as needed. It takes a MustBe::Note as an argument and raises an exception unless its return value is false or nil:

MustBe.notifier = lambda do |note|
  puts note
  false
end

You can freely MustBe.disable and MustBe.enable as needed:

MustBe.disable

3.must == 4
# no message

MustBe.enable

3.must == 4
#=> 3.must.==(4)

Before requiring must_be, you can use ENV['MUST_BE__NOTIFIER'] to set the notifier:

# Default: just raises the note.
ENV['MUST_BE__NOTIFIER'] = :raise

# Puts the note together with its backtrace.
ENV['MUST_BE__NOTIFIER'] = :log

# Invokes ruby-debug.
ENV['MUST_BE__NOTIFIER'] = :debug

# Calls MustBe.disable.
ENV['MUST_BE__NOTIFIER'] = :disable

By default MustBe is mixed into Object. If you want to mix MustBe selectively, set:

ENV['MUST_BE__DO_NOT_AUTOMATICALLY_INCLUDE_IN_OBJECT'] = "anything"

Enjoy must_be.