Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refined4s v0.1.0 #91

Merged
merged 1 commit into from
Dec 10, 2023
Merged

refined4s v0.1.0 #91

merged 1 commit into from
Dec 10, 2023

Conversation

kevin-lee
Copy link
Owner

refined4s v0.1.0

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[Boolean] = {
    import quotes.reflect.*
    a.asTerm match {
      case Inlined(_, _, Literal(IntConstant(num))) =>
        try {
          validate(num)
          Expr(true)
        } catch {
          case _: Throwable => Expr(false)
        }
      case _ =>
        report.error(
          "Something must be a Int literal.",
          a,
        )
        Expr(false)
    }

  }

  override inline def inlinedPredicate(inline a: Int): Boolean = ${ inlinedPredicate0('a) }

  override def invalidReason(a: Int): String = s"The number is a negative Int. [a: ${a.toString}"

  override def predicate(a: Int): Boolean =
    try {
      validate(a)
      true
    } catch {
      case _: Throwable => false
    }
}
def unwrap[A, B](a: A)(using coercible: Coercible[A, B]): B = coercible(a)

unwrap(MyType("abc"))
// String = abc

unwrap(Something(999))
// Int = 999

Add RefinedCtor[T, A] to provide a way to create a validated type T from an actual type A with validation (#49)

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

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

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

  given refinedCtor: RefinedCtor[Type, String] with {
    override def create(a: String): Either[String, MyType] = from(a)
  }
}
RefinedCtor[MyType, String].create("blah")
// Either[String, MyType] = Right("blah")

RefinedCtor[MyType, String].create("")
// Either[String, MyType] = Left("Invalid value: []. It has to be non-empty String but got \"\"")

NOTE: This ticket is only about adding RefinedCtor[T, A], so providing RefinedCtor for Refined and InlinedRefined will be done separately.


Add RefinedCtor type-class instance for RefinedBase (#51)


Add A.refinedTo[T] syntax to create Refined[A] and InlinedRefined[A] (#53)

type MyType = MyType.Type
@SuppressWarnings(Array("org.wartremover.warts.Equals"))
object MyType extends Refined[String] {

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

  override inline def predicate(a: String): Boolean = a != ""
}
import refined4s.syntax.*

"blah".refinedTo[MyType]
// Either[String, MyType#Type] = blah

"".refinedTo[MyType]
// Either[String, MyType#Type] = Invalid value: []. It has to be a non-empty String but got ""

Add T.coerce[A] syntax to easily get the value A from Refined[A]#Type and InlinedRefined[A]#Type (#55)

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 != ""
}
import refined4s.syntax.*

val myType = MyType("blah")
myType.coerce[String]
// String = blah

myType.coerce[Int]
// Compile-time error:
// no given instance of type refined4s.Coercible[MyType.Type, Int] was found
// for parameter coercible of method coerce in object syntax

Add refinedNewtype syntax to create Newtype containing Refined[A] or InlinedRefined[A] (#57)

type MyType = MyType.Type
@SuppressWarnings(Array("org.wartremover.warts.Equals"))
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 != ""
}

type NewMyType = NewMyType.Type
object NewMyType extends Newtype[MyType]
import refined4s.syntax.*

NewMyType(MyType("blah"))
// Either[String, NewMyType#Type] = Right(NewType#Type(MyType("blah")))

NewMyType(MyType(""))
// Either[String, NewMyType#Type] =
//   Left("Failed to create NewMyType: Invalid value: []. It has to be a non-empty String but got \"\"")

Add refined4s-cats module and add validateAs syntax to create Newtype containing Refined[A] or InlinedRefined[A] (#59)

type MyType = MyType.Type
@SuppressWarnings(Array("org.wartremover.warts.Equals"))
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]
}

type NewMyType = NewMyType.Type
object NewMyType extends Newtype[MyType]
import refined4s.cats.syntax.*

"blah".validateAs[NewMyType]
// EitherNec[String, NewType#Type] = Right(NewMyType(NewMyType("blah")))

validateAs("blah")[NewMyType]
// EitherNec[String, NewType#Type] = Right(NewMyType(NewMyType("blah")))

"".validateAs[NewMyType]
// EitherNec[String, NewType#Type] =
//   Left("Failed to create NewMyType: Invalid value: []. It has to be a non-empty String but got \"\"")

validateAs("")[NewMyType]
// EitherNec[String, NewType#Type] =
//   Left("Failed to create NewMyType: Invalid value: []. It has to be a non-empty String but got \"\"")

Add toValue syntax to get the value from Newtype containing Refined[A] or InlinedRefined[A] (#61)

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 != ""
}

type NewMyType = NewMyType.Type
object NewMyType extends Newtype[MyType]
import refined4s.syntax.*

val newMyType = NewMyType(MyType("blah"))
newMyType.toValue
// String = "blah"

[refined4s-cats] Add CatsEq, CatsShow and CatsEqShow to derive Eq and Show from the actual type's Eq and Show (#63)

type MyNewtype = MyNewtype.Type
object MyNewtype extends Newtype[String] with CatsEq[String]

type MyRefinedType = MyRefinedType.Type
object MyRefinedType extends Refined[String] with CatsEq[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 CatsEq[MyRefinedType]
type MyNewtype = MyNewtype.Type
object MyNewtype extends Newtype[String] with CatsShow[String]

type MyRefinedType = MyRefinedType.Type
object MyRefinedType extends Refined[String] with CatsShow[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 CatsShow[MyRefinedType]
type MyNewtype = MyNewtype.Type
object MyNewtype extends Newtype[String] with CatsEqShow[String]

type MyRefinedType = MyRefinedType.Type
object MyRefinedType extends Refined[String] with CatsEqShow[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 CatsEqShow[MyRefinedType]

Use deriving for numericOrdering: Ordering[Type] in Numeric[A] (#65)

trait Numeric[A: math.Ordering] extends Refined[A] {
  given numericOrdering: Ordering[Type]

  // ...
}

can be

trait Numeric[A: math.Ordering] extends Refined[A] {
  given numericOrdering: Ordering[Type] = deriving[Ordering]

  // ...
}

so Numbers don't have to be like this

type NegInt = NegInt.Type
object NegInt extends Numeric[Int] {
  override inline def invalidReason(a: Int): String = expectedMessage("a negative Int")

  override inline def predicate(a: Int): Boolean = a < 0

  override given numericOrdering: Ordering[NegInt] =
    (x, y) => scala.math.Numeric.IntIsIntegral.compare(x.value, y.value)
}

but it can be

type NegInt = NegInt.Type
object NegInt extends Numeric[Int] {
  override inline def invalidReason(a: Int): String = expectedMessage("a negative Int")

  override inline def predicate(a: Int): Boolean = a < 0
}

[core] Add NegShort, NonNegShort, PosShort and NonPosShort to numeric package (#67)


[core] Add NegByte, NonNegByte, PosByte and NonPosByte to numeric package (#70)


[core] Add NegFloat, NonNegFloat, PosFloat and NonPosFloat to numeric package (#72)


[core] Add NegDouble, NonNegDouble, PosDouble and NonPosDouble to numeric package (#76)


[core] Add NegBigInt, NonNegBigInt, PosBigInt and NonPosBigInt to numeric package (#79)

[core] Add NegBigDecimal, NonNegBigDecimal, PosBigDecimal and NonPosBigDecimal to numeric package (#83)

NOTE: Also InlinedNumeric was added to support compile-time validation for the refined BigInt and refined BigDecimal.

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


[core] Add Uri to network package (#85)

import refined4s.types.network.*

Uri("https://github.com/kevin-lee/refined4s")
// InlinedRefined[String]#Type = "https://github.com/kevin-lee/refined4s"

Uri("%^<>[]`{}")
// Compile-time error
import refined4s.types.network.*

val uri = Uri("https://github.com/kevin-lee/refined4s")
uri.value
// String = "https://github.com/kevin-lee/refined4s"

uri.toURI
// java.net.URI = URI("https://github.com/kevin-lee/refined4s")

[core] Add types package and move numeric, strings and network to it (#87)

import refined4s.types.numeric.*
import refined4s.types.strings.*
import refined4s.types.network.*

[core] Add all package to refined4s.types and make it contain all packages in the types package (#89)

So the following

import refined4s.types.all.*

is equivalent to

import refined4s.types.numeric.*
import refined4s.types.strings.*
import refined4s.types.network.*

@kevin-lee kevin-lee added this to the m1 milestone Dec 10, 2023
@kevin-lee kevin-lee self-assigned this Dec 10, 2023
Copy link

codecov bot commented Dec 10, 2023

Codecov Report

Merging #91 (346cfe4) into main (99f3569) will not change coverage.
The diff coverage is n/a.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##              main       #91   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           10        10           
  Lines           63        63           
  Branches         2         2           
=========================================
  Hits            63        63           

@kevin-lee kevin-lee merged commit e6ab70e into main Dec 10, 2023
12 checks passed
@kevin-lee kevin-lee deleted the prepare-to-release branch December 10, 2023 06:41
@kevin-lee kevin-lee modified the milestones: m1, m3 Dec 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant