Skip to content

Commit

Permalink
Merge pull request #214 from theiterators/play-circe-instances
Browse files Browse the repository at this point in the history
Instances support for circe & play-json
  • Loading branch information
pk044 committed Jun 22, 2022
2 parents 4ca35c6 + f44b956 commit 24c6c59
Show file tree
Hide file tree
Showing 11 changed files with 1,060 additions and 2 deletions.
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ lazy val sprayJsonSupport = project

lazy val playJsonSupport = project
.in(file("play-json"))
.dependsOn(macroUtils)
.dependsOn(macroUtils, instances)
.settings(playJsonSettings: _*)
.settings(publishSettings: _*)
.settings(disableScala("3"))
Expand All @@ -344,7 +344,7 @@ lazy val playJsonSupport = project

lazy val circeSupport = project
.in(file("circe"))
.dependsOn(macroUtils)
.dependsOn(macroUtils, instances)
.settings(circeSettings: _*)
.settings(crossBuildSettings: _*)
.settings(publishSettings: _*)
Expand Down
7 changes: 7 additions & 0 deletions circe/src/main/scala/pl/iterators/kebs/circe/KebsCirce.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package pl.iterators.kebs.circe

import io.circe.generic.AutoDerivation
import io.circe.{Decoder, Encoder}
import pl.iterators.kebs.instances.InstanceConverter
import pl.iterators.kebs.macros.CaseClass1Rep

import scala.language.experimental.macros
Expand All @@ -12,6 +13,12 @@ trait KebsCirce extends AutoDerivation {
decoder.emap(obj => Try(rep.apply(obj)).toEither.left.map(_.getMessage))
implicit def flatEncoder[T, A](implicit rep: CaseClass1Rep[T, A], encoder: Encoder[A]): Encoder[T] =
encoder.contramap(rep.unapply)

implicit def instanceConverterEncoder[T, A](implicit rep: InstanceConverter[T, A], encoder: Encoder[A]): Encoder[T] =
encoder.contramap(rep.encode)

implicit def instanceConverterDecoder[T, A](implicit rep: InstanceConverter[T, A], decoder: Decoder[A]): Decoder[T] =
decoder.emap(obj => Try(rep.decode(obj)).toEither.left.map(_.getMessage))
}

