forked from circe/circe-fs2
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
circe#207: generic encoding for streams
- Loading branch information
1 parent
74ed678
commit f0d1934
Showing
5 changed files
with
135 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,3 +8,4 @@ target/ | |
.project | ||
.classpath | ||
tmp/ | ||
.bsp |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,7 @@ language: scala | |
|
||
scala: | ||
- 2.12.10 | ||
- 2.13.0 | ||
- 2.13.4 | ||
|
||
jdk: | ||
- openjdk8 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
package io.circe.fs2 | ||
|
||
import fs2.{Pipe, Stream} | ||
import io.circe.Encoder | ||
import io.circe.syntax._ | ||
import shapeless._ | ||
import shapeless.labelled.FieldType | ||
|
||
object encoding { | ||
def jsonArrayString[F[_], T: Encoder]: Pipe[F, T, String] = | ||
stream => Stream.emit("[") ++ stream.map(t => t.asJson.noSpaces).intersperse(",") ++ Stream.emit("]") | ||
|
||
trait StreamEncoder[F[_], A] { | ||
def encode: A => Stream[F, String] | ||
} | ||
|
||
object StreamEncoder extends LowPriorityImplicits { | ||
|
||
def instance[F[_], A](f: A => Stream[F, String]): StreamEncoder[F, A] = | ||
new StreamEncoder[F, A] { def encode: A => Stream[F, String] = f } | ||
|
||
def apply[F[_], A](implicit enc: StreamEncoder[F, A]): StreamEncoder[F, A] = enc | ||
|
||
implicit def stream[F[_], A: Encoder]: StreamEncoder[F, Stream[F, A]] = StreamEncoder.instance(jsonArrayString) | ||
|
||
implicit def fromOption[F[_], A](implicit enc: StreamEncoder[F, A]): StreamEncoder[F, Option[A]] = | ||
StreamEncoder.instance(_.fold[Stream[F, String]](Stream("null"))(enc.encode)) | ||
|
||
implicit def fromEncoder[F[_], A: Encoder]: StreamEncoder[F, A] = StreamEncoder.instance(a => Stream.emit(a.asJson.noSpaces)) | ||
} | ||
|
||
trait LowPriorityImplicits { | ||
|
||
// TODO: make coproducts work | ||
// implicit def cnilEncoder[F[_]]: StreamEncoder[F, CNil] = | ||
// StreamEncoder.instance(_ => throw new Exception("Inconceivable!")) | ||
// | ||
// implicit def coproductEncoder[F[_], H, T <: Coproduct]( | ||
// implicit | ||
// hEncoder: Lazy[StreamEncoder[F, H]], | ||
// tEncoder: StreamEncoder[F, T] | ||
// ): StreamEncoder[F, H :+: T] = StreamEncoder.instance { | ||
// case Inl(h) => hEncoder.value.encode(h) | ||
// case Inr(t) => tEncoder.encode(t) | ||
// } | ||
|
||
implicit def hnilEncoder[F[_]]: StreamEncoder[F, HNil] = | ||
StreamEncoder.instance(_ => Stream.empty) | ||
|
||
implicit def hlistObjectEncoder[F[_], K <: Symbol, H, T <: HList]( | ||
implicit | ||
witness: Witness.Aux[K], | ||
hEncoder: Lazy[StreamEncoder[F, H]], | ||
tEncoder: StreamEncoder[F, T] | ||
): StreamEncoder[F, FieldType[K, H] :: T] = { | ||
val fieldName = witness.value.name | ||
StreamEncoder.instance { | ||
case h :: t => | ||
val head = hEncoder.value.encode(h) | ||
val tail = tEncoder.encode(t) | ||
val comma = t match { | ||
case HNil => Stream.empty | ||
case _ => Stream.emit(",") | ||
} | ||
Stream.emit(s""""$fieldName":""") ++ head ++ comma ++ tail | ||
} | ||
} | ||
|
||
implicit def genericObjectEncoder[F[_], A, H]( | ||
implicit | ||
generic: LabelledGeneric.Aux[A, H], | ||
hEncoder: Lazy[StreamEncoder[F, H]] | ||
): StreamEncoder[F, A] = | ||
StreamEncoder.instance { value => | ||
hEncoder.value.encode(generic.to(value)).cons1("{") ++ Stream("}") | ||
} | ||
|
||
} | ||
|
||
object syntax { | ||
implicit class StreamEncoderSyntax[A](self: A) { | ||
def asJsonStream[F[_]](implicit enc: StreamEncoder[F, A]): Stream[F, String] = enc.encode(self) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package io.circe.fs2 | ||
|
||
import fs2.{Pure, Stream} | ||
import io.circe.Codec | ||
import io.circe.generic.semiauto._ | ||
import org.scalatest.matchers.should.Matchers | ||
import encoding.syntax._ | ||
import io.circe.jawn.parse | ||
import org.scalatest.flatspec.AnyFlatSpec | ||
|
||
class EncodingSuite extends AnyFlatSpec with Matchers { | ||
case class Simple(s: String) | ||
|
||
object Simple { | ||
implicit val enc: Codec[Simple] = deriveCodec | ||
} | ||
|
||
case class Streamed[F[_], A](a: Int, b: Stream[Pure, A], c: Simple) | ||
|
||
case class Listed[A](a: Int, b: List[A], c: Simple) | ||
|
||
object Listed { | ||
def fromStreamed[F[_], A](s: Streamed[F, A]): Listed[A] = Listed(s.a, s.b.compile.toList, s.c) | ||
implicit def codec[A: Codec]: Codec[Listed[A]] = deriveCodec | ||
} | ||
|
||
it should "encode a case class containing a stream" in { | ||
val streamed = Streamed(1, Stream(Simple("a"), Simple("b"), Simple("c")), Simple("2")) | ||
parse(streamed.asJsonStream[Pure].compile.string).flatMap(_.as[Listed[Simple]]) shouldBe Right(Listed.fromStreamed(streamed)) | ||
} | ||
|
||
it should "encode a nested case class containing a stream" in { | ||
case class Nested(a: Stream[Pure, Int], b: Option[Nested]) | ||
case class NestedL(a: List[Int], b: Option[NestedL]) | ||
object NestedL { | ||
def fromStreamed(s: Nested): NestedL = NestedL(s.a.compile.toList, s.b.map(fromStreamed)) | ||
implicit val codec: Codec[NestedL] = deriveCodec | ||
} | ||
|
||
val streamed = Nested(Stream(1), Some(Nested(Stream(2), None))) | ||
val string = streamed.asJsonStream[Pure].compile.string | ||
|
||
parse(string).flatMap(_.as[NestedL]) shouldBe Right(NestedL.fromStreamed(streamed)) | ||
} | ||
} |