Skip to content

Commit

Permalink
Merge pull request #2 from note/circe-integration2
Browse files Browse the repository at this point in the history
Circe integration
  • Loading branch information
note authored Sep 27, 2023
2 parents 3518aa2 + e82c239 commit 22a7a9a
Show file tree
Hide file tree
Showing 23 changed files with 184 additions and 21 deletions.
2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ danglingParentheses.preset = false
indentOperator.preset = spray
maxColumn = 120
newlines.topLevelStatementBlankLines = [
{ blanks { before = 1, after = 0, beforeEndMarker = 0 } }
{ blanks { before = 0, after = 0, beforeEndMarker = 0 } }
]
rewrite.redundantBraces.maxLines = 5
rewrite.rules = [RedundantBraces, RedundantParens, SortImports, SortModifiers, PreferCurlyFors]
Expand Down
12 changes: 11 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import Common._

lazy val root = (project in file("."))
lazy val miniRefined = (project in file("core"))
.commonSettings("mini-refined")
.settings(
libraryDependencies ++= Dependencies.testDeps
)

lazy val circeIntegration = (project in file("circe-integration"))
.commonSettings("mini-refined-circe")
.settings(
libraryDependencies ++= Dependencies.circe ++ Dependencies.testDeps
)
.dependsOn(miniRefined)

// do not publish the root project
publish / skip := true
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package pl.msitko.refined.circe

import io.circe.{Decoder, Encoder}
import pl.msitko.refined.Refined

// TODO: Find a way to encode those codecs generically instead of defining them for each supported type
// Things like the following are not getting picked up by implicits mechanism, i.e. they work only when given is called explicitly:
// given [T : Decoder, P <: Refined.ValidateExprFor[T]]: Encoder[T Refined P] = ???

given intEncoder[P <: Refined.ValidateExprFor[Int]](using Encoder[Int]): Encoder[Int Refined P] =
summon[Encoder[Int]].contramap(_.value)

inline given intDecoder[P <: Refined.ValidateExprFor[Int]](using Decoder[Int]): Decoder[Int Refined P] =
summon[Decoder[Int]].emap(v => Refined.refineV[P](v))

given stringEncoder[P <: Refined.ValidateExprFor[String]](using Encoder[String]): Encoder[String Refined P] =
summon[Encoder[String]].contramap(_.value)

inline given stringDecoder[P <: Refined.ValidateExprFor[String]](using Decoder[String]): Decoder[String Refined P] =
summon[Decoder[String]].emap(v => Refined.refineV[P](v))

given listEncoder[T, P <: Refined.ValidateExprFor[List[Any]]](using Encoder[List[T]]): Encoder[List[T] Refined P] =
summon[Encoder[List[T]]].contramap(_.value)

inline given listDecoder[T, P <: Refined.ValidateExprFor[List[Any]]](using
Decoder[List[T]]): Decoder[List[T] Refined P] =
summon[Decoder[List[T]]].emap(v => Refined.refineV[T, P](v))
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package pl.msitko.refined.circe

import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder}
import io.circe.syntax.*
import io.circe.{parser, Decoder, Encoder, Printer}
import pl.msitko.refined.auto.*
import pl.msitko.refined.compiletime.ValidateExprInt
import pl.msitko.refined.compiletime.ValidateExprInt.GreaterThan
import pl.msitko.refined.Refined

final case class Library(
name: String Refined StartsWith["lib"],
version: Int Refined GreaterThan[10],
dependencies: List[String] Refined Size[GreaterThan[1]])

class CodecsSpec extends munit.FunSuite:
given enc: Encoder[Library] = deriveEncoder[Library]
given dec: Decoder[Library] = deriveDecoder[Library]

test("should roundtrip for a manually defined Encoder and Decoder for type Library") {
val in = Library("libA", 23, List("depA", "depB"))
val encoded = in.asJson.printWith(Printer.spaces2)
val Right(decoded) = parser.parse(encoded).flatMap(_.as[Library]): @unchecked

assertEquals(decoded, in)
}

