:title: Constraint -- Ensure that objects always satisfy a set of constraints


This library provides a way to ensure that certain objects always
satisfy a specified set of constraints. An object that can be
constrained must be a child of <tt>Constraint::Shell</tt>, which acts as
a wrapper for the actual value. Constraints can added to subclasses and
its instances (per-object constraints). It is possible to define methods
that handle constraint violations and retrofit the object according to
its definition.

With some help from the developer, this library can also be used (to
some extent at least) to generate correct values and to find solutions
fitting the set of constraints.

Project:: http://rubyforge.org/projects/constraint/
Download:: http://rubyforge.org/frs/?group_id=748
Support:: http://rubyforge.org/forum/?group_id=748

In my experience, a certain type of error in dynamically typed languages
is caused by pushing a wrong object to a collection or similar. What
makes this type of error so awkward is that they often result in an
exception in a different part of your program, which is why it can
sometimes be unnecessarily difficult to track down what is actually
causing the problem. The goal of this library is to raise an exception
right where such a thing happens. While ruby is sometimes a little bit
hesitant in raising exceptions, this library tries to gently counteract
this attitude a little.

Be aware that this library introduces yet another level of indirection
and yet additional runtime checks which slows things down a little. If
you define CONSTRAINT_DISABLE before requiring the library, constraint
checks will be deactivated.


= Examples

Constrained classes should inherit from <tt>Constraint::Shell</tt>. The
subclass should provide a way for defining new instances (by defining
#new_constrained) and redefine #process_constrained_value. The
@constrained_value instance variable contains the shell's actual values.

== Basic use

require 'constraint'

class ConstrainedArray < Constraint::Shell
def new_constrained
[]
end

def process_constrained_value
@constrained_value.collect! {|e| yield e}
end
end

class NumericArray < ConstrainedArray
or_constraint("Numeric") {|o, e| e.kind_of?(Numeric)}
end

class EvenNumericArray < NumericArray
and_constraint("Even") {|o, e| e.modulo(2) == 0}
end

enarr = EvenNumericArray.new([2,4,6])
enarr
=> [2, 4, 6]
enarr << 8
=> [2, 4, 6, 8]
enarr << 9
(irb):23:in `irb_binding': Fixnum didn't meet constraint 'Even': 9 (ConstraintViolation)


class EvenInteger < Constraint::Shell
and_constraint("Numeric") {|o, e| e.kind_of?(Integer)}
and_constraint("Even") {|o, e| e.modulo(2) == 0}
end

enum = EvenInteger.new(2)
enum.and_constraint("LT10") {|o, e| e < 10}

enum + 2
=> 4

enum + 1
(irb):11:in `irb_binding': Fixnum didn't meet constraint 'Even': 3 (ConstraintViolation)

enum.on_constraint_violation('Even') {|o, e| (e / 2).round * 2}
enum + 1
=> 2


== Duckismo

class DuckPalace < ConstrainedArray
and_constraint('Quack') {|o, e| e.do_you_quack?}
and_constraint('Ticket') {|o, e| e.your_ticket_please == :valid}
end

whiteduckpalace = DuckPalace.new
whiteduckpalace.and_constraint('White') {|o, e| e.colour == 'white'}
whiteduckpalace.on_constraint_violation('White') do |o, e|
if e.respond_to?(:paint)
e.paint('white')
else
raise ConstraintViolation
end
end


== Generators

With some help from the programmer, +Constraint+ can generate valid
values and collect possible solutions fulfilling a set of constraints.
The only prerequisite is to define the method
#generate_constrained_value that gives the next value in line (starting
from a value given as argument, which can be nil) or raises a
Constraint::NoMoreValues exception if no more values are available. This
isn't the most sophisticated solution but it may be useful in some
situations.

Here is a basic example that illustrates this use of the library:

class NumInRange < Constraint::Shell
constraint_attr :min, 0
constraint_attr :max, nil

and_constraint('Numeric') {|o, e| e.kind_of?(Numeric)}
and_constraint('InRange') {|o, e| o.min <= e && (!o.max || o.max >= e)}
on_constraint_violation('InRange') {|o, e| (o.max && e > o.max) ? o.max : o.min}

def generate_constrained_value(value)
if value
if !max or @max > value
value + 1
else
raise Constraint::NoMoreValues
end
else
@min
end
end
end

class Num1To10 < NumInRange
@min = 1
@max = 10
end

p Num1To10.new
=> 1
p Num1To10.new(-10)
=> 1
p Num1To10.new(5.2) + 2.2
=> 7.4
p Num1To10.new(5.2) + 20.2
=> 10
p Num1To10.new.collect_constrained_values
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
p Num1To10.new(5).collect_constrained_values
=> [5, 6, 7, 8, 9, 10]
p Num1To10.new(5).collect_constrained_values_until {|e| e == 8}
=> [5, 6, 7]