object KebsCirce {
Expand Down
35 changes: 35 additions & 0 deletions circe/src/test/scala/instances/NetInstancesTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package instances

import io.circe.{Decoder, Encoder, Json}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import pl.iterators.kebs.circe.KebsCirce
import pl.iterators.kebs.instances.net.URIString

import java.net.URI

class NetInstancesTests extends AnyFunSuite with Matchers with KebsCirce with URIString {

test("URI standard format") {
val decoder = implicitly[Decoder[URI]]
val encoder = implicitly[Encoder[URI]]
val value = "iteratorshq.com"
val obj = new URI(value)

encoder(obj) shouldBe Json.fromString(value)
decoder(Json.fromString(value).hcursor) shouldBe Right(obj)
}

test("URI wrong format exception") {
val decoder = implicitly[Decoder[URI]]
val value = "not a URI"

decoder(Json.fromString(value).hcursor) shouldBe a [Left[_, _]]
}

test("No CaseClass1Rep implicits derived") {

"implicitly[CaseClass1Rep[URI, String]]" shouldNot typeCheck
"implicitly[CaseClass1Rep[String, URI]]" shouldNot typeCheck
}
}
112 changes: 112 additions & 0 deletions circe/src/test/scala/instances/TimeInstancesMixinTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package instances

import io.circe.{Decoder, Encoder, Json}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import pl.iterators.kebs.circe.KebsCirce
import pl.iterators.kebs.instances.time.LocalDateTimeString
import pl.iterators.kebs.instances.time.mixins.{DurationNanosLong, InstantEpochMilliLong}
import pl.iterators.kebs.instances.{InstanceConverter, TimeInstances}

import java.time._
import java.time.format.DateTimeFormatter

class TimeInstancesMixinTests extends AnyFunSuite with Matchers {

test("Instant epoch milli format") {
object TimeInstancesProtocol extends KebsCirce with InstantEpochMilliLong
import TimeInstancesProtocol._

"implicitly[CaseClass1Rep[Instant, Long]]" shouldNot typeCheck
"implicitly[CaseClass1Rep[Long, Instant]]" shouldNot typeCheck

val decoder = implicitly[Decoder[Instant]]
val encoder = implicitly[Encoder[Instant]]
val value = 123456789
val obj = Instant.ofEpochMilli(value)

encoder(obj) shouldBe Json.fromInt(value)
decoder(Json.fromInt(value).hcursor) shouldBe Right(obj)
}

test("Duration nanos format, Instant epoch milli format") {
object TimeInstancesProtocol extends KebsCirce with DurationNanosLong with InstantEpochMilliLong
import TimeInstancesProtocol._

"implicitly[CaseClass1Rep[Instant, Long]]" shouldNot typeCheck
"implicitly[CaseClass1Rep[Long, Instant]]" shouldNot typeCheck
"implicitly[CaseClass1Rep[Duration, Long]]" shouldNot typeCheck
"implicitly[CaseClass1Rep[Long, Duration]]" shouldNot typeCheck

val decoder_duration = implicitly[Decoder[Duration]]
val encoder_duration = implicitly[Encoder[Duration]]
val value_duration = 123456789
val obj_duration = Duration.ofNanos(value_duration)

val decoder_instant = implicitly[Decoder[Instant]]
val encoder_instant = implicitly[Encoder[Instant]]
val value_instant = 123456789
val obj_instant = Instant.ofEpochMilli(value_instant)

encoder_duration(obj_duration) shouldBe Json.fromInt(value_duration)
decoder_duration(Json.fromInt(value_duration).hcursor) shouldBe Right(obj_duration)

encoder_instant(obj_instant) shouldBe Json.fromInt(value_instant)
decoder_instant(Json.fromInt(value_instant).hcursor) shouldBe Right(obj_instant)
}

test("LocalDateTime custom format using companion object") {
object TimeInstancesProtocol extends KebsCirce with LocalDateTimeString {
val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm")

override implicit val localDateTimeFormatter: InstanceConverter[LocalDateTime, String] =
InstanceConverter.apply[LocalDateTime, String](_.format(formatter), LocalDateTime.parse(_, formatter))
}
import TimeInstancesProtocol._

"implicitly[CaseClass1Rep[LocalDateTime, String]]" shouldNot typeCheck
"implicitly[CaseClass1Rep[String, LocalDateTime]]" shouldNot typeCheck

val encoder = implicitly[Encoder[LocalDateTime]]
val decoder = implicitly[Decoder[LocalDateTime]]
val value = "2007/12/03 10:30"
val obj = LocalDateTime.parse(value, formatter)

encoder(obj) shouldBe Json.fromString(value)
decoder(Json.fromString(value).hcursor) shouldBe Right(obj)
}

test("LocalDateTime custom format with error handling") {
object TimeInstancesProtocol extends KebsCirce with TimeInstances {
val pattern = "yyyy/MM/dd HH:mm"
val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern(pattern)

override implicit val localDateTimeFormatter: InstanceConverter[LocalDateTime, String] =
new InstanceConverter[LocalDateTime, String] {
override def encode(obj: LocalDateTime): String = obj.format(formatter)
override def decode(value: String): LocalDateTime =
try {
LocalDateTime.parse(value, formatter)
} catch {
case e: DateTimeException =>
throw new IllegalArgumentException(
s"${classOf[LocalDateTime]} cannot be parsed from $value – should be in format $pattern",
e)
case e: Throwable => throw e
}
}
}
import TimeInstancesProtocol._

"implicitly[CaseClass1Rep[LocalDateTime, String]]" shouldNot typeCheck
"implicitly[CaseClass1Rep[String, LocalDateTime]]" shouldNot typeCheck

val encoder = implicitly[Encoder[LocalDateTime]]
val decoder = implicitly[Decoder[LocalDateTime]]
val value = "2007/12/03 10:30"
val obj = LocalDateTime.parse(value, formatter)

encoder(obj) shouldBe Json.fromString(value)
decoder(Json.fromString(value).hcursor) shouldBe Right(obj)
}
}
Loading

0 comments on commit 24c6c59

Please sign in to comment.