Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add readers and writers instances for cats non-empty collections #284

Merged
merged 4 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
Expand Down Expand Up @@ -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")

Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
)
}
}
}
Original file line number Diff line number Diff line change
@@ -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"
)
}
}
}
26 changes: 26 additions & 0 deletions modules/cats/src/main/scala-3/tethys.cats/NonEmptySetReader.scala
Original file line number Diff line number Diff line change
@@ -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"
)
}
}
}
6 changes: 6 additions & 0 deletions modules/cats/src/main/scala/tethys/cats/instances.scala
Original file line number Diff line number Diff line change
@@ -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
56 changes: 56 additions & 0 deletions modules/cats/src/main/scala/tethys/cats/readers/CatsReaders.scala
Original file line number Diff line number Diff line change
@@ -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"
)
}
}

}
21 changes: 21 additions & 0 deletions modules/cats/src/main/scala/tethys/cats/writers/CatsWriters.scala
Original file line number Diff line number Diff line change
@@ -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)
}
44 changes: 44 additions & 0 deletions modules/cats/src/test/scala/tethys/cats/CatsSupportTests.scala
Original file line number Diff line number Diff line change
@@ -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]])
}
}
Loading