test("decoder should fail for incorrect Library.name") {
val in = """{
| "name" : "something",
| "version" : 11,
| "dependencies": ["depA", "depB"]
|}""".stripMargin

val Left(decodingError) = parser.parse(in).flatMap(_.as[Library]): @unchecked
assertEquals(
decodingError.getMessage,
"DecodingFailure at .name: Validation of refined type failed: something.startWith(lib)")
}

test("decoder should fail for incorrect Library.version") {
val in = """{
| "name" : "libA",
| "version" : 7,
| "dependencies": ["depA", "depB"]
|}""".stripMargin

val Left(decodingError) = parser.parse(in).flatMap(_.as[Library]): @unchecked
assertEquals(decodingError.getMessage, "DecodingFailure at .version: Validation of refined type failed: 7 > 10")
}

test("decoder should fail for incorrect Library.dependencies") {
val in = """{
| "name" : "libA",
| "version" : 11,
| "dependencies": ["depA"]
|}""".stripMargin

val Left(decodingError) = parser.parse(in).flatMap(_.as[Library]): @unchecked
assertEquals(
decodingError.getMessage,
"DecodingFailure at .dependencies: Validation of refined type failed: list size doesn't hold predicate: 1 > 1")
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,16 @@ object auto:

// T is covariant so `val a: Refined[Int, GreaterThan[10]] = 186` works as well as `val a: Refined[186, GreaterThan[10]] = 186`
// but not sure how important it's that the latter works
final class Refined[+T <: Refined.Base, P <: Refined.ValidateExprFor[T]] private (val value: T) extends AnyVal
final class Refined[+T, P <: Refined.ValidateExprFor[T]] private (val value: T) extends AnyVal {
override def toString: String = value.toString
}

//trait Refined[+Underlying, ValidateExpr]

object Refined:
type Base = Int | String | List[Any]
// type Base = Int | String | List[Any]

type ValidateExprFor[B <: Base] = B match
type ValidateExprFor[B] = B match
case Int => ValidateExprInt
case String => ValidateExprString
case List[Any] => ValidateExprList
Expand All @@ -113,5 +115,17 @@ object Refined:
implicit def unwrap[T <: String, P <: ValidateExprString](in: Refined[T, P]): T = in.value
implicit def unwrap[X, T <: List[X], P <: ValidateExprList](in: Refined[T, P]): List[X] = in.value

inline def refineV[P <: ValidateExprInt]: RT.ValidateInt[P] =
new RT.ValidateInt[P](RT.ValidateExprInt.fromCompiletime[P])
inline def refineV[P <: ValidateExprInt](v: Int): Either[String, Int Refined P] =
RT.ValidateExprInt.fromCompiletime[P].validate(v) match
case Some(err) => Left(s"Validation of refined type failed: $err")
case None => Right(Refined.unsafeApply[Int, P](v))

inline def refineV[P <: ValidateExprString](v: String): Either[String, String Refined P] =
RT.ValidateExprString.fromCompiletime[P].validate(v) match
case Some(err) => Left(s"Validation of refined type failed: $err")
case None => Right(Refined.unsafeApply[String, P](v))

inline def refineV[T, P <: ValidateExprList](v: List[T]): Either[String, List[T] Refined P] =
RT.ValidateExprList.fromCompiletime[P].validate(v) match
case Some(err) => Left(s"Validation of refined type failed: $err")
case None => Right(Refined.unsafeApply[T, P](v))
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import pl.msitko.refined.macros.ListMacros
import scala.compiletime.erasedValue

object ValidateList:

transparent inline def validate[E <: ValidateExprList](inline in: List[_]): String | Null =
inline erasedValue[E] match
case _: Size[t] =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,6 @@ package pl.msitko.refined.runtime
import compiletime.{constValue, erasedValue}
import pl.msitko.refined.compiletime as CT
import pl.msitko.refined.runtime.ValidateExprInt.{And, LowerThan}
import pl.msitko.refined.Refined

class ValidateInt[P <: CT.ValidateExprInt](rtExpr: ValidateExprInt) {

def apply(v: Int): Either[String, Int Refined P] =
rtExpr.validate(v) match
case Some(err) => Left(s"Validation of refined type failed: $err")
case None => Right(Refined.unsafeApply[Int, P](v))

}

sealed trait ValidateExprInt {
def validate(v: Int): Option[String]
Expand All @@ -24,7 +14,6 @@ object ValidateExprInt:
def validate(v: Int): Option[String] = a.validate(v).orElse(b.validate(v))

final case class Or(a: ValidateExprInt, b: ValidateExprInt) extends ValidateExprInt:

def validate(v: Int): Option[String] = a.validate(v) match
case Some(err) =>
b.validate(v) match
Expand All @@ -34,13 +23,11 @@ object ValidateExprInt:
None

final case class LowerThan(t: Int) extends ValidateExprInt:

def validate(v: Int): Option[String] =
if v < t then None
else Some(s"$v < $t")

final case class GreaterThan(t: Int) extends ValidateExprInt:

def validate(v: Int): Option[String] =
if v > t then None
else Some(s"$v > $t")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package pl.msitko.refined.runtime

import compiletime.{constValue, erasedValue}
import pl.msitko.refined.compiletime as CT
import pl.msitko.refined.runtime as RT

sealed trait ValidateExprList:
def validate(v: List[_]): Option[String]

object ValidateExprList:
final case class Size(sizeIntValidator: RT.ValidateExprInt) extends ValidateExprList:
def validate(v: List[_]): Option[String] =
sizeIntValidator.validate(v.size).map(err => s"list size doesn't hold predicate: $err")

inline def fromCompiletime[T <: CT.ValidateExprList]: ValidateExprList =
inline erasedValue[T] match
case _: CT.ValidateExprList.Size[t] => Size(RT.ValidateExprInt.fromCompiletime[t])
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package pl.msitko.refined.runtime

import compiletime.{constValue, erasedValue}
import pl.msitko.refined.compiletime as CT

sealed trait ValidateExprString:
def validate(v: String): Option[String]

object ValidateExprString:

final case class And(a: ValidateExprString, b: ValidateExprString) extends ValidateExprString:
def validate(v: String): Option[String] = a.validate(v).orElse(b.validate(v))

final case class Or(a: ValidateExprString, b: ValidateExprString) extends ValidateExprString:
def validate(v: String): Option[String] = a.validate(v) match
case Some(err) =>
b.validate(v) match
case Some(err2) => Some(s"($err Or $err2)")
case None => None
case None =>
None

final case class StartsWith(t: String) extends ValidateExprString:
def validate(v: String): Option[String] =
if v.startsWith(t) then None
else Some(s"$v.startWith($t)")

final case class EndsWith(t: String) extends ValidateExprString:
def validate(v: String): Option[String] =
if v.endsWith(t) then None
else Some(s"$v.endsWith($t)")

inline def fromCompiletime[T <: CT.ValidateExprString]: ValidateExprString =
inline erasedValue[T] match
case _: CT.ValidateExprString.And[a, b] => And(fromCompiletime[a], fromCompiletime[b])
case _: CT.ValidateExprString.Or[a, b] => Or(fromCompiletime[a], fromCompiletime[b])
case _: CT.ValidateExprString.StartsWith[t] => StartsWith(constValue[t])
case _: CT.ValidateExprString.EndsWith[t] => EndsWith(constValue[t])
8 changes: 8 additions & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import sbt._

object Dependencies {
val circeVersion = "0.14.6"

lazy val circe = Seq(
"io.circe" %% "circe-core" % circeVersion,
"io.circe" %% "circe-generic" % circeVersion % Test,
"io.circe" %% "circe-parser" % circeVersion % Test
)

lazy val munit = "org.scalameta" %% "munit" % "0.7.29" % Test

lazy val testDeps = Seq(munit)
Expand Down

0 comments on commit 22a7a9a

Please sign in to comment.