Skip to content

v2.4.0

Compare
Choose a tag to compare
@Iltotore Iltotore released this 30 Dec 09:59
· 40 commits to main since this release

Introduction

This release adds support for several libraries and other minor features.

Main changes

Doobie & Skunk support

You now can integrate refined types to your database using Skunk or Doobie.

Doobie:

import doobie.*
import doobie.implicits.*

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.all.*
import io.github.iltotore.iron.doobie.given

opaque type CountryCode = Int :| Positive
object CountryCode extends RefinedTypeOps[Int, Positive, CountryCode]

opaque type CountryName = String :| Not[Blank]
object CountryName extends RefinedTypeOps[String, Not[Blank], CountryName]

opaque type Population = Int :| Positive
object Population extends RefinedTypeOps[Int, Positive, Population]

//Refined columns of a table
case class Country(code: CountryCode, name: CountryName, pop: Population)

//Interpolation with refined values
def biggerThan(minPop: Population) =
  sql"""
    select code, name, population, indepyear
    from country
    where population > $minPop
  """.query[Country]

Skunk:

import skunk.*
import skunk.implicits.*
import skunk.codec.all.*
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.all.*
import io.github.iltotore.iron.skunk.*
import io.github.iltotore.iron.skunk.given

type Username = String :| Not[Blank]

// refining a codec at usage site
val a: Query[Void, Username] = sql"SELECT name FROM users".query(varchar.refined)

// defining a codec for a refined opaque type
opaque type PositiveInt = Int :| Positive
object PositiveInt extends RefinedTypeOps[Int, Positive, PositiveInt]:
  given codec: Codec[PositiveInt] = int4.refined[Positive]

// defining a codec for a refined case class
final case class User(name: Username, age: PositiveInt)
given Codec[User] = (varchar.refined[Not[Blank]] *: PositiveInt.codec).to[User]

uPickle support

Iron (via iron-upickle) now provides Writer/Reader instances for uPickle:

import upickle.default._
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.all.*
import io.github.iltotore.iron.upickle.given

opaque type Username = String :| Alphanumeric
object Username extends RefinedTypeOps[String, Alphanumeric, Username]

opaque type Age = Int :| Positive
object Age extends RefinedTypeOps[Int, Positive, Age]

case class User(name: Username, age: Age) derives ReadWriter

write(User("Iltotore", 19)) //{"name":"Iltotore","age":19}

read[User]("""{"name":"Iltotore","age":19}""") //User("Iltotore", 19)
read[User]("""{"name":"Iltotore","age":-19}""") //AbortException: Should be strictly positive
read[User]("""{"name":"Il_totore","age":19}""") //AbortException: Should be alphanumeric

Decline support

You can read refined values from command arguments using Decline and iron-decline.

import cats.implicits.*
import com.monovore.decline.*
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.all.*
import io.github.iltotore.iron.decline.given

type Person = String :| Not[Blank]

opaque type PositiveInt <: Int = Int :| Positive
object PositiveInt extends RefinedTypeOps[Int, Positive, PositiveInt]

object HelloWorld extends CommandApp(
  name = "hello-world",
  header = "Says hello!",
  main = {
    // Defining an option for a constrainted type
    val userOpt =
      Opts.option[Person]("target", help = "Person to greet.")
        .withDefault("world")

    // Defining an option for a refined opaque type
    val nOpt =
      Opts.option[PositiveInt]("quiet", help = "Number of times message is printed.")
        .withDefault(PositiveInt(1))

    (userOpt, nOpt).mapN { (user, n) => 
      (1 to n).map(_ => println(s"Hello $user!"))
    }
  }
)

New constraint aliases

  • Positive0/Negative0: equivalent to Positive/Negative but including 0 (aka non strict positivity/negativity)
  • SemanticVersion: ensure that a String respects Semantic Versioning format

Contributors

Full Changelog: v2.3.0...v2.4.0