Skip to content

Constraint Usage

Iltotore edited this page Dec 25, 2021 · 17 revisions

This page informs on how to use Iron's constraints.

Note: When using Iron, you usually want to use the following imports:

//You can specify constraints you want if needed but be sure to import `given` or the given instance of your constraint
import io.github.iltotore.iron.*, constraint.{given, *}

//If you have other Iron modules like numeric:
import numeric.constraint.{given, *}

Basic usage

You can attach a constraint to:

  • Variables
  • Methods (Return type)
  • Parameters

using the type Constrained[A, B] or its alias /[A, B]:

inline def log(x: Double / Greater[0d]): Raw[Double] = x.map(Math.log)

You can also use type aliases:

type >[A, V] = A / Greater[V]

inline def log(x: Double > 0d): Raw[Double] = x.map(Math.log)

More information on the Constraint Creation Page

Note: You need to have a suitable constraint behaviour in the implicit scope.

Bad value handling

A Constrained[A, B] is a Refined[A] ensuring that A passed (successfully or not) the B constraint. Refined[A] is simply an alias for Either[IllegalValueError[A], A]. This Either-based structure allows functional error handling.

This functional approach allows constraints to synergise efficiently with other functional libraries like Cats or ZIO.

Fail fast

The fail-fast approach is useful when you need the precedent step to be successful before continuing.

import io.github.iltotore.iron.*, constraint.{given, *}, string.constraint.{given, *}

def parse(input: String / Match["[0-9]"]): Either[IllegalValueError[String], Double] = input.map(_.toDouble)

def inverse(x: Double \ 0d): Double \ 0d = x.map(1 / _)

//input can be some user input caught via cats
def process(input: String): Either[IllegalValueError[?], Double] = for {
  parsed <- parse(input)
  result <- inverse(parsed)
} yield result
process("5") //Right(0.2)
process("0") //Left(IllegalValueError)
process("a") //Left(IllegalValueError)

Accumulative failure

When you have multiple, independents, values to build a single result (Example: a form), we don't want to stop once the first illegal value: we want to fail for all bad inputs, we want to accumulate them.

Here is an example of error accumulation in a form

import io.github.iltotore.iron.*, constraint.{given, *}, string.constraint.{given, *}, catsSupport.*
import cats.implicits.*, cats.syntax.apply.*

case class Account(username: String, email: String, password: String)

object Account {

  //Here, the Match[String] constraint isn't natively implemented in Iron and is only used as example.
  
  //Type aliases are not mandatory. They can be used for readability purpose.
  type Username = String / (Match["^[a-zA-Z0-9]+"] DescribedAs "Username should be alphanumeroc")
  type Email = String / (Match["^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"] DescribedAs "Value must be an email")
  //At least one upper, one lower and one number
  type Password = String / (Match["^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])[a-zA-Z0-9]+"] DescribedAs
          "Password must contain at least an upper, a lower and a number")
  
  //Input values passed by the user
  def createAccount(username: Username, email: Email, password: Password): RefinedNec[Account] = (
    username.toField("username").toValidatedNec,
    email.toField("email").toValidatedNec,
    password.toField("password").toValidatedNec
  ).mapN(Account.apply)
}
//Right(Account(...))
Account.createAccount("Iltotore", "[email protected]", "SafePassword1")

//Left(NonEmptyChain(IllegalValueError("username", "Username should be alphanumeric")))
Account.createAccount("Il_totore", "[email protected]", "SafePassword1")

/* 
 * Left(NonEmptyChain(
 *   IllegalValueError("username", "Username should be alphanumeric"),
 *   IllegalValueError("password", "Password must contain at least an upper, a lower and a number")
 * ))
 */
Account.createAccount("Il_totore", "[email protected]", "this_is_not_fine")

Imperative usage

Since 1.2.0, you can handle bad values in an imperative style using the refined capability:

def log(x: Double > 0d): Raw[Double] = refined {
  Math.log(x)
}

In the second line at x, an implicit conversion will be tried. If the value does not satisfy the requirements, the result returned by the refined capability will be a Left containing the refinement error.

You can also "throw" explicitly a refinement error using the reject method:

def foo(x: Double / SomeConstraint): Raw[Double] = refined {
  reject(x, "Something went wrong")
}

foo(1d) //Left(IllegalValueError(1d, "Something went wrong")

Field-based error

You can use a field name instead of the input value for your errors. This is useful when working on a form or a REST API.

inline def log(x: Double > 0d): Refined[Double] = x.map(Math.log)

log(-1).toField("x") //IllegalValueError("x", "Value should be greater than the specified value")

Compile time evaluation

A constraint is evaluated at compile time if the value and the behaviour is fully inline. Otherwise, the compiler will inline as much as possible the constraint then evaluate at runtime.

Runtime fallback

The fallback behaviour can be configured using the -D iron.fallback argument:

  • error: Throw a compile time error
  • warn: Warn, then fallback to runtime
  • allow (default): Silently fallback to runtime

You can also configure this behaviour individually by extending the right trait:

  • Constraint.CompileTimeOnly: Throw a compile-time error if unable to evaluate.
  • Constraint.RuntimeOnly: Iron will not try to check this constraint at compile-time and directly fallback to runtime evaluation

Advanced usage

Algebra

Iron provides a fluent way to express complex constraints by chaining constraints with the same algebra.

The numeric module provides a mathematical algebra:

def cos(x: Double): Double / (-1d < ?? < 1d)

The ?? placeholder represents the value passed to the Constrained.

You can also chain multiple constraints:

def foo(x: Double / (-1d < ?? < 1d <= 2d)): Unit = ???

Note: you cannot chain constraints before the placeholder.

Check this page for more details about creation of such constraints.