diff --git a/build.sbt b/build.sbt index 6ebd126f..ae0b6df5 100644 --- a/build.sbt +++ b/build.sbt @@ -9,7 +9,7 @@ lazy val scala3 = "3.3.0" ThisBuild / scalaVersion := scala3 lazy val commonSettings = Seq( - version := "0.28.2", + version := "0.28.4", organization := "com.tethys-json", licenses := Seq("Apache-2.0" -> url("https://www.apache.org/licenses/LICENSE-2.0")), homepage := Some(url("https://github.com/tethys-json/tethys")), @@ -86,7 +86,7 @@ lazy val tethys = project.in(file(".")) crossScalaVersions := Seq.empty, commonSettings ) - .aggregate(core, `macro-derivation`, `jackson-211`, `jackson-212`, `jackson-213`, json4s, circe, refined, enumeratum) + .aggregate(core, `macro-derivation`, `jackson-211`, `jackson-212`, `jackson-213`, json4s, circe, refined, enumeratum, cats) lazy val modules = file("modules") @@ -106,6 +106,18 @@ lazy val core = project.in(modules / "core") libraryDependencies ++= addScalaReflect(scalaVersion.value) ) +lazy val cats = project.in(modules / "cats") + .settings(crossScalaSettings) + .settings(commonSettings) + .settings(testSettings) + .settings( + name := "tethys-cats", + libraryDependencies ++= Seq( + "org.typelevel" %% "cats-core" % "2.10.0", + ) + ) + .dependsOn(core) + lazy val `macro-derivation` = project.in(modules / "macro-derivation") .settings(crossScalaSettings) .settings(commonSettings) diff --git a/modules/cats/src/main/scala-2.12/tethys/cats/NonEmptySetReader.scala b/modules/cats/src/main/scala-2.12/tethys/cats/NonEmptySetReader.scala new file mode 100644 index 00000000..853f1aa2 --- /dev/null +++ b/modules/cats/src/main/scala-2.12/tethys/cats/NonEmptySetReader.scala @@ -0,0 +1,26 @@ +package tethys.cats + +import cats.data.NonEmptySet +import tethys.JsonReader +import tethys.readers.{FieldName, ReaderError} +import tethys.readers.tokens.TokenIterator + +import scala.collection.immutable.{Seq, SortedSet} + +trait NonEmptySetReader { + + implicit def readerForNes[T: JsonReader: Ordering] + : JsonReader[NonEmptySet[T]] = + new JsonReader[NonEmptySet[T]] { + override def read( + it: TokenIterator + )(implicit fieldName: FieldName): NonEmptySet[T] = + NonEmptySet.fromSet(SortedSet(JsonReader[Seq[T]].read(it): _*)) match { + case Some(value) => value + case None => + ReaderError.wrongJson( + s"Seq is empty and can't be converted to NonEmptySet" + ) + } + } +} diff --git a/modules/cats/src/main/scala-2.13+/tethys/cats/NonEmptySetReader.scala b/modules/cats/src/main/scala-2.13+/tethys/cats/NonEmptySetReader.scala new file mode 100644 index 00000000..e7883f4b --- /dev/null +++ b/modules/cats/src/main/scala-2.13+/tethys/cats/NonEmptySetReader.scala @@ -0,0 +1,26 @@ +package tethys.cats + +import cats.data.NonEmptySet +import tethys.JsonReader +import tethys.readers.{FieldName, ReaderError} +import tethys.readers.tokens.TokenIterator + +import scala.collection.immutable.SortedSet + +trait NonEmptySetReader { + + implicit def readerForNes[T: JsonReader: Ordering] + : JsonReader[NonEmptySet[T]] = + new JsonReader[NonEmptySet[T]] { + override def read( + it: TokenIterator + )(implicit fieldName: FieldName): NonEmptySet[T] = + NonEmptySet.fromSet(SortedSet.from(JsonReader[Seq[T]].read(it))) match { + case Some(value) => value + case None => + ReaderError.wrongJson( + s"Seq is empty and can't be converted to NonEmptySet" + ) + } + } +} diff --git a/modules/cats/src/main/scala-3/tethys.cats/NonEmptySetReader.scala b/modules/cats/src/main/scala-3/tethys.cats/NonEmptySetReader.scala new file mode 100644 index 00000000..e7883f4b --- /dev/null +++ b/modules/cats/src/main/scala-3/tethys.cats/NonEmptySetReader.scala @@ -0,0 +1,26 @@ +package tethys.cats + +import cats.data.NonEmptySet +import tethys.JsonReader +import tethys.readers.{FieldName, ReaderError} +import tethys.readers.tokens.TokenIterator + +import scala.collection.immutable.SortedSet + +trait NonEmptySetReader { + + implicit def readerForNes[T: JsonReader: Ordering] + : JsonReader[NonEmptySet[T]] = + new JsonReader[NonEmptySet[T]] { + override def read( + it: TokenIterator + )(implicit fieldName: FieldName): NonEmptySet[T] = + NonEmptySet.fromSet(SortedSet.from(JsonReader[Seq[T]].read(it))) match { + case Some(value) => value + case None => + ReaderError.wrongJson( + s"Seq is empty and can't be converted to NonEmptySet" + ) + } + } +} diff --git a/modules/cats/src/main/scala/tethys/cats/instances.scala b/modules/cats/src/main/scala/tethys/cats/instances.scala new file mode 100644 index 00000000..94e89b30 --- /dev/null +++ b/modules/cats/src/main/scala/tethys/cats/instances.scala @@ -0,0 +1,6 @@ +package tethys.cats + +import tethys.cats.readers.CatsReaders +import tethys.cats.writers.CatsWriters + +object instances extends CatsReaders with CatsWriters with NonEmptySetReader diff --git a/modules/cats/src/main/scala/tethys/cats/readers/CatsReaders.scala b/modules/cats/src/main/scala/tethys/cats/readers/CatsReaders.scala new file mode 100644 index 00000000..4c2031ad --- /dev/null +++ b/modules/cats/src/main/scala/tethys/cats/readers/CatsReaders.scala @@ -0,0 +1,56 @@ +package tethys.cats.readers + +import cats.data._ +import tethys.readers.tokens.TokenIterator +import tethys.readers.{FieldName, ReaderError} +import tethys.JsonReader +import tethys.JsonReader.iterableReader + +trait CatsReaders { + + implicit def readerForNel[T: JsonReader]: JsonReader[NonEmptyList[T]] = + new JsonReader[NonEmptyList[T]] { + override def read( + it: TokenIterator + )(implicit fieldName: FieldName): NonEmptyList[T] = + NonEmptyList.fromList(JsonReader[List[T]].read(it)) match { + case Some(value) => value + case None => + ReaderError.wrongJson( + s"List is empty and can't be converted to NonEmptyList" + ) + } + } + + implicit def readerForNev[T: JsonReader]: JsonReader[NonEmptyVector[T]] = + new JsonReader[NonEmptyVector[T]] { + override def read( + it: TokenIterator + )(implicit fieldName: FieldName): NonEmptyVector[T] = + NonEmptyVector.fromVector(JsonReader[Vector[T]].read(it)) match { + case Some(value) => value + case None => + ReaderError.wrongJson( + s"Vector is empty and can't be converted to NonEmptyVector" + ) + } + } + + implicit def readerForChain[T: JsonReader]: JsonReader[Chain[T]] = + JsonReader[Seq[T]].map(Chain.fromIterableOnce) + + implicit def readerForNec[T: JsonReader]: JsonReader[NonEmptyChain[T]] = + new JsonReader[NonEmptyChain[T]] { + override def read( + it: TokenIterator + )(implicit fieldName: FieldName): NonEmptyChain[T] = + NonEmptyChain.fromChain(JsonReader[Chain[T]].read(it)) match { + case Some(value) => value + case None => + ReaderError.wrongJson( + s"Chain is empty and can't be converted to NonEmptyChain" + ) + } + } + +} diff --git a/modules/cats/src/main/scala/tethys/cats/writers/CatsWriters.scala b/modules/cats/src/main/scala/tethys/cats/writers/CatsWriters.scala new file mode 100644 index 00000000..a14efa56 --- /dev/null +++ b/modules/cats/src/main/scala/tethys/cats/writers/CatsWriters.scala @@ -0,0 +1,21 @@ +package tethys.cats.writers + +import cats.data._ +import tethys.JsonWriter + +trait CatsWriters { + implicit def writerForNev[T: JsonWriter]: JsonWriter[NonEmptyVector[T]] = + JsonWriter[Vector[T]].contramap(_.toVector) + + implicit def writerForNel[T: JsonWriter]: JsonWriter[NonEmptyList[T]] = + JsonWriter[List[T]].contramap(_.toList) + + implicit def writerForNes[T: JsonWriter]: JsonWriter[NonEmptySet[T]] = + JsonWriter[Set[T]].contramap(_.toSortedSet) + + implicit def writerForChain[T: JsonWriter]: JsonWriter[Chain[T]] = + JsonWriter[List[T]].contramap(_.toList) + + implicit def writerForNec[T: JsonWriter]: JsonWriter[NonEmptyChain[T]] = + JsonWriter[Chain[T]].contramap(_.toChain) +} diff --git a/modules/cats/src/test/scala/tethys/cats/CatsSupportTests.scala b/modules/cats/src/test/scala/tethys/cats/CatsSupportTests.scala new file mode 100644 index 00000000..7bd0c36d --- /dev/null +++ b/modules/cats/src/test/scala/tethys/cats/CatsSupportTests.scala @@ -0,0 +1,44 @@ +package tethys.cats + +import cats.data._ +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import tethys.commons.TokenNode._ +import instances._ +import tethys.readers.ReaderError +import tethys.writers.tokens.SimpleTokenWriter._ + +class CatsSupportTests extends AnyFlatSpec with Matchers { + val nev: NonEmptyVector[String] = NonEmptyVector.of("a", "b") + val nel: NonEmptyList[Int] = NonEmptyList.of(1, 2) + val nes: NonEmptySet[Int] = NonEmptySet.of(1, 2, 3, 4) + val chain: Chain[Int] = Chain.fromIterableOnce(Seq(1, 2, 3)) + val nec: NonEmptyChain[String] = NonEmptyChain.of("a", "b", "c") + + behavior of "CatsWriters" + it should "write non-empty" in { + nev.asTokenList shouldBe arr("a", "b") + nel.asTokenList shouldBe arr(1, 2) + nes.asTokenList shouldBe arr(1, 2, 3, 4) + chain.asTokenList shouldBe arr(1, 2, 3) + nec.asTokenList shouldBe arr("a", "b", "c") + } + + behavior of "CatsReaders" + it should "read non-empty" in { + nev shouldBe arr("a", "b").tokensAs[NonEmptyVector[String]] + assertThrows[ReaderError](Nil.tokensAs[NonEmptyVector[String]]) + + nel shouldBe arr(1, 2).tokensAs[NonEmptyList[Int]] + assertThrows[ReaderError](Nil.tokensAs[NonEmptyList[Int]]) + + nes shouldBe arr(1, 2, 3, 4).tokensAs[NonEmptySet[Int]] + nes shouldBe arr(4, 4, 1, 3, 3, 2).tokensAs[NonEmptySet[Int]] + assertThrows[ReaderError](Nil.tokensAs[NonEmptySet[Int]]) + + chain shouldBe arr(1, 2, 3).tokensAs[Chain[Int]] + + nec shouldBe arr("a", "b", "c").tokensAs[NonEmptyChain[String]] + assertThrows[ReaderError](Nil.tokensAs[NonEmptyChain[String]]) + } +}