From 9b2eef811f43db2248cdec5a3503b64687a3bdb4 Mon Sep 17 00:00:00 2001 From: Valentin Kasas Date: Mon, 11 Feb 2019 18:37:02 +0100 Subject: [PATCH] Play JSON codecs (#37) * First (almost) working draft for an interpreter to play Json Reads * Add naive interperter for `play.api.libs.json.Writes` * Add `hyloNT` and use it to make Reads/Writes interpreters symmetric * Handle errors slightly more correctly * Add tests * Remove unused `CataInterpreter` and `HyloInterpreter` * Moved tests according to changes introduced by #39 * Fix and refactor after rebasing on `prototyping` * Make it easier to create interpreters * Move all recursion-related code to a separate file/package * Rename interpreters --- build.sbt | 12 +- modules/core/src/main/scala/Json.scala | 15 +- .../core/src/main/scala/SchemaModule.scala | 91 ++++++--- modules/core/src/main/scala/recursion.scala | 48 +++++ modules/core/src/main/scala/schemas.scala | 2 +- .../src/main/scala/GenericAlgebra.scala | 4 +- .../generic/src/main/scala/ShowModule.scala | 3 +- .../src/main/scala/PlayJsonModule.scala | 174 ++++++++++++++++++ .../scalacheck/src/main/scala/GenModule.scala | 13 +- .../src/main/scala/GenModuleExamples.scala | 10 +- .../src/main/scala/GenericGenModule.scala | 10 +- modules/tests/src/main/scala/Main.scala | 3 +- .../src/main/scala/PlayJsonExamples.scala | 82 +++++++++ .../tests/src/main/scala/ShowExamples.scala | 12 +- 14 files changed, 421 insertions(+), 58 deletions(-) create mode 100644 modules/core/src/main/scala/recursion.scala create mode 100644 modules/play-json/src/main/scala/PlayJsonModule.scala create mode 100644 modules/tests/src/main/scala/PlayJsonExamples.scala diff --git a/build.sbt b/build.sbt index 88e916c..83c1d76 100644 --- a/build.sbt +++ b/build.sbt @@ -47,6 +47,16 @@ lazy val scalacheck = project ) .dependsOn(core) +lazy val playJson = project + .in(file("modules/play-json")) + .settings( + name := "scalaz-schema-play-json", + libraryDependencies ++= Seq( + "com.typesafe.play" %% "play-json" % "2.6.10" + ) + ) + .dependsOn(core) + lazy val tests = project .in(file("modules/tests")) .settings( @@ -57,4 +67,4 @@ lazy val tests = project "org.scalaz" %% "testz-runner" % testzVersion ) ) - .dependsOn(core, scalacheck, generic) + .dependsOn(core, scalacheck, generic, playJson) diff --git a/modules/core/src/main/scala/Json.scala b/modules/core/src/main/scala/Json.scala index 54dc2c5..fdcea2e 100644 --- a/modules/core/src/main/scala/Json.scala +++ b/modules/core/src/main/scala/Json.scala @@ -13,14 +13,13 @@ object Json { trait JsonModule[R <: Realisation] extends SchemaModule[R] { import Json._ - import SchemaF._ - implicit final def algebra( + implicit final def encoderInterpreter( implicit primNT: R.Prim ~> Encoder, fieldLabel: R.ProductTermId <~< String, branchLabel: R.SumTermId <~< String - ): HAlgebra[RSchema, Encoder] = - new (RSchema[Encoder, ?] ~> Encoder) { + ): RInterpreter[Encoder] = + Interpreter.cata[RSchema, Encoder](new (RSchema[Encoder, ?] ~> Encoder) { val encloseInBraces = (s: String) => s"{$s}" def makeField(name: String) = (s: String) => s""""$name":$s""" @@ -31,16 +30,16 @@ trait JsonModule[R <: Realisation] extends SchemaModule[R] { case PrimSchema(prim) => primNT(prim) case :*:(left, right) => (a => left(a._1) + "," + right(a._2)) case :+:(left, right) => (a => a.fold(left, right)) - case i: RIso[Encoder, _, A] => + case i: IsoSchema[R.Prim, R.SumTermId, R.ProductTermId, Encoder, _, A] => i.base.compose(i.iso.reverseGet) - case r: RRecord[Encoder, _, A] => + case r: Record[R.Prim, R.SumTermId, R.ProductTermId, Encoder, A, _] => encloseInBraces.compose(r.fields).compose(r.iso.reverseGet) case SeqSchema(element) => (a => a.map(element).mkString("[", ",", "]")) case ProductTerm(id, base) => makeField(fieldLabel(id)).compose(base) - case u: RUnion[Encoder, _, A] => + case u: Union[R.Prim, R.SumTermId, R.ProductTermId, Encoder, A, _] => encloseInBraces.compose(u.choices).compose(u.iso.reverseGet) case SumTerm(id, base) => makeField(branchLabel(id)).compose(base) case One() => (_ => "null") } - } + }) } diff --git a/modules/core/src/main/scala/SchemaModule.scala b/modules/core/src/main/scala/SchemaModule.scala index 421cc13..d460523 100644 --- a/modules/core/src/main/scala/SchemaModule.scala +++ b/modules/core/src/main/scala/SchemaModule.scala @@ -2,9 +2,9 @@ package scalaz package schema -import monocle.Iso +import recursion._ -final case class Fix[F[_[_], _], A](unFix: F[Fix[F, ?], A]) +import monocle.Iso trait Realisation { type Prim[A] @@ -133,8 +133,62 @@ final case class IsoSchema[Prim[_], SumTermId, ProductTermId, F[_], A0, A]( IsoSchema(nt(base), iso) } +/** + * An interpreter able to derive a `F[A]` from a schema for `A` (for any `A`). + * Such interpreters will usually be implemented using a recursion scheme like + * 'cataNT`or hyloNT`. + */ +trait Interpreter[F[_], G[_]] { self => + + /** + * A natural transformation that will transform a schema for any type `A` + * into an `F[A]`. + */ + def interpret: F ~> G + + def compose[H[_]](nt: H ~> F) = self match { + case i: ComposedInterpreter[h, G, F] => ComposedInterpreter(i.underlying, i.nt.compose(nt)) + case x => ComposedInterpreter(x, nt) + } +} + +final case class ComposedInterpreter[F[_], G[_], H[_]](underlying: Interpreter[F, G], nt: H ~> F) + extends Interpreter[H, G] { + final override val interpret = underlying.interpret.compose(nt) +} + +class CataInterpreter[S[_[_], _], F[_]]( + algebra: HAlgebra[S, F] +)(implicit ev: HFunctor[S]) + extends Interpreter[Fix[S, ?], F] { + final override val interpret = cataNT(algebra) +} + +class HyloInterpreter[S[_[_], _], F[_], G[_]]( + coalgebra: HCoalgebra[S, G], + algebra: HAlgebra[S, F] +)(implicit ev: HFunctor[S]) + extends Interpreter[G, F] { + final override val interpret = hyloNT(coalgebra, algebra) +} + object SchemaF { + implicit def schemaHFunctor[Prim[_], SumTermId, ProductTermId] = + new HFunctor[SchemaF[Prim, SumTermId, ProductTermId, ?[_], ?]] { + + def hmap[F[_], G[_]](nt: F ~> G) = + new (SchemaF[Prim, SumTermId, ProductTermId, F, ?] ~> SchemaF[ + Prim, + SumTermId, + ProductTermId, + G, + ? + ]) { + def apply[A](fa: SchemaF[Prim, SumTermId, ProductTermId, F, A]) = fa.hmap(nt) + } + } + type FSchema[Prim[_], SumTermId, ProductTermId, A] = Fix[SchemaF[Prim, SumTermId, ProductTermId, ?[_], ?], A] @@ -185,24 +239,6 @@ object SchemaF { def toSchema = Fix(new :*:(l.toSchema, r.toSchema)) } - - // Schema syntax - - /////////////////////// - // Schema operations - /////////////////////// - - type HAlgebra[F[_[_], _], G[_]] = F[G, ?] ~> G - - def cataNT[Prim[_], SumTermId, ProductTermId, F[_]]( - alg: HAlgebra[SchemaF[Prim, SumTermId, ProductTermId, ?[_], ?], F] - ): (FSchema[Prim, SumTermId, ProductTermId, ?] ~> F) = - new (FSchema[Prim, SumTermId, ProductTermId, ?] ~> F) { self => - - def apply[A](f: FSchema[Prim, SumTermId, ProductTermId, A]): F[A] = - alg.apply[A](f.unFix.hmap[F](self)) - } - } trait SchemaModule[R <: Realisation] { @@ -211,6 +247,8 @@ trait SchemaModule[R <: Realisation] { import SchemaF._ + type RInterpreter[F[_]] = Interpreter[Schema, F] + type RSchema[F[_], A] = SchemaF[R.Prim, R.SumTermId, R.ProductTermId, F, A] type Schema[A] = FSchema[R.Prim, R.SumTermId, R.ProductTermId, A] @@ -229,6 +267,17 @@ trait SchemaModule[R <: Realisation] { type RSeq[F[_], A] = SeqSchema[F, A, R.Prim, R.SumTermId, R.ProductTermId] type RIso[F[_], A, B] = IsoSchema[R.Prim, R.SumTermId, R.ProductTermId, F, A, B] + object Interpreter { + + def cata[S[_[_], _], F[_]](alg: HAlgebra[S, F])(implicit ev: HFunctor[S]) = + new CataInterpreter[S, F](alg) + + def hylo[S[_[_], _], F[_], G[_]](coalg: HCoalgebra[S, G], alg: HAlgebra[S, F])( + implicit ev: HFunctor[S] + ) = new HyloInterpreter(coalg, alg) + + } + //////////////// // Public API //////////////// @@ -243,7 +292,7 @@ trait SchemaModule[R <: Realisation] { def -+>: (id: R.SumTermId): LabelledSum[A] = LabelledSum1(id, schema) - def to[F[_]](implicit algebra: HAlgebra[RSchema, F]): F[A] = cataNT(algebra)(schema) + def to[F[_]](implicit interpreter: RInterpreter[F]): F[A] = interpreter.interpret(schema) def imap[B](_iso: Iso[A, B]): Schema[B] = schema.unFix match { case IsoSchema(base, iso) => Fix(IsoSchema(base, iso.composeIso(_iso))) diff --git a/modules/core/src/main/scala/recursion.scala b/modules/core/src/main/scala/recursion.scala new file mode 100644 index 0000000..c513628 --- /dev/null +++ b/modules/core/src/main/scala/recursion.scala @@ -0,0 +1,48 @@ +package scalaz + +package schema + +package recursion { + + trait HFunctor[H[_[_], _]] { + def hmap[F[_], G[_]](nt: F ~> G): H[F, ?] ~> H[G, ?] + } + + final case class Fix[F[_[_], _], A](unFix: F[Fix[F, ?], A]) + + final case class HEnvT[E, F[_[_], _], G[_], I](ask: E, fa: F[G, I]) + + object HEnvT { + + implicit def hfunctor[E, F[_[_], _]](implicit F: HFunctor[F]): HFunctor[HEnvT[E, F, ?[_], ?]] = + new HFunctor[HEnvT[E, F, ?[_], ?]] { + + def hmap[M[_], N[_]](nt: M ~> N) = new (HEnvT[E, F, M, ?] ~> HEnvT[E, F, N, ?]) { + def apply[I](fm: HEnvT[E, F, M, I]) = HEnvT(fm.ask, F.hmap(nt)(fm.fa)) + } + } + } +} + +package object recursion { + type HAlgebra[F[_[_], _], G[_]] = F[G, ?] ~> G + type HCoalgebra[F[_[_], _], G[_]] = G ~> F[G, ?] + + def cataNT[S[_[_], _], F[_]]( + alg: HAlgebra[S, F] + )(implicit S: HFunctor[S]): (Fix[S, ?] ~> F) = + new (Fix[S, ?] ~> F) { self => + + def apply[A](f: Fix[S, A]): F[A] = + alg.apply[A](S.hmap(self)(f.unFix)) + } + + def hyloNT[S[_[_], _], F[_], G[_]](coalgebra: HCoalgebra[S, F], algebra: HAlgebra[S, G])( + implicit S: HFunctor[S] + ): F ~> G = new (F ~> G) { self => + + def apply[A](fa: F[A]): G[A] = + algebra(S.hmap(self)(coalgebra(fa))) + } + +} diff --git a/modules/core/src/main/scala/schemas.scala b/modules/core/src/main/scala/schemas.scala index 9e4f89d..4f5066c 100644 --- a/modules/core/src/main/scala/schemas.scala +++ b/modules/core/src/main/scala/schemas.scala @@ -30,5 +30,5 @@ object JsonSchema extends Realisation { final case object JsonString extends JsonPrim[String] final case object JsonNumber extends JsonPrim[BigDecimal] final case object JsonBool extends JsonPrim[Boolean] - final case object JsonNull extends JsonPrim[Null] + final case object JsonNull extends JsonPrim[Unit] } diff --git a/modules/generic/src/main/scala/GenericAlgebra.scala b/modules/generic/src/main/scala/GenericAlgebra.scala index 83a6735..a200489 100644 --- a/modules/generic/src/main/scala/GenericAlgebra.scala +++ b/modules/generic/src/main/scala/GenericAlgebra.scala @@ -4,9 +4,9 @@ package schema package generic -trait GenericSchemaModule[R <: Realisation] extends SchemaModule[R] { +import recursion._ - import SchemaF._ +trait GenericSchemaModule[R <: Realisation] extends SchemaModule[R] { def covariantTargetFunctor[H[_]]( primNT: R.Prim ~> H, diff --git a/modules/generic/src/main/scala/ShowModule.scala b/modules/generic/src/main/scala/ShowModule.scala index 1a91e0c..2aa4332 100644 --- a/modules/generic/src/main/scala/ShowModule.scala +++ b/modules/generic/src/main/scala/ShowModule.scala @@ -4,8 +4,9 @@ package schema package generic +import recursion._ + trait ShowModule[R <: Realisation] extends GenericSchemaModule[R] { - import SchemaF._ implicit val showDecidableInstance: Decidable[Show] = new Decidable[Show] { override def choose2[Z, A1, A2](a1: => Show[A1], a2: => Show[A2])(f: Z => A1 \/ A2): Show[Z] = diff --git a/modules/play-json/src/main/scala/PlayJsonModule.scala b/modules/play-json/src/main/scala/PlayJsonModule.scala new file mode 100644 index 0000000..8aedf12 --- /dev/null +++ b/modules/play-json/src/main/scala/PlayJsonModule.scala @@ -0,0 +1,174 @@ +package scalaz + +package schema + +package play.json + +import recursion._ + +import Scalaz._ +import Liskov._ +import _root_.play.api.libs.json._ +import _root_.play.api.libs.functional.syntax._ + +trait PlayJsonModule[R <: Realisation] extends SchemaModule[R] { + + import SchemaF._ + + type LabelledSchema[A] = (Boolean, FSchema[R.Prim, R.SumTermId, R.ProductTermId, A]) + + private def ascribeWith( + label: Boolean + ): Schema ~> LabelledSchema = + new (Schema ~> LabelledSchema) { + + def apply[A](fschema: Schema[A]): LabelledSchema[A] = + (label, fschema) + } + + final val labelRecordFields: HCoalgebra[HEnvT[Boolean, RSchema, ?[_], ?], LabelledSchema] = + new (LabelledSchema ~> HEnvT[Boolean, RSchema, LabelledSchema, ?]) { + + def apply[A](seed: LabelledSchema[A]): HEnvT[Boolean, RSchema, LabelledSchema, A] = + seed match { + case (x, Fix(r @ Record(_, _))) => HEnvT(x, r.hmap(ascribeWith(true))) + case (x, Fix(:*:(left, right))) => + HEnvT( + x, + :*:( + ascribeWith(false)(left), + ascribeWith(x)(right) + ) + ) + case (x, s) => HEnvT(x, s.unFix.hmap(ascribeWith(x))) + } + } + + // This is needed in order to use `traverse` in `reads`. + implicit private val readsApplicative = new Applicative[JsResult] { + + def point[A](a: => A): JsResult[A] = JsSuccess(a) + + def ap[A, B](fa: => JsResult[A])(f: => JsResult[A => B]): JsResult[B] = (f, fa) match { + // Shamelessly copied from the play-json library + case (JsSuccess(f, _), JsSuccess(a, _)) => JsSuccess(f(a)) + case (JsError(e1), JsError(e2)) => JsError(JsError.merge(e1, e2)) + case (JsError(e), _) => JsError(e) + case (_, JsError(e)) => JsError(e) + + } + } + + // This is needed to allow undefined optional fields to be treated as `None`. + final private def undefinedAsNull[A](field: String, r: Reads[A]): Reads[A] = Reads { json => + ((json \ field) match { + case JsDefined(v) => r.reads(v) + case _ => r.reads(JsNull) + }).repath(JsPath \ field) + } + + final private val labellingSeed = + new (Schema ~> LabelledSchema) { + + def apply[A](fSchema: Schema[A]): LabelledSchema[A] = + (false, fSchema) + } + + implicit final def readsInterpreter( + implicit primNT: R.Prim ~> Reads, + branchLabel: R.SumTermId <~< String, + fieldLabel: R.ProductTermId <~< String + ): RInterpreter[Reads] = + Interpreter + .hylo( + labelRecordFields, + new (HEnvT[Boolean, RSchema, Reads, ?] ~> Reads) { + + def apply[A](schema: HEnvT[Boolean, RSchema, Reads, A]): Reads[A] = schema.fa match { + case One() => + Reads { + case JsNull => JsSuccess(()) + case _ => JsError(Seq(JsPath -> Seq(JsonValidationError("error.expected.null")))) + } + case :+:(left, right) => + Reads( + json => + left + .reads(json) + .fold( + el => + right.reads(json).map(\/-.apply) match { + case JsError(er) => JsError(JsError.merge(el, er)) + case x => x + }, + a => JsSuccess(-\/(a)) + ) + ) + case p: :*:[Reads, a, b, R.Prim, R.SumTermId, R.ProductTermId] => + if (schema.ask) + p.left.and(p.right)((x: a, y: b) => (x, y)) + else + (JsPath \ "_1") + .read(p.left) + .and((JsPath \ "_2").read(p.right))((x: a, y: b) => (x, y)) + case PrimSchema(p) => primNT(p) + case SumTerm(id, schema) => undefinedAsNull(branchLabel(id), schema) + case u: Union[R.Prim, R.SumTermId, R.ProductTermId, Reads, A, a] => + u.choices.map(u.iso.get) + case ProductTerm(id, schema) => undefinedAsNull(fieldLabel(id), schema) + case r: Record[R.Prim, R.SumTermId, R.ProductTermId, Reads, A, a] => + r.fields.map(r.iso.get) + case SeqSchema(elem) => + Reads { + case JsArray(elems) => + elems.toList.traverse(elem.reads _) + case _ => JsError(Seq(JsPath -> Seq(JsonValidationError("error.expected.jsarray")))) + } + case i: IsoSchema[R.Prim, R.SumTermId, R.ProductTermId, Reads, a0, A] => + i.base.map(i.iso.get) + } + } + ) + .compose(labellingSeed) + + implicit final def writesInterpreter( + implicit primNT: R.Prim ~> Writes, + branchLabel: R.SumTermId <~< String, + fieldLabel: R.ProductTermId <~< String + ): RInterpreter[Writes] = + Interpreter + .hylo( + labelRecordFields, + new (HEnvT[Boolean, RSchema, Writes, ?] ~> Writes) { + + def apply[A](env: HEnvT[Boolean, RSchema, Writes, A]): Writes[A] = env.fa match { + case One() => Writes(_ => JsNull) + case :+:(left, right) => Writes(_.fold(left.writes, right.writes)) + case :*:(left, right) => + if (env.ask) + Writes( + pair => + (left.writes(pair._1), right.writes(pair._2)) match { + case (l @ JsObject(_), r @ JsObject(_)) => l ++ r + // the following case is impossible, but scalac cannot know that. + case (l, r) => Json.obj("_1" -> l, "_2" -> r) + } + ) + else + Writes( + pair => Json.obj("_1" -> left.writes(pair._1), "_2" -> right.writes(pair._2)) + ) + case PrimSchema(p) => primNT(p) + case SumTerm(id, s) => Writes(a => Json.obj(branchLabel(id) -> s.writes(a))) + case Union(base, iso) => base.contramap(iso.reverseGet) + case ProductTerm(id, s) => Writes(a => Json.obj(fieldLabel(id) -> s.writes(a))) + case Record(base, iso) => base.contramap(iso.reverseGet) + case SeqSchema(elem) => Writes(seq => JsArray(seq.map(elem.writes(_)))) + case IsoSchema(base, iso) => base.contramap(iso.reverseGet) + } + + } + ) + .compose(labellingSeed) + +} diff --git a/modules/scalacheck/src/main/scala/GenModule.scala b/modules/scalacheck/src/main/scala/GenModule.scala index 5e6e648..6283c41 100644 --- a/modules/scalacheck/src/main/scala/GenModule.scala +++ b/modules/scalacheck/src/main/scala/GenModule.scala @@ -6,14 +6,12 @@ package scalacheck import org.scalacheck._ -import org.scalacheck._ - trait GenModule[R <: Realisation] extends SchemaModule[R] { - import SchemaF._ - - implicit final def algebra(implicit primNT: R.Prim ~> Gen): HAlgebra[RSchema, Gen] = - new (RSchema[Gen, ?] ~> Gen) { + implicit final def genInterpreter( + implicit primNT: R.Prim ~> Gen + ): RInterpreter[Gen] = + Interpreter.cata(new (RSchema[Gen, ?] ~> Gen) { def apply[A](schema: RSchema[Gen, A]): Gen[A] = schema match { @@ -32,6 +30,7 @@ trait GenModule[R <: Realisation] extends SchemaModule[R] { case SumTerm(_, base) => base case One() => Gen.const(()) } - } + + }) } diff --git a/modules/tests/src/main/scala/GenModuleExamples.scala b/modules/tests/src/main/scala/GenModuleExamples.scala index 2c62e90..ab60c5c 100644 --- a/modules/tests/src/main/scala/GenModuleExamples.scala +++ b/modules/tests/src/main/scala/GenModuleExamples.scala @@ -15,7 +15,7 @@ trait PrimToGen { case JsonSchema.JsonString => arbitrary[String] case JsonSchema.JsonNumber => arbitrary[BigDecimal] case JsonSchema.JsonBool => arbitrary[Boolean] - case JsonSchema.JsonNull => arbitrary[Null] + case JsonSchema.JsonNull => arbitrary[Unit] } } @@ -73,19 +73,19 @@ object GenModuleExamples { stream .pureApply(genParameters, genSeed1) .zip(stream.pureApply(genParameters, genSeed1)) - .take(100) + .take(10) .toList .forall(p => p._1 == p._2) && stream .pureApply(genParameters, genSeed1) .zip(stream.pureApply(genParameters, genSeed2)) - .take(100) + .take(10) .toList .forall(p => p._1 != p._2) } - val result = prop(Gen.Parameters.default) - if (result.success) Succeed else Fail(List(Right(result.status.toString))) + val result = Test.check(Test.Parameters.default, prop) + if (result.passed) Succeed else Fail(List(Right(result.status.toString))) } ) } diff --git a/modules/tests/src/main/scala/GenericGenModule.scala b/modules/tests/src/main/scala/GenericGenModule.scala index effc7cd..4415f82 100644 --- a/modules/tests/src/main/scala/GenericGenModule.scala +++ b/modules/tests/src/main/scala/GenericGenModule.scala @@ -9,8 +9,6 @@ import generic.GenericSchemaModule trait GenericGenModule[R <: Realisation] extends GenericSchemaModule[R] { - import SchemaF._ - implicit val genApplicativeInstance: Applicative[Gen] = new Applicative[Gen] { override def ap[T, U](fa: => Gen[T])(f: => Gen[T => U]): Gen[U] = fa.flatMap(a => f.map(_(a))) @@ -30,15 +28,15 @@ trait GenericGenModule[R <: Realisation] extends GenericSchemaModule[R] { } yield x } - implicit final def algebra( + implicit final def genericGenInterpreter( implicit primNT: R.Prim ~> Gen - ): HAlgebra[RSchema, Gen] = - covariantTargetFunctor[Gen]( + ): RInterpreter[Gen] = Interpreter.cata( + covariantTargetFunctor( primNT, λ[Gen ~> λ[X => Gen[List[X]]]](x => Gen.listOf(x)), λ[RProductTerm[Gen, ?] ~> Gen](gen => gen.schema), λ[RSumTerm[Gen, ?] ~> Gen](gen => gen.schema), Gen.const(()) ) - + ) } diff --git a/modules/tests/src/main/scala/Main.scala b/modules/tests/src/main/scala/Main.scala index 3150eae..3db9df3 100644 --- a/modules/tests/src/main/scala/Main.scala +++ b/modules/tests/src/main/scala/Main.scala @@ -45,7 +45,8 @@ object Main { ("Examples", SchemaModuleExamples.tests(harness)), ("JSON", JsonExamples.tests(harness)), ("Scalacheck generators", GenModuleExamples.tests(harness)), - ("Generic Show interpreter", ShowExamples.tests(harness)) + ("Generic Show interpreter", ShowExamples.tests(harness)), + ("Play-JSON codecs", PlayJsonExamples.tests(harness)) ) def main(args: Array[String]): Unit = { diff --git a/modules/tests/src/main/scala/PlayJsonExamples.scala b/modules/tests/src/main/scala/PlayJsonExamples.scala new file mode 100644 index 0000000..3ff2cc3 --- /dev/null +++ b/modules/tests/src/main/scala/PlayJsonExamples.scala @@ -0,0 +1,82 @@ +package scalaz + +package schema + +package tests + +import testz._ +import _root_.play.api.libs.json._ +import org.scalacheck._, Prop._, Arbitrary._ + +object PlayJsonExamples { + + val module = new TestModule with play.json.PlayJsonModule[JsonSchema.type] + with scalacheck.GenModule[JsonSchema.type] { + + implicit val primToGenNT = new (JsonSchema.Prim ~> Gen) { + override def apply[A](prim: JsonSchema.Prim[A]): Gen[A] = prim match { + case JsonSchema.JsonString => arbitrary[String] + case JsonSchema.JsonNumber => arbitrary[BigDecimal] + case JsonSchema.JsonBool => arbitrary[Boolean] + case JsonSchema.JsonNull => arbitrary[Unit] + } + } + + implicit val jsonPrimWrites = new (JsonSchema.Prim ~> Writes) { + + def apply[A](p: JsonSchema.Prim[A]): Writes[A] = p match { + case JsonSchema.JsonNull => Writes(_ => JsNull) + case JsonSchema.JsonBool => Writes(b => JsBoolean(b)) + case JsonSchema.JsonNumber => Writes(n => JsNumber(n)) + case JsonSchema.JsonString => Writes(s => JsString(s)) + } + } + + implicit val jsonPrimReads = new (JsonSchema.Prim ~> Reads) { + + def apply[A](p: JsonSchema.Prim[A]): Reads[A] = p match { + case JsonSchema.JsonNull => + Reads { + case JsNull => JsSuccess(()) + case _ => JsError("expected 'null'") + } + case JsonSchema.JsonBool => JsPath.read[Boolean] + case JsonSchema.JsonString => JsPath.read[String] + case JsonSchema.JsonNumber => JsPath.read[BigDecimal] + } + + } + } + + def tests[T](harness: Harness[T]): T = { + import harness._ + import module._ + import JsonSchema._ + + section("play-json codecs")( + test("Cumbersome tuple nested representation") { () => + val triple = prim(JsonString) :*: prim(JsonBool) :*: prim(JsonString) + + val value = ("foo", (true, "bar")) + + assert( + triple.to[Writes].writes(value) == Json + .obj("_1" -> "foo", "_2" -> Json.obj("_1" -> true, "_2" -> "bar")) + ) + }, + test("Derived `Reads` and `Writes` for a given schema should be symmetrical") { () => + implicit val personGen = Arbitrary(person.to[Gen]) + + val reader = person.to[Reads] + val writer = person.to[Writes] + + val prop = forAll { (pers: Person) => + reader.reads(writer.writes(pers)) == JsSuccess(pers) + } + val result = Test.check(Test.Parameters.default, prop) + if (result.passed) Succeed else Fail(List(Right(result.status.toString))) + + } + ) + } +} diff --git a/modules/tests/src/main/scala/ShowExamples.scala b/modules/tests/src/main/scala/ShowExamples.scala index a214ab6..4fd12c6 100644 --- a/modules/tests/src/main/scala/ShowExamples.scala +++ b/modules/tests/src/main/scala/ShowExamples.scala @@ -9,10 +9,10 @@ import generic._ object ShowExamples { - import JsonSchema._ - val showModule = new TestModule with ShowModule[JsonSchema.type] { + import JsonSchema._ + val primToShowNT = new (JsonSchema.Prim ~> Show) { def apply[A](fa: JsonSchema.Prim[A]): Show[A] = @@ -20,9 +20,13 @@ object ShowExamples { case JsonNumber => Show.showFromToString[BigDecimal] case JsonBool => Show.showFromToString[Boolean] case JsonString => Show.shows[String](s => s""""$s"""") - case JsonNull => Show.shows[Null](_ => "null") + case JsonNull => Show.shows[Unit](_ => "null") } } + + implicit val interpreter = + Interpreter.cata(showAlgebra(primToShowNT, identity[String], identity[String])) + } def tests[T](harness: Harness[T]): T = { @@ -33,8 +37,6 @@ object ShowExamples { test("commons Show Instance") { () => { - implicit val alg = showAlgebra(primToShowNT, identity[String], identity[String]) - val testCases: List[(Person, String)] = List( Person(null, None) -> """(name = ("null"), role = (()))""", Person("Alfred", None) -> """(name = ("Alfred"), role = (()))""",