diff --git a/argonaut/src/main/scala/io/finch/argonaut/Decoders.scala b/argonaut/src/main/scala/io/finch/argonaut/Decoders.scala index e4cdfcea2..9a52fe2e6 100644 --- a/argonaut/src/main/scala/io/finch/argonaut/Decoders.scala +++ b/argonaut/src/main/scala/io/finch/argonaut/Decoders.scala @@ -1,13 +1,11 @@ package io.finch.argonaut -import argonaut.{CursorHistory, DecodeJson, Json} -import argonaut.JawnParser._ -import com.twitter.util.{Return, Throw, Try} +import argonaut._ +import argonaut.DecodeJson +import cats.syntax.either._ +import com.twitter.util.{Return, Throw} import io.finch._ import io.finch.internal.HttpContent -import java.nio.charset.StandardCharsets -import jawn.Parser -import scala.util.{Failure, Success} trait Decoders { @@ -16,18 +14,9 @@ trait Decoders { */ implicit def decodeArgonaut[A](implicit d: DecodeJson[A]): Decode.Json[A] = Decode.json { (b, cs) => - val err: (String, CursorHistory) => Try[A] = { (str, hist) => Throw(new Exception(str)) } - - val attemptJson = cs match { - case StandardCharsets.UTF_8 => - Parser.parseFromByteBuffer[Json](b.asByteBuffer)(facade) - case _ => - Parser.parseFromString[Json](b.asString(cs))(facade) - } - - attemptJson match { - case Success(value) => d.decodeJson(value).fold(err, Return.apply) - case Failure(error) => Throw(error) + Parse.parse(b.asString(cs)).flatMap(_.as[A].result.leftMap(_._1)) match { + case Right(result) => Return(result) + case Left(error) => Throw(new Exception(error)) } } } diff --git a/build.sbt b/build.sbt index 99e871fbf..7b3740b2e 100644 --- a/build.sbt +++ b/build.sbt @@ -11,16 +11,17 @@ lazy val finagleVersion = "6.45.0" lazy val twitterServerVersion = "1.30.0" lazy val finagleOAuth2Version = "0.6.45" lazy val finagleHttpAuthVersion = "0.1.0" -lazy val circeVersion = "0.8.0" -lazy val circeJacksonVersion = "0.8.0" +lazy val circeVersion = "0.9.0-M1" +lazy val circeJacksonVersion = "0.9.0-M1" lazy val catbirdVersion = "0.15.0" lazy val shapelessVersion = "2.3.2" -lazy val catsVersion = "0.9.0" +lazy val catsVersion = "1.0.0-MF" lazy val sprayVersion = "1.3.3" lazy val playVersion = "2.6.0-RC2" lazy val jacksonVersion = "2.8.8" lazy val argonautVersion = "6.2" lazy val json4sVersion = "3.5.2" +lazy val iterateeVersion = "0.13.0" lazy val compilerOptions = Seq( "-deprecation", @@ -177,15 +178,26 @@ lazy val finch = project.in(file(".")) "io.spray" %% "spray-json" % sprayVersion )) .aggregate( - core, generic, argonaut, jackson, json4s, circe, playjson, sprayjson, benchmarks, test, jsonTest, + core, iteratee, generic, argonaut, jackson, json4s, circe, playjson, sprayjson, benchmarks, test, jsonTest, oauth2, examples, sse ) - .dependsOn(core, generic, circe) + .dependsOn(core, iteratee, generic, circe) lazy val core = project .settings(moduleName := "finch-core") .settings(allSettings) +lazy val iteratee = project + .settings(moduleName := "finch-iteratee") + .settings(allSettings) + .settings( + libraryDependencies ++= Seq( + "io.iteratee" %% "iteratee-core" % iterateeVersion, + "io.iteratee" %% "iteratee-twitter" % iterateeVersion + ) + ) + .dependsOn(core % "compile->compile;test->test") + lazy val generic = project .settings(moduleName := "finch-generic") .settings(allSettings) @@ -206,17 +218,17 @@ lazy val jsonTest = project.in(file("json-test")) .settings( libraryDependencies ++= Seq( "io.circe" %% "circe-core" % circeVersion, - "io.circe" %% "circe-jawn" % circeVersion + "io.circe" %% "circe-jawn" % circeVersion, + "io.circe" %% "circe-streaming" % circeVersion ) ++ testDependencies ) - .dependsOn(core) + .dependsOn(core, iteratee) lazy val argonaut = project .settings(moduleName := "finch-argonaut") .settings(allSettings) .settings(libraryDependencies ++= Seq( - "io.argonaut" %% "argonaut" % argonautVersion, - "io.argonaut" %% "argonaut-jawn" % argonautVersion + "io.argonaut" %% "argonaut" % argonautVersion )) .dependsOn(core, jsonTest % "test") @@ -244,12 +256,13 @@ lazy val circe = project .settings( libraryDependencies ++= Seq( "io.circe" %% "circe-core" % circeVersion, + "io.circe" %% "circe-streaming" % circeVersion, "io.circe" %% "circe-jawn" % circeVersion, "io.circe" %% "circe-generic" % circeVersion % "test", "io.circe" %% "circe-jackson28" % circeJacksonVersion ) ) - .dependsOn(core, jsonTest % "test") + .dependsOn(core, iteratee, jsonTest % "test") lazy val playjson = project .settings(moduleName :="finch-playjson") @@ -318,7 +331,7 @@ lazy val examples = project "com.github.finagle" %% "finagle-oauth2" % finagleOAuth2Version ) ) - .dependsOn(core, circe, jackson, oauth2) + .dependsOn(core, circe, jackson, oauth2, iteratee) lazy val benchmarks = project .settings(moduleName := "finch-benchmarks") diff --git a/circe/src/main/scala/io/finch/circe/Decoders.scala b/circe/src/main/scala/io/finch/circe/Decoders.scala index 2b85c2978..e4261098c 100644 --- a/circe/src/main/scala/io/finch/circe/Decoders.scala +++ b/circe/src/main/scala/io/finch/circe/Decoders.scala @@ -1,12 +1,16 @@ package io.finch.circe -import com.twitter.util.{Return, Throw, Try} +import com.twitter.util.{Future, Return, Throw, Try} +import io.catbird.util._ import io.circe.Decoder import io.circe.jawn._ -import io.finch.Decode +import io.circe.streaming._ +import io.finch.{Application, Decode} import io.finch.internal.HttpContent +import io.finch.iteratee.Enumerate import java.nio.charset.StandardCharsets + trait Decoders { /** @@ -20,4 +24,17 @@ trait Decoders { attemptJson.fold[Try[A]](Throw.apply, Return.apply) } + + implicit def enumerateCirce[A : Decoder]: Enumerate.Json[A] = { + Enumerate.instance[A, Application.Json]((enum, cs) => { + val parsed = cs match { + case StandardCharsets.UTF_8 => + enum.map(_.asByteArray).through(byteStreamParser[Future]) + case _ => + enum.map(_.asString(cs)).through(stringStreamParser[Future]) + } + parsed.through(decoder[Future, A]) + }) + } + } diff --git a/circe/src/main/scala/io/finch/circe/package.scala b/circe/src/main/scala/io/finch/circe/package.scala index 807462d18..65dbab1c9 100644 --- a/circe/src/main/scala/io/finch/circe/package.scala +++ b/circe/src/main/scala/io/finch/circe/package.scala @@ -12,8 +12,8 @@ package object circe extends Encoders with Decoders { /** * Provides a [[Printer]] that drops null keys. */ - object dropNullKeys extends Encoders with Decoders { - private[this] val printer: Printer = Printer.noSpaces.copy(dropNullKeys = true) + object dropNullValues extends Encoders with Decoders { + private[this] val printer: Printer = Printer.noSpaces.copy(dropNullValues = true) protected def print(json: Json, cs: Charset): Buf = Buf.ByteBuffer.Owned(printer.prettyByteBuffer(json, cs)) } @@ -29,4 +29,3 @@ package object circe extends Encoders with Decoders { Buf.ByteArray.Owned(io.circe.jackson.jacksonPrint(json).getBytes(cs.name)) } } - diff --git a/circe/src/test/scala/io/finch/circe/test/CirceSpec.scala b/circe/src/test/scala/io/finch/circe/test/CirceSpec.scala index db8a1510e..774f100ba 100644 --- a/circe/src/test/scala/io/finch/circe/test/CirceSpec.scala +++ b/circe/src/test/scala/io/finch/circe/test/CirceSpec.scala @@ -6,6 +6,7 @@ import io.finch.test.AbstractJsonSpec class CirceSpec extends AbstractJsonSpec { import io.finch.circe._ checkJson("circe") + checkEnumerateJson("circe") } class CirceJacksonSpec extends AbstractJsonSpec { @@ -14,6 +15,6 @@ class CirceJacksonSpec extends AbstractJsonSpec { } class CirceDropNullKeysSpec extends AbstractJsonSpec { - import io.finch.circe.dropNullKeys._ + import io.finch.circe.dropNullValues._ checkJson("circe-dropNullKeys") } diff --git a/examples/src/main/scala/io/finch/iteratee/Main.scala b/examples/src/main/scala/io/finch/iteratee/Main.scala new file mode 100644 index 000000000..28795e715 --- /dev/null +++ b/examples/src/main/scala/io/finch/iteratee/Main.scala @@ -0,0 +1,73 @@ +package io.finch.iteratee + +import scala.util.Random + +import com.twitter.finagle.Http +import com.twitter.util.{Await, Future} +import io.catbird.util._ +import io.circe.generic.auto._ +import io.finch._ +import io.finch.circe._ +import io.iteratee.{Enumerator, Iteratee} + +/** + * A Finch application featuring iteratee-based streaming support. + * This approach is more advanced and performant then basic [[com.twitter.concurrent.AsyncStream]] + * + * There are three endpoints in this example: + * + * 1. `sumJson` - streaming request + * 2. `streamJson` - streaming response + * 3. `isPrime` - end-to-end (request - response) streaming + * + * Use the following sbt command to run the application. + * + * {{{ + * $ sbt 'examples/runMain io.finch.iteratee.Main' + * }}} + * + * Use the following HTTPie/curl commands to test endpoints. + * + * {{{ + * $ curl -X POST --header "Transfer-Encoding: chunked" -d '{"i": 40} {"i": 2}' localhost:8081/sumJson + * + * $ http --stream GET :8081/streamJson + * + * $ curl -X POST --header "Transfer-Encoding: chunked" -d '{"i": 40} {"i": 42}' localhost:8081/streamPrime + * }}} + */ +object Main { + + private val stream: Stream[Int] = Stream(Random.nextInt()).flatMap(_ => stream) + + val sumJson: Endpoint[Result] = post("sumJson" :: enumeratorJsonBody[Number]) { (enum: Enumerator[Future, Number]) => + enum.into(Iteratee.fold[Future, Number, Result](Result(0))(_ add _)).map(Ok) + } + + val streamJson: Endpoint[Enumerator[Future, Number]] = get("streamJson") { + Ok(Enumerator.enumStream[Future, Int](stream).map(Number.apply)) + } + + val isPrime: Endpoint[Enumerator[Future, IsPrime]] = post("streamPrime" :: enumeratorJsonBody[Number]) { + (enum: Enumerator[Future, Number]) => Ok(enum.map(_.isPrime)) + } + + def main(args: Array[String]): Unit = + Await.result(Http.server + .withStreaming(enabled = true) + .serve(":8081", (sumJson :+: streamJson :+: isPrime).toServiceAs[Application.Json]) + ) + +} + +case class Result(result: Int) { + def add(n: Number): Result = copy(result = result + n.i) +} + +case class Number(i: Int) { + + def isPrime: IsPrime = IsPrime(!(2 +: (3 to Math.sqrt(i.toDouble).toInt by 2) exists (i % _ == 0))) + +} + +case class IsPrime(isPrime: Boolean) diff --git a/iteratee/src/main/scala/io/finch/iteratee/Enumerate.scala b/iteratee/src/main/scala/io/finch/iteratee/Enumerate.scala new file mode 100644 index 000000000..73861340b --- /dev/null +++ b/iteratee/src/main/scala/io/finch/iteratee/Enumerate.scala @@ -0,0 +1,48 @@ +package io.finch.iteratee + +import java.nio.charset.Charset +import scala.annotation.implicitNotFound + +import com.twitter.io.Buf +import com.twitter.util.Future +import io.finch.Application +import io.iteratee.Enumerator + +/** + * Enumerate HTTP streamed payload represented as [[Enumerator]] (encoded with [[Charset]]) into + * an [[Enumerator]] of arbitrary type `A`. + */ +trait Enumerate[A] { + + type ContentType <: String + + def apply(enumerator: Enumerator[Future, Buf], cs: Charset): Enumerator[Future, A] +} + +object Enumerate extends EnumerateInstances { + + @implicitNotFound( +"""An Enumerator endpoint requires implicit Enumerate instance in scope, probably decoder for ${A} is missing. + + Make sure ${A} is one of the following: + + * A com.twitter.io.Buf + * A value of a type with an io.finch.iteratee.Enumerate instance (with the corresponding content-type) +""" + ) + type Aux[A, CT <: String] = Enumerate[A] {type ContentType = CT} + + type Json[A] = Aux[A, Application.Json] +} + +trait EnumerateInstances { + def instance[A, CT <: String] + (f: (Enumerator[Future, Buf], Charset) => Enumerator[Future, A]): Enumerate.Aux[A, CT] = new Enumerate[A] { + type ContentType = CT + + def apply(enumerator: Enumerator[Future, Buf], cs: Charset): Enumerator[Future, A] = f(enumerator, cs) + } + + implicit def buf2bufDecode[CT <: String]: Enumerate.Aux[Buf, CT] = + instance[Buf, CT]((enum, _) => enum) +} diff --git a/iteratee/src/main/scala/io/finch/iteratee/package.scala b/iteratee/src/main/scala/io/finch/iteratee/package.scala new file mode 100644 index 000000000..3973c691b --- /dev/null +++ b/iteratee/src/main/scala/io/finch/iteratee/package.scala @@ -0,0 +1,96 @@ +package io.finch + +import cats.Eval +import com.twitter.finagle.http.Response +import com.twitter.io._ +import com.twitter.util.Future +import io.catbird.util._ +import io.finch.internal._ +import io.finch.items.RequestItem +import io.iteratee.{Enumerator, Iteratee} +import io.iteratee.twitter.FutureModule +import shapeless.Witness + +/** + * Iteratee module + */ +package object iteratee extends IterateeInstances { + + import syntax._ + + private[finch] def enumeratorFromReader(reader: Reader): Enumerator[Future, Buf] = { + def rec(reader: Reader): Enumerator[Future, Buf] = { + reader.read(Int.MaxValue).intoEnumerator.flatMap { + case None => empty[Buf] + case Some(buf) => enumOne(buf).append(rec(reader)) + } + } + rec(reader).ensureEval(Eval.later(Future.value(reader.discard()))) + } + + /** + * An evaluating [[Endpoint]] that reads a required chunked streaming binary body, interpreted as + * an `Enumerator[Future, A]`. The returned [[Endpoint]] only matches chunked (streamed) requests. + */ + def enumeratorBody[A, CT <: String](implicit decode: Enumerate.Aux[A, CT]): Endpoint[Enumerator[Future, A]] = { + new Endpoint[Enumerator[Future, A]] { + final def apply(input: Input): Endpoint.Result[Enumerator[Future, A]] = { + if (!input.request.isChunked) { + EndpointResult.Skipped + } else { + val req = input.request + EndpointResult.Matched( + input, + Rerunnable(Output.payload(decode(enumeratorFromReader(req.reader), req.charsetOrUtf8))) + ) + } + } + + final override def item: RequestItem = items.BodyItem + final override def toString: String = "enumeratorBody" + } + } + + /** + * An evaluating [[Endpoint]] that reads a required chunked streaming JSON body, interpreted as + * an `Enumerator[Future, A]`. The returned [[Endpoint]] only matches chunked (streamed) requests. + */ + def enumeratorJsonBody[A](implicit ad: Enumerate.Aux[A, Application.Json]): Endpoint[Enumerator[Future, A]] = + enumeratorBody[A, Application.Json].withToString("enumeratorJsonBody") + +} + +trait IterateeInstances extends LowPriorityInstances { + + implicit def enumeratorToJsonResponse[A](implicit + e: Encode.Aux[A, Application.Json], + w: Witness.Aux[Application.Json] + ): ToResponse.Aux[Enumerator[Future, A], Application.Json] = { + withCustomIteratee[A, Application.Json](writer => + foreachM((buf: Buf) => writer.write(buf.concat(ToResponse.NewLine))) + ) + } +} + +trait LowPriorityInstances extends FutureModule { + implicit def enumeratorToResponse[A, CT <: String](implicit + e: Encode.Aux[A, CT], + w: Witness.Aux[CT] + ): ToResponse.Aux[Enumerator[Future, A], CT] = { + withCustomIteratee(writer => foreachM((buf: Buf) => writer.write(buf))) + } + + protected def withCustomIteratee[A, CT <: String](iteratee: Writer => Iteratee[Future, Buf, Unit])(implicit + e: Encode.Aux[A, CT], + w: Witness.Aux[CT] + ): ToResponse.Aux[Enumerator[Future, A], CT] = { + ToResponse.instance[Enumerator[Future, A], CT]((enum, cs) => { + val response = Response() + response.setChunked(true) + response.contentType = w.value + val writer = response.writer + enum.ensureEval(Eval.later(writer.close())).map(e.apply(_, cs)).into(iteratee(writer)) + response + }) + } +} diff --git a/iteratee/src/test/scala/io/finch/iteratee/EnumerateEndpointSpec.scala b/iteratee/src/test/scala/io/finch/iteratee/EnumerateEndpointSpec.scala new file mode 100644 index 000000000..f87b4ddb5 --- /dev/null +++ b/iteratee/src/test/scala/io/finch/iteratee/EnumerateEndpointSpec.scala @@ -0,0 +1,62 @@ +package io.finch.iteratee + +import com.twitter.finagle.http.Request +import com.twitter.io.{Buf, Writer} +import com.twitter.util._ +import io.catbird.util._ +import io.finch.{Application, EndpointResult, FinchSpec, Input} +import io.finch.internal._ +import org.scalatest.prop.GeneratorDrivenPropertyChecks + +class EnumerateEndpointSpec extends FinchSpec with GeneratorDrivenPropertyChecks { + + private implicit val enumerateString = Enumerate.instance[String, Application.Json]((enum, cs) => { + enum.map(_.asString(cs)) + }) + + "enumeratorBody" should "enumerate input stream" in { + forAll { (data: List[Buf]) => + val req = Request() + req.setChunked(chunked = true) + write(data, req.writer) + + val Some(enumerator) = + enumeratorBody[Buf, Application.OctetStream].apply(Input.fromRequest(req)).awaitValueUnsafe() + + Await.result(enumerator.toVector) should contain theSameElementsAs data + } + + } + + "enumeratorBody.toString" should "be correct" in { + enumeratorBody[Buf, Application.OctetStream].toString shouldBe "enumeratorBody" + } + + "enumeratorBody" should "skip matching if request is not chunked" in { + enumeratorBody[Buf, Application.OctetStream].apply(Input.fromRequest(Request())) shouldBe EndpointResult.Skipped + } + + "enumeratorJsonBody" should "enumerate input stream if required Enumerate instance is presented" in { + forAll { (data: List[String]) => + val req = Request() + req.setChunked(chunked = true) + write(data.map(Buf.Utf8.apply), req.writer) + + val Some(enumerator) = enumeratorJsonBody[String].apply(Input.fromRequest(req)).awaitValueUnsafe() + + Await.result(enumerator.toVector) should contain theSameElementsAs data + } + } + + "enumeratorJsonBody.toString" should "be correct" in { + enumeratorJsonBody[Buf].toString shouldBe "enumeratorJsonBody" + } + + private def write(data: List[Buf], writer: Writer with Closable): Future[Unit] = { + data match { + case Nil => writer.close() + case head :: tail => writer.write(head).foreach(_ => write(tail, writer)) + } + } + +} diff --git a/iteratee/src/test/scala/io/finch/iteratee/ToResponseSpec.scala b/iteratee/src/test/scala/io/finch/iteratee/ToResponseSpec.scala new file mode 100644 index 000000000..18fae331d --- /dev/null +++ b/iteratee/src/test/scala/io/finch/iteratee/ToResponseSpec.scala @@ -0,0 +1,37 @@ +package io.finch.iteratee + +import java.nio.charset.StandardCharsets + +import com.twitter.io.Buf +import com.twitter.util.{Await, Future} +import io.catbird.util._ +import io.finch.{Application, FinchSpec, Text, ToResponse} +import io.iteratee.Enumerator +import io.iteratee.twitter.FutureModule +import org.scalatest.prop.GeneratorDrivenPropertyChecks + +class ToResponseSpec extends FinchSpec with GeneratorDrivenPropertyChecks with FutureModule { + + "enumeratorToResponse" should "correctly encode Enumerator to Response" in { + forAll { (data: List[Buf]) => + Await.result( + enumeratorFromReader(response[Buf, Text.Plain](data).reader).toVector + ) should contain theSameElementsAs data + } + } + + "enumeratorToJsonResponse" should "insert new lines after each chunk" in { + forAll { (data: List[Buf]) => + Await.result( + enumeratorFromReader(response[Buf, Application.Json](data).reader).toVector + ) should contain theSameElementsAs data.map(_.concat(ToResponse.NewLine)) + } + } + + private def response[A, CT <: String](data: List[A])(implicit tr: ToResponse.Aux[Enumerator[Future, A], CT]) = { + val toResponse = implicitly[ToResponse.Aux[Enumerator[Future, A], CT]] + val enumerator = enumList(data) + + toResponse(enumerator, StandardCharsets.UTF_8) + } +} diff --git a/json-test/src/main/scala/io/finch/test/AbstractJsonSpec.scala b/json-test/src/main/scala/io/finch/test/AbstractJsonSpec.scala index a792b880e..f3aa43715 100644 --- a/json-test/src/main/scala/io/finch/test/AbstractJsonSpec.scala +++ b/json-test/src/main/scala/io/finch/test/AbstractJsonSpec.scala @@ -6,6 +6,7 @@ import cats.Eq import cats.instances.AllInstances import io.circe.Decoder import io.finch.{Decode, Encode} +import io.finch.iteratee.Enumerate import io.finch.test.data._ import org.scalacheck.{Arbitrary, Gen} import org.scalatest.{FlatSpec, Matchers} @@ -28,14 +29,18 @@ abstract class AbstractJsonSpec extends FlatSpec with Matchers with Checkers wit new Exception(s) ) + private def loop(name: String, ruleSet: Laws#RuleSet, library: String): Unit = + for ((id, prop) <- ruleSet.all.properties) it should (s"$library.$id.$name") in { check(prop) } + def checkJson(library: String)(implicit e: Encode.Json[List[ExampleNestedCaseClass]], d: Decode.Json[List[ExampleNestedCaseClass]] ): Unit = { - def loop(name: String, ruleSet: Laws#RuleSet): Unit = - for ((id, prop) <- ruleSet.all.properties) it should (s"$library.$id.$name") in { check(prop) } + loop("List[ExampleNestedCaseClass]", JsonLaws.encoding[List[ExampleNestedCaseClass]].all, library) + loop("List[ExampleNestedCaseClass]", JsonLaws.decoding[List[ExampleNestedCaseClass]].all, library) + } - loop("List[ExampleNestedCaseClass]", JsonLaws.encoding[List[ExampleNestedCaseClass]].all) - loop("List[ExampleNestedCaseClass]", JsonLaws.decoding[List[ExampleNestedCaseClass]].all) + def checkEnumerateJson(library: String)(implicit en: Enumerate.Json[ExampleNestedCaseClass]): Unit = { + loop("ExampleNestedCaseClass", JsonLaws.enumerating[ExampleNestedCaseClass].all, library) } } diff --git a/json-test/src/main/scala/io/finch/test/JsonLaws.scala b/json-test/src/main/scala/io/finch/test/JsonLaws.scala index cc1777ef2..96a5dc149 100644 --- a/json-test/src/main/scala/io/finch/test/JsonLaws.scala +++ b/json-test/src/main/scala/io/finch/test/JsonLaws.scala @@ -7,10 +7,14 @@ import cats.instances.AllInstances import cats.laws._ import cats.laws.discipline._ import com.twitter.io.Buf +import com.twitter.util._ import io.circe.{Decoder, Encoder} import io.circe.jawn +import io.circe.streaming._ import io.finch._ import io.finch.internal.HttpContent +import io.finch.iteratee.Enumerate +import io.iteratee.twitter.FutureModule import org.scalacheck.{Arbitrary, Prop} import org.typelevel.discipline.Laws @@ -39,6 +43,41 @@ trait DecodeJsonLaws[A] extends Laws with AllInstances { ) } +trait EnumerateJsonLaws[A] extends Laws with AllInstances with FutureModule { + + private implicit val monad = F + + def enumerate: Enumerate.Json[A] + + def success(a: List[A], cs: Charset)(implicit e: Encoder[A], d: Decoder[A]): IsEq[Vector[A]] = { + val json = enumList(a).map(a => e(a).noSpaces).intersperse("\n") + val enum = json.map(str => Buf.ByteArray.Owned(str.getBytes(cs.name))) + val toCompare = json.through(stringStreamParser[Future]).through(decoder[Future, A]) + Await.result(enumerate(enum, cs).toVector) <-> Await.result(toCompare.toVector) + } + + def failure(s: String, cs: Charset): Boolean = { + val enum = enumOne(Buf.ByteArray.Owned(s"NOT A JSON$s".getBytes(cs.name))) + Try( + Await.result(enumerate(enum, cs).toVector) + ).isThrow + } + + def all(implicit + a: Arbitrary[A], + cs: Arbitrary[Charset], + e: Encoder[A], + d: Decoder[A], + eq: Eq[A] + ): RuleSet = new DefaultRuleSet( + name = "enumerate", + parent = None, + "success" -> Prop.forAll { (a: List[A], cs: Charset) => success(a, cs) }, + "failure" -> Prop.forAll { (s: String, cs: Charset) => failure(s, cs) } + ) + +} + trait EncodeJsonLaws[A] extends Laws with AllInstances { def encode: Encode.Json[A] @@ -62,4 +101,9 @@ object JsonLaws { new DecodeJsonLaws[A] { val decode: Decode.Json[A] = implicitly[Decode.Json[A]] } + + def enumerating[A : Enumerate.Json]: EnumerateJsonLaws[A] = + new EnumerateJsonLaws[A] { + val enumerate: Enumerate.Json[A] = implicitly[Enumerate.Json[A]] + } }