Skip to content

v2.1.0

Compare
Choose a tag to compare
@Iltotore Iltotore released this 15 Apr 06:39
· 79 commits to main since this release
9465c5b

Introduction

This release brings new ergonomic enhancements for type refinements, ScalaCheck support and some minor fixes.

Main changes

Better compile-time errors

Compile-time errors (constraint not satisfied or not evaluable) are now fancier and more helpful:

def log(x: Double :| Positive): Double = Math.log(x)
log(-1.0)
-- Constraint Error --------------------------------------------------------
Could not satisfy a constraint for type scala.Double.

Value: -1.0
Message: Should be strictly positive
----------------------------------------------------------------------------
val runtimeValue: Double = ???
log(runtimeValue)
-- Constraint Error --------------------------------------------------------
Cannot refine non full inlined input at compile-time.
To test a constraint at runtime, use the `refine` extension method.

Note: Due to a Scala limitation, already-refined types cannot be tested at compile-time (unless proven by an `Implication`).

Inlined input: runtimeValue
----------------------------------------------------------------------------

Refine further

You can now refine an already-refined constraints incrementally. This is particularly useful for fine-grained validation errors. Example from the docs:

type Username = DescribedAs[Alphanumeric, "Username should be alphanumeric"]
type Password = DescribedAs[
  Alphanumeric & MinLength[5] & Exists[Letter] & Exists[Digit],
  "Password should have at least 5 characters, be alphanumeric and contain at least one letter and one digit"
]

case class User(name: String :| Username, password: String :| Password)

def createUser(name: String, password: String): Either[String, User] =
  for
    validName     <- name.refineEither[Username]
    alphanumeric  <- password.refineEither[Alphanumeric]
    minLength     <- alphanumeric.refineFurtherEither[MinLength[5]]
    hasLetter     <- minLength.refineFurtherEither[Exists[Letter]]
    validPassword <- hasLetter.refineFurtherEither[Exists[Digit]]
  yield
    User(validName, validPassword)

createUser("Iltotore", "abc123") //Right(User("Iltotore", "abc123"))
createUser("Iltotore", "abc1") //Left("Should have a minimum length of 5")
createUser("Iltotore", "abcde") //Left("At least one element: (Should be a digit)")
createUser("Iltotore", "abc123  ") //Left("Should be alphanumeric")

Assuming constraints

You can now assume that a constraint holds using the .assume[C]. It basically acts like an alias for asInstanceOf[A :| C].

val x: Int :| GreaterEqual[0] = util.Random.nextInt(10).assume //Compiles

Scalacheck support

Iron now provides via the iron-scalacheck Arbitrary instances for refined types:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.*
import io.github.iltotore.iron.scalacheck.numeric.given

forAll { (x: Int :| Positive) => x > 0 } //Success

All refined types (including custom ones) are supported but some constraints have customized and more optimized generators.

Contributors

Full Changelog: v2.0.0...v2.1.0