A library for defining and validating data structures.
Constraints provide the foundation for data validation in Stannum. Fundamentally, each Stannum::Constraintencapsulates a predicate - a statement that can be either true or false - that can be applied to other objects. If the statement is true about the object, that object “matches” the constraint. If the statement is false, the object does not match the constraint.
The easiest way to define a constraint is by passing a block to Stannum::Constraint.new:
constraint = Stannum::Constraint.new do |object|
object.is_a?(String) && !object.empty?
end
Here, we’ve created a very simple constraint, which will match any non-empty string and will not match empty strings or other objects. When you define a constraint using Stannum::Constraint.new, the constraint will pass for an object if and only if the block returns true for that object. We can also pass in additional metadata about the constraint such as a type or message to display (see Errors, Types and Messages, below).
Defining a constraint is only half the battle - next, we need to use the constraint. Each Stannum::Constraint defines a standard set of methods to match objects.
First, the #matches? method will return true if the object matches the constraint, and will return false if the object does not match.
constraint.matches?(nil)
#=> false
constraint.matches?('')
#=> false
constraint.matches?('Greetings, programs!')
#=> true
Knowing that an object does not match isn’t always enough information - we need to know why. Stannum defines the Stannum::Errors object for this purpose (see Errors). We can use the #match method to both check whether an object matches the constraint and return any errors with one method call.
status, errors = constraint.matches?(nil)
status
#=> false
errors
#=> an instance of Stannum::Errors
errors.empty?
#=> false
status, errors = constraint.match('Greetings, programs!')
status
#=> true
errors
#=> an instance of Stannum::Errors
errors.empty?
#=> true
Finally, if we already know that an object does not match the constraint, we can check its errors using the #errors_for method.
errors = constraint.errors_for(nil)
#=> an instance of Stannum::Errors
errors.empty?
#=> false
errors.summary
#=> 'is invalid'
Important Note: Stannum does not guarantee that #errors_for will return an empty Errors object for an object that matches the constraint. Always check whether the object matches the constraint before checking the errors.
A constraint can also be used to check if an object does not match the constraint. Each Stannum::Constraint defines helpers for the negated use case.
The #does_not_match? method is the inverse of #matches?. It will return false if the object matches the constraint, and will return true if the object does not match.
constraint.does_not_match?(nil)
#=> true
status, errors = constraint.negated_match('')
status
#=> true
errors
#=> an instance of Stannum::Errors
errors.empty?
#=> true
constraint.does_not_match?('Greetings, programs!')
#=> false
Negated matches can also generate errors objects. Whereas the errors from a standard match will list how the object fails to match the constraint, the errors from a negated match will list how the object does match the constraint. The #negated_match method will both check that the object does not match the constraint and return the relevant errors, while the #negated_errors_for method will return the negated errors for a matching object.
errors = constraint.negated_errors_for('Greetings, programs!')
#=> an instance of Stannum::Errors
errors.empty?
#=> false
errors.summary
#=> 'is valid'
We can customize the error returned by the constraint for a non-matching object by setting the constraint type and/or message.
constraint = Stannum::Constraint.new(
message: 'must be even',
type: 'example.constraints.even'
) { |i| i.even? }
The constraint #type identifies the kind of constraint. For example, a case or conditional statement that checks for an error of a particular variety would look at the error’s type. The constraint #message, on the other hand, is a human-readable description of the error. A flash message or rendered might use the error’s message to display the status to the user. An API response might provide both the type and the message.
The constraint type and message are used to generate the corresponding error:
errors = constraint.errors_for(nil)
errors.count
#=> 1
errors.first.message
#=> 'must be even'
errors.first.type
#=> 'example.constraints.even'
The error message can also be generated automatically from the type (see Generating Messages).
Defining a subclass of Stannum::Constraint allows for greater control over the predicate logic and the generated errors.
class EvenIntegerConstraint < Stannum::Constraint
NEGATED_TYPE = 'examples.constraints.odd'
TYPE = 'examples.constraints.even'
def errors_for(actual, errors: nil)
return super if actual.is_a?(Integer)
(errors || Stannum::Errors.new)
.add('examples.constraints.type', type: Integer)
end
def matches?(actual)
actual.is_a?(Integer) && actual.even?
end
end
Let’s take it from the top. We start by defining ::NEGATED_TYPE and ::TYPE constraints. These serve two purposes: first, the constraint will use these values as the default #type and #negated_type properties, without having to pass in values to the constructor. Second, we are declaring the type of error this constraints will return to the rest of the code. This allows us to reference these values elsewhere, such as a case or conditional statement checking for the presense of this error.
Second, we define our #matches? method. This method takes one parameter (the object being matched) and returns either true or false. Our other matching methods - #does_not_match?, #match, and #negated_match - will delegate to this implementation unless we specifically override them.
Finally, we are defining the errors to be returned from our constraint using the #errors_for method. This method takes one required argument actual, which is the object being matched. If the object is an integer, then we fall back to the default behavior: super will add an error with a #type equal to the constraint’s #type (or the :type passed into the constructor, if any). If the object is not an integer, then we instead display a custom error. In addition to the error #type, we are defining some error #data. In addition, #errors_for can take an optional keyword :errors, which is either an instance of Stannum::Errors or nil. This allows the user to pass an existing errors object to #errors_for, which will add its own errors to the given errors object instead of creating a new one.
errors = constraint.errors_for(nil)
errors.count
#=> 1
errors.first.type
#=> 'examples.constraints.type'
errors.first.data
#=> { type: Integer }
errors = constraint.errors_for('')
errors.count
#=> 1
errors.first.type
#=> 'examples.constraints.even'
errors.first.data
#=> {}
We can likewise define the behavior of the constraint when negated. We’ve already set the ::NEGATED_TYPE constant, but we can go further and override the #does_not_match? and/or #negated_errors_for methods as well for full control over the behavior when performing a negated match.
Stannum includes a number of pre-defined constraints.
Boolean constraint will match either true or false.Enum constraint matches any of the given values.Equality constraint matches any object equal to the given value.Format constraint matches any string containing the given substring or matching the given pattern.Presence constraint matches any non-nil and non-empty? object. It’s inverse is the Absence constraint.Signature constraint matches objects that respond to the given methods.Type constraint matches objects that are instances of the given class or method.Uuid constraint matches a valid hyphen-separated UUID with either uppercase or lowercase alphanumeric characters.Some constraints are more useful in specific situations or when building or testing advanced behavior:
Anything constraint will match all objects.Delegator constraint defers its matching to another constraint.Nothing constraint does not match any objects.Union constraint matches any object that matches any of the given child constraints.A full list can be found in the Reference Documentation in the Constraints namespace.
Stannum also includes a number of pre-defined Type constraints, including ArrayType, HashType, IntegerType, NilType, and StringType.
A full list can be found in the Reference Documentation in the Type Constraints namespace.
Back to Documentation