require "constrain/version"
module Constrain
# Raised on any error
class Error < StandardError; end
# Raised if types doesn't match a class expression
class TypeError < Error
def initialize(value, exprs)
super "Expected #{value.inspect} to match #{Constrain.fmt_exprs(exprs)}"
end
end
# Check that value matches one of the class expressions. Raises a
# Constrain::Error if the expression is invalid and a Constrain::TypeError if
# the value doesn't match
def constrain(value, *exprs)
return if exprs.any? { |expr| Constrain.check(value, expr) }
error = TypeError.new(value, exprs)
error.set_backtrace(caller[1..-1])
raise error
end
# Return true if the value matches the class expression. Raises a
# Constrain::Error if the expression is invalid
def self.check(value, expr)
case expr
when Class
value.is_a?(expr)
when Array
!expr.empty? or raise Error, "Empty array"
value.is_a?(Array) && value.all? { |elem| expr.any? { |e| check(elem, e) } }
when Hash
value.is_a?(Hash) && value.all? { |key, value|
expr.any? { |key_expr, value_expr|
[[key, key_expr], [value, value_expr]].all? { |value, expr|
if expr.is_a?(Array) && (expr.size > 1 || expr.first.is_a?(Array))
expr.any? { |e| check(value, e) }
else
check(value, expr)
end
}
}
}
when Proc
expr.call(value)
else
raise Error, "Illegal expression #{expr.inspect}"
end
end
# Render a class expression as a String. Same as
# exprs.map(&:inspect).join(", ") except that Proc objects are rendered as
# "Proc@:"
def self.fmt_exprs(exprs)
exprs.map { |expr| fmt_expr(expr) }.join(", ")
end
# Render a class expression as a String. Same as +expr.inspect+ except that
# Proc objects are rendered as "Proc@>:"
#
def self.fmt_expr(expr)
case expr
when Class; expr.to_s
when Array; "[" + expr.map { |expr| fmt_expr(expr) }.join(", ") + "]"
when Hash; "{" + expr.map { |k,v| "#{fmt_expr(k)} => #{fmt_expr(v)}" }.join(", ") + "}"
when Proc; "Proc@#{expr.source_location.first}:#{expr.source_location.last}"
else
raise Error, "Illegal expression"
end
end
end