Skip to content

Compile time validation of literal values built from strings

License

Notifications You must be signed in to change notification settings

satorg/typelevel.literally

 
 

Repository files navigation

Literally

Compile time validation of literal values built from strings.

Consider a type like Port:

case class Port private (value: Int)

object Port {
  val MinValue = 0
  val MaxValue = 65535

  def fromInt(i: Int): Option[Port] =
    if (i < MinValue || i > MaxValue) None else Some(new Port(i))
}

This library simplifies the definition of literal values which are validated at compilation time:

val p: Port = port"8080"
// p: Port = Port(8080)

val q: Port = port"100000"
// <console>:17: error: invalid port - must be integer between 0 and 65535

Validation is performed at compile time. This library provides the macro implementations for both Scala 2 and Scala 3 which powers custom string literals.

Usage

Defining a custom string literal for a type A involves:

  • Implementing an org.typelevel.literally.Literally[A] instance
  • Defining an extension method on a StringContext which uses the defined Literally[A] instance
import org.typelevel.literally.Literally

object literals:      
  extension (inline ctx: StringContext)
    inline def port(inline args: Any*): Port =
      ${PortLiteral('ctx, 'args)}

  object PortLiteral extends Literally[Port]:
    def validate(s: String)(using Quotes) =
      s.toIntOption.flatMap(Port.fromInt) match
        case None => Left(s"invalid port - must be integer between ${Port.MinValue} and ${Port.MaxValue}")
        case Some(_) => Right('{Port.fromInt(${Expr(s)}.toInt).get})

The same pattern is used for Scala 2, though the syntax for extension methods and macros are a bit different:

import scala.util.Try
import org.typelevel.literally.Literally

object literals {
  implicit class short(val sc: StringContext) extends AnyVal {
    def port(args: Any*): Port = macro PortLiteral.make
  }

  object PortLiteral extends Literally[Port] {
    def validate(c: Context)(s: String): Either[String, c.Expr[Port]] = {
      import c.universe.{Try => _, _}
      Try(s.toInt).toOption.flatMap(Port.fromInt) match {
        case None => Left(s"invalid port - must be integer between ${Port.MinValue} and ${Port.MaxValue}")
        case Some(_) => Right(c.Expr(q"Port.fromInt($s.toInt).get"))
      }
    }

    def make(c: Context)(args: c.Expr[Any]*): c.Expr[Port] = apply(c)(args: _*)
  }
}

The tests directory in this project has more examples.

About

Compile time validation of literal values built from strings

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Scala 100.0%