Skip to content

Commit

Permalink
Merge pull request #74 from ulanzetz/master
Browse files Browse the repository at this point in the history
Add tethys-refined module
  • Loading branch information
REDNBLACK authored Feb 8, 2021
2 parents 409e9ab + 2f87c4e commit b47bf64
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
.cache
.history
.lib
.bsp

# Intellij Idea
*.iml
Expand Down
13 changes: 12 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ lazy val testSettings = Seq(
lazy val tethys = project.in(file("."))
.settings(commonSettings)
.dependsOn(core, `macro-derivation`, `jackson-backend`)
.aggregate(core, `macro-derivation`, `jackson-backend`, json4s, circe, enumeratum)
.aggregate(core, `macro-derivation`, `jackson-backend`, json4s, circe, enumeratum, refined)

lazy val modules = file("modules")

Expand Down Expand Up @@ -131,6 +131,17 @@ lazy val enumeratum = project.in(modules / "enumeratum")
)
.dependsOn(core)

lazy val refined = project.in(modules / "refined")
.settings(commonSettings)
.settings(testSettings)
.settings(
name := "tethys-refined",
libraryDependencies ++= Seq(
"eu.timepit" %% "refined" % "0.9.20"
)
)
.dependsOn(core)

lazy val benchmarks = project.in(modules / "benchmarks")
.settings(commonSettings)
.settings(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package tethys.refined

import eu.timepit.refined.api.{RefType, Validate}
import tethys.readers.{FieldName, KeyReader, ReaderError}
import tethys.readers.tokens.TokenIterator
import tethys.{JsonReader, JsonWriter}

trait TethysRefinedInstances {
implicit final def RefinedJsonWriter[T: JsonWriter, P, F[_, _]: RefType]: JsonWriter[F[T, P]] =
JsonWriter[T].contramap(RefType[F].unwrap)

implicit final def RefinedJsonReader[T: JsonReader, P, F[_, _]: RefType](
implicit validate: Validate[T, P]
): JsonReader[F[T, P]] =
new JsonReader[F[T, P]] {
override def read(it: TokenIterator)(implicit fieldName: FieldName): F[T, P] =
fromEither(RefType[F].refine(JsonReader[T].read(it)))
}

implicit final def RefinedKeyReader[T, P, F[_, _]: RefType](
implicit reader: KeyReader[T],
validate: Validate[T, P]
): KeyReader[F[T, P]] = new KeyReader[F[T, P]] {
override def read(s: String)(implicit fieldName: FieldName): F[T, P] =
fromEither(RefType[F].refine(reader.read(s)))
}

private def fromEither[A](either: Either[String, A])(implicit fieldName: FieldName): A =
either match {
case Right(value) => value
case Left(err) => ReaderError.wrongJson(s"Refined error: $err")
}
}
3 changes: 3 additions & 0 deletions modules/refined/src/main/scala/tethys/refined/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package tethys

package object refined extends TethysRefinedInstances
142 changes: 142 additions & 0 deletions modules/refined/src/test/scala/tethys/refined/RefinedSupportTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package tethys.refined

import eu.timepit.refined._
import eu.timepit.refined.api.Refined
import eu.timepit.refined.collection.NonEmpty
import eu.timepit.refined.numeric._
import eu.timepit.refined.string.IPv4
import eu.timepit.refined.types.numeric._
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import tethys.commons.TokenNode.{value => token, _}
import tethys.readers.ReaderError
import tethys.writers.tokens.SimpleTokenWriter._

class RefinedSupportTest extends AnyFlatSpec with Matchers {
val posInt: PosInt = refineMV[Positive](5)
val nonNegInt: NonNegInt = refineMV[NonNegative](0)
val negInt: NegInt = refineMV[Negative](-5)

val posLong: PosLong = refineMV[Positive](5L)
val nonNegLong: NonNegLong = refineMV[NonNegative](0L)
val negLong: NegLong = refineMV[Negative](-5L)

val posDouble: PosDouble = refineMV[Positive](5.0)
val nonNegDouble: NonNegDouble = refineMV[NonNegative](0.0)
val negDouble: NegDouble = refineMV[Negative](-5.0)
val nonNaNDouble: NonNaNDouble = refineMV[NonNaN](5.0)

val posFloat: PosFloat = refineMV[Positive](5.0f)
val nonNegFloat: NonNegFloat = refineMV[NonNegative](0.0f)
val negFloat: NegFloat = refineMV[Negative](-5.0f)
val nonNaNFloat: NonNaNFloat = refineMV[NonNaN](5.0f)

val ipV4: String Refined IPv4 = refineMV[IPv4]("192.168.0.1")

val nel: List[String] Refined NonEmpty =
refineV[NonEmpty](List("a", "b")).getOrElse(throw new RuntimeException("Empty list"))

behavior of "RefinedJsonWriter"

it should "work with numerics" in {
posInt.asTokenList shouldBe token(5)
nonNegInt.asTokenList shouldBe token(0)
negInt.asTokenList shouldBe token(-5)

posLong.asTokenList shouldBe token(5L)
nonNegLong.asTokenList shouldBe token(0L)
negLong.asTokenList shouldBe token(-5L)

posDouble.asTokenList shouldBe token(5.0)
nonNegDouble.asTokenList shouldBe token(0.0)
negDouble.asTokenList shouldBe token(-5.0)
nonNaNDouble.asTokenList shouldBe token(5.0)

posFloat.asTokenList shouldBe token(5.0f)
nonNegFloat.asTokenList shouldBe token(0.0f)
negFloat.asTokenList shouldBe token(-5.0f)
nonNaNFloat.asTokenList shouldBe token(5.0f)
}

it should "work with strings" in {
ipV4.asTokenList shouldBe token("192.168.0.1")
}

it should "work with collections" in {
nel.asTokenList shouldBe arr("a", "b")
}

behavior of "RefinedJsonReader"

it should "work with numerics" in {
// Int
token(5).tokensAs[PosInt] shouldBe posInt
assertThrows[ReaderError](token(0).tokensAs[PosInt])

token(0).tokensAs[NonNegInt] shouldBe nonNegInt
assertThrows[ReaderError](token(-5).tokensAs[NonNegInt])

token(-5).tokensAs[NegInt] shouldBe negInt
assertThrows[ReaderError](token(5).tokensAs[NegInt])

// Long
token(5L).tokensAs[PosLong] shouldBe posLong
assertThrows[ReaderError](token(0L).tokensAs[PosLong])

token(0L).tokensAs[NonNegLong] shouldBe nonNegLong
assertThrows[ReaderError](token(-5L).tokensAs[NonNegLong])

token(-5L).tokensAs[NegLong] shouldBe negLong
assertThrows[ReaderError](token(5L).tokensAs[NegLong])

// Double
token(5.0).tokensAs[PosDouble] shouldBe posDouble
assertThrows[ReaderError](token(0.0).tokensAs[PosDouble])

token(0.0).tokensAs[NonNegDouble] shouldBe nonNegDouble
assertThrows[ReaderError](token(-5.0).tokensAs[NonNegDouble])

token(-5.0).tokensAs[NegDouble] shouldBe negDouble
assertThrows[ReaderError](token(5.0).tokensAs[NegDouble])

token(5.0).tokensAs[NonNaNDouble] shouldBe nonNaNDouble
assertThrows[ReaderError](token(Double.NaN).tokensAs[NonNaNDouble])

// Float
token(5.0f).tokensAs[PosFloat] shouldBe posFloat
assertThrows[ReaderError](token(0.0f).tokensAs[PosDouble])

token(0.0f).tokensAs[NonNegFloat] shouldBe nonNegFloat
assertThrows[ReaderError](token(-5.0f).tokensAs[NonNegFloat])

token(-5.0f).tokensAs[NegFloat] shouldBe negFloat
assertThrows[ReaderError](token(5.0f).tokensAs[NegDouble])

token(5.0f).tokensAs[NonNaNFloat] shouldBe nonNaNFloat
assertThrows[ReaderError](token(Float.NaN).tokensAs[NonNaNDouble])
}

it should "work with strings" in {
token("192.168.0.1").tokensAs[String Refined IPv4] shouldBe ipV4

assertThrows[ReaderError](token("aaa").tokensAs[String Refined IPv4])
}

it should "work with collections" in {
arr("a", "b").tokensAs[List[String] Refined NonEmpty] shouldBe nel

assertThrows[ReaderError](arr().tokensAs[List[String] Refined NonEmpty])
}

behavior of "RefinedKeyReader"

it should "work with refined strings" in {
type Limits = Map[String Refined IPv4, Int]

val limits: Limits = Map(refineMV[IPv4]("192.168.0.1") -> 1, refineMV[IPv4]("192.168.1.1") -> 2)

obj("192.168.0.1" -> 1, "192.168.1.1" -> 2).tokensAs[Limits] shouldBe limits

assertThrows[ReaderError](obj("192.168.0.1" -> 1, "192.168.256.1" -> 2).tokensAs[Limits])
}
}

0 comments on commit b47bf64

Please sign in to comment.