Skip to content

Commit

Permalink
support zio-test GEN and jsoniter
Browse files Browse the repository at this point in the history
  • Loading branch information
kitlangton committed Feb 28, 2024
1 parent 787c4ed commit f554e6f
Show file tree
Hide file tree
Showing 15 changed files with 263 additions and 95 deletions.
78 changes: 50 additions & 28 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ inThisBuild(

Global / onChangedBuildSource := ReloadOnSourceChanges

val zioVersion = "2.0.21"
val tapirVersion = "1.9.10"
val zioVersion = "2.0.21"

val sharedSettings = Seq(
scalacOptions ++= Seq(
"-deprecation",
// "-explain",
"-Xcheck-macros"
// "-Ycheck:all"
),
Expand All @@ -34,21 +34,23 @@ lazy val root = (project in file("."))
name := "neotype"
)
.aggregate(
core.js,
core.jvm,
circe.js,
circe.jvm,
core.js,
core.jvm,
examples,
tapir.js,
tapir.jvm,
zio.js,
zio.jvm,
zioConfig,
zioJson.js,
zioJson.jvm,
zioConfig,
zioQuill,
zioSchema.js,
zioSchema.jvm,
examples,
tapir.js,
tapir.jvm
zioTest.js,
zioTest.jvm
)

lazy val core = (crossProject(JSPlatform, JVMPlatform) in file("modules/core"))
Expand All @@ -57,38 +59,38 @@ lazy val core = (crossProject(JSPlatform, JVMPlatform) in file("modules/core"))
sharedSettings
)

lazy val zioJson = (crossProject(JSPlatform, JVMPlatform) in file("modules/neotype-zio-json"))
lazy val circe = (crossProject(JSPlatform, JVMPlatform) in file("modules/neotype-circe"))
.settings(
name := "neotype-zio-json",
name := "neotype-circe",
sharedSettings,
libraryDependencies ++= Seq(
"dev.zio" %%% "zio-json" % "0.6.2"
"io.circe" %%% "circe-core" % "0.14.6",
"io.circe" %%% "circe-parser" % "0.14.6"
)
)
.dependsOn(core)

lazy val circe = (crossProject(JSPlatform, JVMPlatform) in file("modules/neotype-circe"))
lazy val jsoniter = (crossProject(JSPlatform, JVMPlatform) in file("modules/neotype-jsoniter"))
.settings(
name := "neotype-circe",
name := "neotype-jsoniter",
sharedSettings,
libraryDependencies ++= Seq(
"io.circe" %%% "circe-core" % "0.14.6",
"io.circe" %%% "circe-parser" % "0.14.6"
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.28.2",
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.28.2"
)
)
.dependsOn(core)

lazy val zioQuill = (project in file("modules/neotype-zio-quill"))
lazy val tapir = (crossProject(JSPlatform, JVMPlatform) in file("modules/neotype-tapir"))
.settings(
name := "neotype-zio-quill",
name := "neotype-tapir",
sharedSettings,
libraryDependencies ++= Seq(
"io.getquill" %% "quill-jdbc-zio" % "4.8.1",
"org.postgresql" % "postgresql" % "42.5.4" % Test,
"com.h2database" % "h2" % "2.1.214" % Test
"com.softwaremill.sttp.tapir" %%% "tapir-core" % tapirVersion,
"com.softwaremill.sttp.tapir" %%% "tapir-json-pickler" % tapirVersion
)
)
.dependsOn(core.jvm)
.dependsOn(core)

