Skip to content

Releases: kevin-lee/refined4s

v0.5.0

23 Dec 09:52
Compare
Choose a tag to compare

0.5.0 - 2023-12-23

New Features

  • [refined4s-cats] Add contraCoercible to F[B] => F[A] with Contravariant[F] and Coercible[A, B] (#152)

    This is useful to derive type-class instances for Newtype, Refined and InlinedRefined from the type-class instances of the actual types.

    e.g.)

    import refined4s.modules.cats.derivation.instances.contraCoercible
    
    inline given eqDerived[A, B](using coercible: Coercible[A, B], eqB: Eq[B]): Eq[A] =
      contraCoercible(eqB)
    
    inline given showDerived[A, B](using coercible: Coercible[A, B], showB: Show[B]): Show[A] =
      contraCoercible(showB)

    where B is the actual type and A could be a newtype or a refined type.


Improvement

  • Add missing inline to invalidReason in the sub-types of InlinedNumeric (#146)

  • [refined4s-pureconfig] Add type name to the error message of PureconfigRefinedConfigReader and refined4s.modules.pureconfig.derivation.instances.derivedRefinedConfigReader (#150)

    So a message like this

    
    Invalid value found: -2373683071661092303 with error: Invalid value: [-2373683071661092303]. It must be a positive Long
    

    should be like this instead.

    The value -2373683071661092303 cannot be created as the expected type, mytypes.blah.Id.Type, due to the following error: Invalid value: [-2373683071661092303]. It must be a positive Long
    

  • [refined4s-doobie] Make type-classes in refined4s.modules.doobie.derivation.instances inline (#156)

  • [refined4s-core] Improve compile-time error message for inline apply method (#158)

    The old compile-time error for the constant value passed to the inline apply method may look like this.

    [error] 2780 |      NegBigInt(1)
    [error]      |      ^^^^^^^^^^^^
    [error]      |A literal string is expected as an argument to `compiletime.error`. Got "Invalid value: [BigInt.apply(1)]. ".+(
    [error]      |  {
    [error]      |    val a$proxy4: BigInt = BigInt.apply(1)
    [error]      |    "It must be a negative BigInt":String:String
    [error]      |  }:String
    [error]      |)
    [error] one error found
    

    It is not so readable, and it is hard to comprehend what is wrong with the input.

    After this release, it will be like

    [error] 2780 |      NegBigInt(1)
    [error]      |      ^^^^^^^^^^^^
    [error]      |      Invalid value: [BigInt.apply(1)]. It must be a negative BigInt
    [error] one error found
    

Internal Housekeeping

  • [refined4s-circe] Update refined4s.modules.circe.derivation.instances.derivedEncoder to use contraCoercible and make it inline (#154)

    The old code has the following issue if it's turned into an inline method.

    [error] 13 |    a => encoder(coercible(a))
    [error]    |    ^^^^^^^^^^^^^^^^^^^^^^^^^^
    [error]    |An inline given alias with a function value as right-hand side can significantly increase
    [error]    |generated code size. You should either drop the `inline` or rewrite the given with an
    [error]    |explicit `apply` method.
    

  • Rename Coercible methods and type parameters (#160)
    • wrap and unwrap suffixed with M: M to TC (type constructor)
    • wrap and unwrap suffixed with MOfM: MOfM to HKT (higher-kinded type)
    • M[*] to F[*]
    • M1[*] to F[*] and M2[*] to G[*] e.g.) M1[M2[A]] to F[G[A]]

v0.4.0

16 Dec 09:00
Compare
Choose a tag to compare

0.4.0 - 2023-12-16

New Features

  • Add refined4s-doobie-ce2 and refined4s-doobie-ce3 modules to support doobie (#123)
  • [refined4s-doobie] Add Gets and Put for Newtype, Refined and InlinedRefined with Coercible and RefinedCtor (#124)
  • [refined4s-doobie] Add DoobiePut, DoobieNewtypeGet, DoobieRefinedGet, DoobieNewtypeGetPut and DoobieRefinedGetPut to have circe Get and Put derived from the actual type for Newtype, Refined and InlinedRefined (#127)

v0.3.0

15 Dec 12:51
Compare
Choose a tag to compare

0.3.0 - 2023-12-15

Changes

  • [refined4s-cats] Replace Eq and Show instances for the existing refined types with the ones derived from the actual types using Coercible (#107)

New Features

  • Add refined4s-circe module to support circe (#101)

  • [refined4s-circe] Add Encoder and Decoders for Newtype, Refined and InlinedRefined with Coercible and RefinedCtor (#103)

  • [refined4s-circe] Add CirceEncoder, CirceNewtypeDecoder, CirceRefinedDecoder, CirceNewtypeCodec and CirceRefinedCodec to have circe Encoder and Decoder derived from the actual type for Newtype, Refined and InlinedRefined (#104)

    import refined4s.modules.circe.derivation.*
    type MyNewtype = MyNewtype.Type
    object MyNewtype extends Newtype[String] with CirceEncoder[String]
    
    type MyRefinedType = MyRefinedType.Type
    object MyRefinedType extends Refined[String] with CirceEncoder[String] {
      override inline def invalidReason(a: String): String =
        "It has to be a non-empty String but got \"" + a + "\""
    
      override inline def predicate(a: String): Boolean = a != ""
    }
    
    type MyRefinedNewtype = MyRefinedNewtype.Type
    object MyRefinedNewtype extends Newtype[MyRefinedType] with CirceEncoder[MyRefinedType]
    type MyNewtype = MyNewtype.Type
    object MyNewtype extends Newtype[String] with CirceNewtypeDecoder[String]
    
    type MyRefinedType = MyRefinedType.Type
    object MyRefinedType extends Refined[String] with CirceRefinedDecoder[String] {
      override inline def invalidReason(a: String): String =
        "It has to be a non-empty String but got \"" + a + "\""
    
      override inline def predicate(a: String): Boolean = a != ""
    }
    
    type MyRefinedNewtype = MyRefinedNewtype.Type
    object MyRefinedNewtype extends Newtype[MyRefinedType] with CirceNewtypeDecoder[MyRefinedType]
  • [refined4s-core] Add PortNumber, SystemPortNumber, NonSystemPortNumber, UserPortNumber and DynamicPortNumber (#110)

  • Add refined4s-pureconfig module to support pureconfig (#112)

  • [refined4s-pureconfig] Add ConfigReader and ConfigWriters for Newtype, Refined and InlinedRefined with Coercible and RefinedCtor (#113)

    import refined4s.modules.pureconfig.derivation.instances.given
  • [refined4s-pureconfig] Add PureconfigNewtypeConfigReader and PureconfigRefinedConfigReader to provider ConfigReader for Newtype, Refined and InlinedRefined (#116)

    type MyNewtype = MyNewtype.Type
    object MyNewtype extends Newtype[String] with PureconfigNewtypeConfigReader[String]
    
    type MyRefinedType = MyRefinedType.Type
    object MyRefinedType extends Refined[String] with PureconfigRefinedConfigReader[String] {
      override inline def invalidReason(a: String): String =
        "It has to be a non-empty String but got \"" + a + "\""
    
      override inline def predicate(a: String): Boolean = a != ""
    }
    
    type MyRefinedNewtype = MyRefinedNewtype.Type
    object MyRefinedNewtype extends Newtype[MyRefinedType] with PureconfigNewtypeConfigReader[MyRefinedType]
    
    type MyInlinedRefinedType = MyInlinedRefinedType.Type
    object MyInlinedRefinedType extends InlinedRefined[String] with PureconfigRefinedConfigReader[String] {
      override inline def invalidReason(a: String): String =
        "It has to be a non-empty String but got \"" + a + "\""
    
      override inline def predicate(a: String): Boolean = a != ""
    
      override inline def inlinedPredicate(inline a: String): Boolean = a != ""
    }
    
    type MyInlinedRefinedNewtype = MyInlinedRefinedNewtype.Type
    object MyInlinedRefinedNewtype extends Newtype[MyRefinedType] with PureconfigNewtypeConfigReader[MyRefinedType]
  • [refined4s-pureconfig] Add PureconfigConfigWriter to provider ConfigWriter for Newtype, Refined and InlinedRefined (#118)

v0.2.0

10 Dec 10:42
Compare
Choose a tag to compare

0.2.0 - 2023-12-10

Changes

  • Rename refined4s.cats to refined4s.modules.cats (#94)
    import refined4s.modules.cats.derivation.*
    import refined4s.modules.cats.syntax.*

New Features

  • Add Eq[A] and Show[A] instances for the existing refined types (#95)

    So with

    import refined4s.modules.cats.derivation.instances.given

    Eq and Show instances for the following types are available

    • NegInt
    • NonNegInt
    • PosInt
    • NonPosInt
    • NegLong
    • NonNegLong
    • PosLong
    • NonPosLong
    • NegShort
    • NonNegShort
    • PosShort
    • NonPosShort
    • NegByte
    • NonNegByte
    • PosByte
    • NonPosByte
    • NegFloat
    • NonNegFloat
    • PosFloat
    • NonPosFloat
    • NegDouble
    • NonNegDouble
    • PosDouble
    • NonPosDouble
    • NegBigInt
    • NonNegBigInt
    • PosBigInt
    • NonPosBigInt
    • NegBigDecimal
    • NonNegBigDecimal
    • PosBigDecimal
    • NonPosBigDecimal
    • NonEmptyString
    • Uri

v0.1.0

10 Dec 07:51
Compare
Choose a tag to compare

0.1.0 - 2023-12-10

New Features

Add Refined[A] (#1)

A trait Refined[A] should provide a way to create a refined type with validation.
It should also provide a way to validate in compile-time if applicable.

So it should be able to do like this.

type NonEmptyString = NonEmptyString.Type

object NonEmptyString extends Refined[String] {
  inline override def predicate(a: String): Boolean = a != ""
}

NonEmptyString("Blah") // It compiles
NonEmptyString("") // A compile-time error

NonEmptyString.from("Blah") // Either[String, NonEmptyString] = Right(NonEmptyString("Blah"))
NonEmptyString.from("") // Either[String, NonEmptyString] = Left("Invalid value: ")

NonEmptyString.unsafeFrom("Blah") // NonEmptyString = NonEmptyString("Blah")
NonEmptyString.unsafeFrom("") // IllegalArgumentException

[core] Add NonEmptyString (#5)

NonEmptyString("blah") // compiles
NonEmptyString("") // compile-time error

NonEmptyString.from("blah")
// Either[String, NonEmptyString] = Right(NonEmptyString("blah"))

NonEmptyString.from("")
// Either[String, NonEmptyString] = Left(Invalid value: []. It should be a non-empty String value but got [])

NonEmptyString.unsafeFrom("blah")
// NonEmptyString = NonEmptyString("blah")

NonEmptyString.unsafeFrom("")
// IllegalArgumentException(Invalid value: []. It should be a non-empty String value but got []) is thrown

[core] Add numeric.NegInt (#7)

NegInt(-1) // compiles
NegInt(0) // compile-time error
NegInt(1) // compile-time error

NegInt.from(-123)
// Either[String, NegInt] = Right(NegInt(-123))

NegInt.from(123)
// Either[String, NegInt] = Left(Invalid value: 123. It must be a negative Int)

NegInt.unsafeFrom(-999)
// NegInt = NegInt(-999)

NegInt.unsafeFrom(999)
// IllegalArgumentException(Invalid value: 999. It must be a negative Int) is thrown

[core] Add numeric.NonPosInt (#8)

NonPosInt(-1) // compiles
NonPosInt(0)  // compiles
NonPosInt(1)  // compile-time error

NonPosInt.from(-123)
// Either[String, NonPosInt] = Right(NonPosInt(-123))

NonPosInt.from(123)
// Either[String, NonPosInt] = Left(Invalid value: 123. It must be a non-positive Int)

NonPosInt.unsafeFrom(-999)
// NegInt = NonPosInt(-999)

NonPosInt.unsafeFrom(999)
// IllegalArgumentException(Invalid value: 999. It must be a non-positive Int) is thrown

[core] Add numeric.PosInt (#9)

PosInt(1) // compiles
PosInt(0) // compile-time error
PosInt(-1) // compile-time error

PosInt.from(123)
// Either[String, PosInt] = Right(PosInt(123))

PosInt.from(-123)
// Either[String, PosInt] = Left(Invalid value: -123. It must be a positive Int)

PosInt.unsafeFrom(999)
// PosInt = PosInt(999)

PosInt.unsafeFrom(-999)
// IllegalArgumentException(Invalid value: -999. It must be a positive Int) is thrown

[core] Add numeric.NonNegInt (#10)

NonNegInt(1) // compiles
NonNegInt(0) // compiles
NonNegInt(-1) // compile-time error

NonNegInt.from(123)
// Either[String, NonNegInt] = Right(NonNegInt(123))

NonNegInt.from(-123)
// Either[String, NonNegInt] = Left(Invalid value: -123. It must be a non-negative Int)

NonNegInt.unsafeFrom(999)
// NonNegInt = NonNegInt(999)

NonNegInt.unsafeFrom(-999)
// IllegalArgumentException(Invalid value: -999. It must be a non-negative Int) is thrown

[core] Add numeric.Numeric (#11)

Numeric[A: math.Ordering] provides Ordering[Numeric[A]#Type] derived from A and Ordered[Numeric[A]#Type] converted from Ordering[Numeric[A]#Type].


[core] Add numeric.NegLong (#18)

NegLong(-1L) // compiles
NegLong(0L)  // compile-time error
NegLong(1L)  // compile-time error

NegLong.from(-123L)
// Either[String, NegLong] = Right(NegLong(-123L))

NegLong.from(123L)
// Either[String, NegLong] = Left("Invalid value: 123L. It must be a negative Long")

NegLong.unsafeFrom(-999L)
// NegLong = NegLong(-999L)

NegLong.unsafeFrom(999L)
// IllegalArgumentException(Invalid value: 999L. It must be a negative Long) is thrown

[core] Add numeric.NonNegLong (#19)

NonNegLong(1L)  // compiles
NonNegLong(0L)  // compiles
NonNegLong(-1L) // compile-time error

NonNegLong.from(123L)
// Either[String, NonNegLong] = Right(NonNegLong(123L))

NonNegLong.from(-123L)
// Either[String, NonNegLong] = Left("Invalid value: -123L. It must be a non-negative Long")

NonNegLong.unsafeFrom(999L)
// NonNegLong = NonNegLong(999L)

NonNegLong.unsafeFrom(-999L)
// IllegalArgumentException(Invalid value: -999L. It must be a non-negative Long) is thrown

[core] Add numeric.PosLong (#20)

PosLong(1L)  // compiles
PosLong(0L)  // compile-time error
PosLong(-1L) // compile-time error

PosLong.from(123L)
// Either[String, PosLong] = Right(PosLong(123L))

PosLong.from(-123L)
// Either[String, PosLong] = Left("Invalid value: -123L. It must be a positive Long")

PosLong.unsafeFrom(999L)
// PosLong = PosLong(999L)

PosLong.unsafeFrom(-999L)
// IllegalArgumentException(Invalid value: -999L. It must be a positive Long) is thrown

[core] Add numeric.NonPosLong (#21)

NonPosLong(-1L) // compiles
NonPosLong(0L)  // compiles
NonPosLong(1L)  // compile-time error

NonPosLong.from(-123L)
// Either[String, NonPosLong] = Right(NonPosLong(-123L))

NonPosLong.from(123L)
// Either[String, NonPosLong] = Left("Invalid value: 123L. It must be a non-positive Long")

NonPosLong.unsafeFrom(-999L)
// NonPosLong = NonPosLong(-999L)

NonPosLong.unsafeFrom(999L)
// IllegalArgumentException(Invalid value: 999L. It must be a non-positive Long) is thrown

Extract the essential properties of Refined and create RefinedBase (#30)

Refined to

  • RefinedBase
  • Refined

Refined should have only the apply method for the compile-time validation to create a value for the refined type.


Add InlinedRefined (#32)

It should look like this.

trait InlinedRefined[A] extends RefinedBase[A] {

  inline def inlinedInvalidReason(inline a: A): String

  inline def inlinedPredicate(inline a: A): Boolean

  inline def apply(inline a: A): Type
}

Add deriving to derive type-class from the base type (#40)

import cats.*

type MyType = MyType.Type
object MyType extends Refined[String] {

  override inline def invalidReason(a: String): String =
    "It has to be a non-empty String but got [" + a + "]"

  override inline def predicate(a: String): Boolean = a != ""

  given eqMyType: Eq[MyType] = deriving[Eq]

  given showMyType: Show[MyType] = deriving[Show]
}
import cats.syntax.all.*

MyType("blah") === MyType("blah")
// Boolean = true

MyType("blah").show
// String = blah

Add Coercible to type-safely cast refined type to the actual type (#42)

type MoreThan2CharString = MoreThan2CharsString.Type
object MoreThan2CharsString extends InlinedRefined[String] {

  override inline def invalidReason(a: String): String =
    "The String should have more than 2 chars but got " + a

  override def predicate(a: String): Boolean = a.length > 2

  override inline def inlinedInvalidReason(inline a: String): String =
    invalidReason(codeOf(a))

  override inline def inlinedPredicate(inline a: String): Boolean =
    ${ checkStringLength('a) }

  private def checkStringLength(strExpr: Expr[String])(using Quotes): Expr[Boolean] = {
    val str = strExpr.valueOrAbort
    if predicate(str) then Expr(true) else Expr(false)
  }
}
def foo[T, S](t: T)(using coercible: Coercible[T, S]): S =
  coercible(t)

val s: String = foo(MoreThan2CharsString(">>>>> aaa"))
println(s)
// String = >>>>> aaa

NOTE: For this ticket, the focus is solely on adding Coercible; subsequent integration with Refined to utilize Coercible will be addressed later.

The idea of Coercible is from scala-newtype's Coercible.


Add Newtype (#44)

It is solely to create a newtype just like an opaque type but with a few mandatory methods and type-classes.

type Something = Something.Type
object Something extends Newtype[String] {
  given eqMyType: Eq[Something]     = deriving[Eq]
  given showMyType: Show[Something] = deriving[Show]
}

Then it can be

val something = Something("blah")
something.value
// String = blah

something.show
// String = blah

Something("blah") === Something("blah")
// Boolean = true

Something("blah") =!= Something("blah")
// Boolean = false

Something("blah") === Something("lala")
// Boolean = false

Something("blah") =!= Something("lala")
// Boolean = true

Make RefinedBase extend NewtypeBase to support unwrapping (getting value) for Refined and InlinedRefined types with Coercible (#46)

type MyType = MyType.Type
object MyType extends InlinedRefined[String] {

  override inline def invalidReason(a: String): String =
    "It has to be a non-empty String but got [" + a + "]"

  override inline def predicate(a: String): Boolean = a != ""

  override inline def inlinedPredicate(inline a: String): Boolean = a != ""
}
type Something = Something.Type
object Something extends InlinedRefined[Int] {

  private def inlinedPredicate0(a: Expr[Int])(using Quotes): Expr[Boolea...
Read more