lazy val zio = (crossProject(JSPlatform, JVMPlatform) in file("modules/neotype-zio"))
.settings(
Expand Down Expand Up @@ -117,21 +119,41 @@ lazy val zioSchema = (crossProject(JSPlatform, JVMPlatform) in file("modules/neo
sharedSettings,
libraryDependencies ++= Seq(
"dev.zio" %%% "zio-schema" % "1.0.1",
"dev.zio" %%% "zio-json" % "0.6.2" % Test,
"dev.zio" %%% "zio-json" % "0.6.2" % Test,
"dev.zio" %%% "zio-schema-json" % "1.0.1" % Test
)
)
.dependsOn(core)

val tapirVersion = "1.9.10"
lazy val tapir = (crossProject(JSPlatform, JVMPlatform) in file("modules/neotype-tapir"))
lazy val zioJson = (crossProject(JSPlatform, JVMPlatform) in file("modules/neotype-zio-json"))
.settings(
name := "neotype-tapir",
name := "neotype-zio-json",
sharedSettings,
libraryDependencies ++= Seq(
// TODO Does this make sense?
"com.softwaremill.sttp.tapir" %%% "tapir-core" % tapirVersion,
"com.softwaremill.sttp.tapir" %%% "tapir-json-pickler" % tapirVersion
"dev.zio" %%% "zio-json" % "0.6.2"
)
)
.dependsOn(core)

lazy val zioQuill = (project in file("modules/neotype-zio-quill"))
.settings(
name := "neotype-zio-quill",
sharedSettings,
libraryDependencies ++= Seq(
"io.getquill" %% "quill-jdbc-zio" % "4.8.1",
"org.postgresql" % "postgresql" % "42.5.4" % Test,
"com.h2database" % "h2" % "2.1.214" % Test
)
)
.dependsOn(core.jvm)

lazy val zioTest = (crossProject(JSPlatform, JVMPlatform) in file("modules/neotype-zio-test"))
.settings(
name := "neotype-zio-test",
sharedSettings,
libraryDependencies ++= Seq(
"dev.zio" %%% "zio-test" % zioVersion,
"dev.zio" %%% "zio-test-magnolia" % zioVersion
)
)
.dependsOn(core)
Expand Down
18 changes: 13 additions & 5 deletions examples/src/main/scala/neotype/examples/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@ package neotype.examples
import neotype.*

object Main extends App:
Email("kit@gmail.com") // OK
FourSeasons("Spring") // OK
NonEmptyString("Good") // OK
FiveElements("REALLY LONG ELEMENT") // OK
PositiveIntList(List(5, 10))
// These will all compile.
Email("kit@gmail.com") // OK
FourSeasons("Spring") // OK
FiveElements("Fire") // OK
NonEmptyString("Good") // OK
PositiveIntList(List(5, 10)) // OK

// Uncomment out the following lines one at a time to see some fun compile errors.
// Email("kitgmail.com") // BAD
// FourSeasons("Splinter") // BAD
// FiveElements("Cheese") // BAD
// NonEmptyString("") // BAD
// PositiveIntList(List(5, -5)) // BAD
1 change: 1 addition & 0 deletions examples/src/main/scala/neotype/examples/Newtypes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ given NonEmptyString: Newtype[String] with

type Email = Email.Type
given Email: Newtype[String] with

inline def validate(value: String): Boolean =
value.contains("@") && value.contains(".")

Expand Down
8 changes: 5 additions & 3 deletions modules/core/shared/src/main/scala/neotype/Calc.scala
Original file line number Diff line number Diff line change
Expand Up @@ -423,9 +423,10 @@ enum CalcPattern[A]:

def matches(value: Any)(using Map[String, Any], Quotes): Boolean =
this match
case Value(value) => value.result == value
case Variable(_) => true
case Wildcard() => true
case Value(patternValue) =>
patternValue.result == value
case Variable(_) => true
case Wildcard() => true
case Alternative(patterns) =>
patterns.exists(_.matches(value))

Expand All @@ -437,6 +438,7 @@ object CalcPattern:
case r.Bind(name, r.Wildcard()) => CalcPattern.Variable(name)
case Seal(Calc(value)) => CalcPattern.Value(value)
case r.Alternatives(patterns) => CalcPattern.Alternative(patterns.map(parse))
case other => r.report.errorAndAbort(s"CalcPattern parse failed to parse: ${other}")

object Unseal:
def unapply(expr: Expr[?])(using Quotes): Option[quotes.reflect.Term] =
Expand Down
3 changes: 1 addition & 2 deletions modules/core/shared/src/main/scala/neotype/Macros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ private[neotype] object Macros:
case Calc[A](calc) =>
scala.util.Try(calc.result(using Map.empty)) match
case Failure(exception) =>
// report.errorAndAbort(s"Failed to execute parsed validation: $exception")
report.errorAndAbort(ErrorMessages.failedToParseValidateMethod(a, nt, treeSource, isBodyInline))
case Success(true) =>
a.asExprOf[T]
Expand All @@ -67,7 +66,7 @@ private[neotype] object Macros:
case _ =>
report.errorAndAbort(ErrorMessages.failedToParseValidateMethod(a, nt, treeSource, isBodyInline))

def applyAllImpl[A: Type, T: Type, NT <: Newtype[A] { type Type = T }: Type](
def applyAllImpl[A: Type, T: Type, NT <: ValidatedWrapper[A] { type Type = T }: Type](
as: Expr[Seq[A]],
self: Expr[NT]
)(using Quotes): Expr[List[T]] =
Expand Down
37 changes: 9 additions & 28 deletions modules/core/shared/src/main/scala/neotype/package.scala
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
package neotype

import StringFormatting.*

import javax.swing.text.Position
import scala.compiletime.summonInline
import scala.deriving.Mirror
import scala.quoted.*
import scala.util.{Failure, Success}

trait Wrapper[A]:
type Type

trait ValidatedWrapper[A] extends Wrapper[A]:
self =>

def validate(input: A): Boolean

def failureMessage: String = "Validation Failed"

inline def apply(inline input: A): Type =
${ Macros.applyImpl[A, Type, self.type]('input, '{ INPUT => validate(INPUT) }, 'failureMessage) }

inline def applyAll(inline values: A*): List[Type] =
${ Macros.applyAllImpl[A, Type, self.type]('values, 'self) }

trait ValidateEvidence
inline given ValidateEvidence = new ValidateEvidence {}
extension (using ValidateEvidence)(inline bool: Boolean) //
Expand All @@ -36,26 +38,15 @@ abstract class Newtype[A] extends ValidatedWrapper[A]:
self =>
opaque type Type = A

inline def apply(inline input: A): Type =
${ Macros.applyImpl[A, Type, self.type]('input, '{ INPUT => validate(INPUT) }, 'failureMessage) }

inline def applyAll(inline values: A*): List[Type] =
${ Macros.applyAllImpl[A, Type, self.type]('values, 'self) }

def make(input: A): Either[String, Type] =
if validate(input) then Right(input)
else Left(failureMessage)

extension (inline input: Type) //
inline def unwrap: A = input

inline def unsafeWrap(inline input: A): Type = input
inline def unsafeWrapF[F[_]](inline input: F[A]): F[Type] = input
inline def unsafe(inline input: A): Type =
make:
input
.getOrElse:
throw IllegalArgumentException:
failureMessage

object Newtype:
type WithType[A, B] = Newtype[A] { type Type = B }
Expand All @@ -70,23 +61,18 @@ object Newtype:

inline def applyF[F[_]](inline input: F[A]): F[Type] = input

inline def unsafeWrapF[F[_]](inline input: F[A]): F[Type] = input

object Simple:
type WithType[A, B] = Newtype.Simple[A] { type Type = B }

abstract class Subtype[A] extends ValidatedWrapper[A]:
self =>
opaque type Type <: A = A

inline def apply(inline input: A): Type =
${ Macros.applyImpl[A, Type, self.type]('input, '{ validate(_) }, 'failureMessage) }

def make(input: A): Either[String, Type] =
if validate(input) then Right(input)
else Left(failureMessage)

inline def cast(inline input: Type): A = input
// inline def cast(inline input: Type): A = input
inline def castF[F[_]](inline input: F[Type]): F[A] = input

inline def unsafeWrap(inline input: A): Type = input
Expand All @@ -111,11 +97,6 @@ object Subtype:
inline def unwrap: A = input

inline def applyF[F[_]](inline input: F[A]): F[Type] = input
inline def cast(inline input: A): Type = input
inline def castF[F[_]](inline input: F[A]): F[Type] = input

inline def unsafeWrap(inline input: A): Type = input
inline def unsafeWrapF[F[_]](inline input: F[A]): F[Type] = input

object Simple:
type WithType[A, B <: A] = Subtype.Simple[A] { type Type = B }
27 changes: 5 additions & 22 deletions modules/core/shared/src/test/scala/neotype/NewtypeSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -87,27 +87,10 @@ object NewtypeSpec extends ZIOSpecDefault:
assertTrue(res.map(PositiveIntNewtype.unwrap(_)) == Left("Validation Failed"))
}
),
suite("unsafe")(
test("success"){
val res =
PositiveIntNewtype
.unsafe:
1
.unwrap
assertTrue:
res == 1
},
test("failure")(
try
PositiveIntNewtype
.unsafe:
-1
.unwrap
assertNever:
"Should blow up on negative value"
catch
case _ =>
assertCompletes
),
suite("unsafeWrap")(
test("success") {
val res = PositiveIntNewtype.unsafeWrap(1).unwrap
assertTrue(res == 1)
}
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ given VariousStringNewtype: Newtype[String] with
inline def validate(string: String): Boolean =
string.startsWith("a") && string.endsWith("z") &&
string.length > 0 && string.contains("b") &&
string.isUUID && string.isURL
string.isUUID && string.isURL && string.isEmail

type IsUUID = IsUUID.Type
given IsUUID: Newtype[String] with
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package neotype.jsoniter

import com.github.plokhotnyuk.jsoniter_scala.core.{JsonReader, JsonValueCodec, JsonWriter}
import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker
import neotype.*

// Newtype
inline given [A, B](using newType: Newtype.WithType[A, B]): JsonValueCodec[B] = new JsonValueCodec[B]:
private val codec = JsonCodecMaker.make[A]

override def decodeValue(in: JsonReader, default: B): B =
val decoded = codec.decodeValue(in, newType.unwrap(default))
newType.make(decoded) match
case Left(value) => in.decodeError(s"Failed to decode $value")
case Right(value) => value

override def encodeValue(x: B, out: JsonWriter): Unit =
codec.encodeValue(newType.unwrap(x), out)

override def nullValue: B = null.asInstanceOf[B]

// Newtype.Simple
inline given [A, B](using newType: Newtype.Simple.WithType[A, B]): JsonValueCodec[B] =
newType.applyF(JsonCodecMaker.make[A])

// Subtype
inline given [A, B <: A](using subType: Subtype.WithType[A, B]): JsonValueCodec[B] =
new JsonValueCodec[B]:
private val codec = JsonCodecMaker.make[A]

override def decodeValue(in: JsonReader, default: B): B =
val decoded = codec.decodeValue(in, default)
subType.make(decoded) match
case Left(value) => in.decodeError(s"Failed to decode $value")
case Right(value) => value

override def encodeValue(x: B, out: JsonWriter): Unit =
codec.encodeValue(x, out)

override def nullValue: B = null.asInstanceOf[B]

// Subtype.Simple
inline given [A, B <: A](using subType: Subtype.Simple.WithType[A, B]): JsonValueCodec[B] =
subType.applyF(JsonCodecMaker.make[A])
Loading

0 comments on commit f554e6f

Please sign in to comment.