Skip to content

Commit

Permalink
Merge branch 'master' into update/scodec-bits-1.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
cornerman authored Jun 10, 2024
2 parents b5906cf + 75e9b66 commit 42e9ed4
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 16 deletions.
41 changes: 35 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,10 @@ This will not bloat your project. It only has an effect if you explicitly depend

Get latest release:
```scala
libraryDependencies += "com.github.cornerman" %%% "chameleon" % "0.3.5"
libraryDependencies += "com.github.cornerman" %%% "chameleon" % "0.4.0"
```

Or get development snapshots via jitpack:
```scala
resolvers += "jitpack" at "https://jitpack.io"
libraryDependencies += "com.github.cornerman.chameleon" %%% "chameleon" % "master-SNAPSHOT"
```
We additionally publish sonatype snapshots for every commit on master.

# Usage

Expand All @@ -54,6 +50,39 @@ Have typeclasses for cats (`Contravariant`, `Functor`, `Invariant`):
import chameleon.ext.cats._
```

## http4s

We have an extra package to integrate `http4s` with chameleon `Serializer` and `Deserializer`. Specifically to create `EntityEncoder` and `EntityDecoder`.

Usage:
```scala
libraryDependencies += "com.github.cornerman" %%% "chameleon-http4s" % "0.4.0"
```

To work with json inside your API:
```scala
import chameleon.ext.http4s.JsonStringCodec.*
import chameleon.ext.upickle.*

// serialize case class as json response
Ok(json(MyCaseClass("hallo")))

// deserialize json request into case class
jsonAs[MyCaseClass](someRequest)
```

You can also use the auto import, which provides implicit `EntityEncoder` / `EntityDecoder` for all types with `Serializer` / `Deserializer`. This should only be imported when exclusively working with json.
```scala
import chameleon.ext.http4s.JsonStringCodec.auto.*
import chameleon.ext.upickle.*

// serialize
Ok(MyCaseClass("hallo"))

// deserialize
someRequest.as[MyCaseClass]
```

# Motivation

Say, you want to write a library that needs serialization but abstracts over the type of serialization. Then you might end up with something like this:
Expand Down
11 changes: 11 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,14 @@ lazy val chameleon = crossProject(JSPlatform, JVMPlatform)
)
})
)

lazy val http4s = crossProject(JSPlatform, JVMPlatform)
.crossType(CrossType.Pure)
.dependsOn(chameleon)
.settings(commonSettings)
.settings(
name := "chameleon-http4s",
libraryDependencies ++=
Deps.http4s.value ::
Nil,
)
13 changes: 12 additions & 1 deletion chameleon/src/main/scala/ext/jsoniter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import com.github.plokhotnyuk.jsoniter_scala.core

import scala.util.{Try, Success, Failure}

//TODO with CodecMakerConfig
object jsoniter {
implicit def jsoniterSerializerDeserializer2[T: core.JsonValueCodec]: SerializerDeserializer[T, String] = new Serializer[T, String] with Deserializer[T, String] {
implicit def jsoniterSerializerDeserializerString[T: core.JsonValueCodec]: SerializerDeserializer[T, String] = new Serializer[T, String] with Deserializer[T, String] {
override def serialize(arg: T): String = core.writeToString(arg)

override def deserialize(arg: String): Either[Throwable, T] =
Expand All @@ -15,4 +16,14 @@ object jsoniter {
case Failure(t) => Left(t)
}
}

implicit def jsoniterSerializerDeserializerByteArray[T: core.JsonValueCodec]: SerializerDeserializer[T, Array[Byte]] = new Serializer[T, Array[Byte]] with Deserializer[T, Array[Byte]] {
override def serialize(arg: T): Array[Byte] = core.writeToArray(arg)

override def deserialize(arg: Array[Byte]): Either[Throwable, T] =
Try(core.readFromArray[T](arg)) match {
case Success(arg) => Right(arg)
case Failure(t) => Left(t)
}
}
}
11 changes: 10 additions & 1 deletion chameleon/src/main/scala/ext/scalapb.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import java.nio.ByteBuffer
import scala.util.{Failure, Success}

object scalapb {
implicit def scalapbSerializerDeserializer[T <: GeneratedMessage](implicit companion: GeneratedMessageCompanion[T]): SerializerDeserializer[T, ByteBuffer] = new Serializer[T, ByteBuffer] with Deserializer[T, ByteBuffer] {
implicit def scalapbSerializerDeserializerByteBuffer[T <: GeneratedMessage](implicit companion: GeneratedMessageCompanion[T]): SerializerDeserializer[T, ByteBuffer] = new Serializer[T, ByteBuffer] with Deserializer[T, ByteBuffer] {
override def serialize(arg: T): ByteBuffer = ByteBuffer.wrap(companion.toByteArray(arg))
override def deserialize(arg: ByteBuffer): Either[Throwable, T] = {
val bytes = new Array[Byte](arg.remaining)
Expand All @@ -17,4 +17,13 @@ object scalapb {
}
}
}

implicit def scalapbSerializerDeserializerByteArray[T <: GeneratedMessage](implicit companion: GeneratedMessageCompanion[T]): SerializerDeserializer[T, Array[Byte]] = new Serializer[T, Array[Byte]] with Deserializer[T, Array[Byte]] {
override def serialize(arg: T): Array[Byte] = companion.toByteArray(arg)
override def deserialize(bytes: Array[Byte]): Either[Throwable, T] =
companion.validate(bytes) match {
case Success(arg) => Right(arg)
case Failure(t) => Left(t)
}
}
}
11 changes: 10 additions & 1 deletion chameleon/src/main/scala/ext/scodec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,21 @@ import java.nio.ByteBuffer
object scodec {
case class DeserializeException(msg: String) extends Exception(msg)

implicit def scodecSerializerDeserializer[T : Codec]: SerializerDeserializer[T, ByteBuffer] = new Serializer[T, ByteBuffer] with Deserializer[T, ByteBuffer] {
implicit def scodecSerializerDeserializerByteBuffer[T : Codec]: SerializerDeserializer[T, ByteBuffer] = new Serializer[T, ByteBuffer] with Deserializer[T, ByteBuffer] {
override def serialize(arg: T): ByteBuffer = Codec[T].encode(arg).require.toByteBuffer
override def deserialize(arg: ByteBuffer): Either[Throwable, T] =
Codec[T].decode(BitVector(arg)).toEither match {
case Right(result) => Right(result.value)
case Left(err) => Left(DeserializeException(err.message))
}
}

implicit def scodecSerializerDeserializerByteArray[T : Codec]: SerializerDeserializer[T, Array[Byte]] = new Serializer[T, Array[Byte]] with Deserializer[T, Array[Byte]] {
override def serialize(arg: T): Array[Byte] = Codec[T].encode(arg).require.toByteArray
override def deserialize(arg: Array[Byte]): Either[Throwable, T] =
Codec[T].decode(BitVector(arg)).toEither match {
case Right(result) => Right(result.value)
case Left(err) => Left(DeserializeException(err.message))
}
}
}
29 changes: 23 additions & 6 deletions chameleon/src/main/scala/ext/upickle.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,39 @@ import _root_.upickle.default
import scala.util.{Try, Success, Failure}

trait upickle { api: Api =>
implicit def upickleSerializer[T: api.Writer]: chameleon.Serializer[T, String] = upickle.upickleSerializerOfApi(api)
implicit def upickleDeserializer[T: api.Reader]: chameleon.Deserializer[T, String] = upickle.upickleDeserializerOfApi(api)
implicit def upickleSerializerString[T: api.Writer]: chameleon.Serializer[T, String] = upickle.upickleSerializerStringOfApi(api)
implicit def upickleDeserializerString[T: api.Reader]: chameleon.Deserializer[T, String] = upickle.upickleDeserializerStringOfApi(api)

implicit def upickleSerializerByteArray[T: api.Writer]: chameleon.Serializer[T, Array[Byte]] = upickle.upickleSerializerByteArrayOfApi(api)
implicit def upickleDeserializerByteArray[T: api.Reader]: chameleon.Deserializer[T, Array[Byte]] = upickle.upickleDeserializerByteArrayOfApi(api)
}

object upickle {
def upickleSerializerOfApi[T](api: Api)(implicit writer: api.Writer[T]): Serializer[T, String] = new Serializer[T, String] {
def upickleSerializerStringOfApi[T](api: Api)(implicit writer: api.Writer[T]): Serializer[T, String] = new Serializer[T, String] {
override def serialize(arg: T): String = api.write(arg)
}
def upickleDeserializerOfApi[T](api: Api)(implicit reader: api.Reader[T]): Deserializer[T, String] = new Deserializer[T, String] {
def upickleDeserializerStringOfApi[T](api: Api)(implicit reader: api.Reader[T]): Deserializer[T, String] = new Deserializer[T, String] {
override def deserialize(arg: String): Either[Throwable, T] =
Try(api.read[T](arg)) match {
case Success(arg) => Right(arg)
case Failure(t) => Left(t)
}
}

implicit def upickleSerializer[T: default.Writer]: Serializer[T, String] = upickleSerializerOfApi(default)
implicit def upickleDeserializer[T : default.Reader]: Deserializer[T, String] = upickleDeserializerOfApi(default)
def upickleSerializerByteArrayOfApi[T](api: Api)(implicit writer: api.Writer[T]): Serializer[T, Array[Byte]] = new Serializer[T, Array[Byte]] {
override def serialize(arg: T): Array[Byte] = api.writeBinary(arg)
}
def upickleDeserializerByteArrayOfApi[T](api: Api)(implicit reader: api.Reader[T]): Deserializer[T, Array[Byte]] = new Deserializer[T, Array[Byte]] {
override def deserialize(arg: Array[Byte]): Either[Throwable, T] =
Try(api.readBinary[T](arg)) match {
case Success(arg) => Right(arg)
case Failure(t) => Left(t)
}
}

implicit def upickleSerializerString[T: default.Writer]: Serializer[T, String] = upickleSerializerStringOfApi(default)
implicit def upickleDeserializerString[T : default.Reader]: Deserializer[T, String] = upickleDeserializerStringOfApi(default)

implicit def upickleSerializerByteArray[T: default.Writer]: Serializer[T, Array[Byte]] = upickleSerializerByteArrayOfApi(default)
implicit def upickleDeserializerByteArray[T : default.Reader]: Deserializer[T, Array[Byte]] = upickleDeserializerByteArrayOfApi(default)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package chameleon.ext.http4s

import cats.effect.Concurrent
import cats.implicits._
import chameleon.{Deserializer, Serializer}
import org.http4s.headers.`Content-Type`
import org.http4s.{DecodeResult, EntityDecoder, EntityEncoder, Headers, MalformedMessageBodyFailure, Media, MediaType}

object JsonByteArrayCodec {
def jsonDecoderOf[F[_]: Concurrent, A](implicit deserializer: Deserializer[A, Array[Byte]]): EntityDecoder[F, A] =
EntityDecoder.decodeBy(MediaType.application.json) { media =>
EntityDecoder.collectBinary(media).flatMap { chunk =>
DecodeResult[F, A](Concurrent[F].pure(
deserializer.deserialize(chunk.toArray).leftMap(error => MalformedMessageBodyFailure("Invalid JSON", Some(error)))
))
}
}

def jsonEncoderOf[F[_], A](implicit serializer: Serializer[A, Array[Byte]]): EntityEncoder[F, A] =
EntityEncoder.encodeBy[F, A](Headers(`Content-Type`(MediaType.application.json))) {
EntityEncoder.byteArrayEncoder[F].contramap(serializer.serialize).toEntity(_)
}

case class json[A](value: A)

def jsonAs[A] = new JsonAsPartialApplied[A]
class JsonAsPartialApplied[A] {
def apply[F[_]: Concurrent](media: Media[F])(implicit deserializer: Deserializer[A, Array[Byte]]): F[A] = media.as[A](implicitly, jsonDecoderOf[F, A])
}

implicit def jsonEntityDecoderJson[F[_]: Concurrent, A](implicit deserializer: Deserializer[A, Array[Byte]]): EntityDecoder[F, json[A]] =
jsonDecoderOf[F, A].map(json(_))

implicit def jsonEntityEncoderJson[F[_], A](implicit serializer: Serializer[A, Array[Byte]]): EntityEncoder[F, json[A]] =
jsonEncoderOf[F, A].contramap(_.value)

object auto {
implicit def jsonEntityDecoder[F[_] : Concurrent, A](implicit deserializer: Deserializer[A, Array[Byte]]): EntityDecoder[F, A] = jsonDecoderOf[F, A]

implicit def jsonEntityEncoder[F[_], A](implicit serializer: Serializer[A, Array[Byte]]): EntityEncoder[F, A] = jsonEncoderOf[F, A]
}
}
42 changes: 42 additions & 0 deletions http4s/src/main/scala/chameleon/ext/http4s/JsonStringCodec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package chameleon.ext.http4s

import cats.effect.Concurrent
import cats.implicits._
import chameleon.{Deserializer, Serializer}
import org.http4s.headers.`Content-Type`
import org.http4s.{DecodeResult, EntityDecoder, EntityEncoder, Headers, MalformedMessageBodyFailure, Media, MediaType}

object JsonStringCodec {
def jsonDecoderOf[F[_] : Concurrent, A](implicit deserializer: Deserializer[A, String]): EntityDecoder[F, A] =
EntityDecoder.decodeBy(MediaType.application.json) { media =>
DecodeResult[F, A](
EntityDecoder.decodeText(media).map { json =>
deserializer.deserialize(json).leftMap(error => MalformedMessageBodyFailure("Invalid JSON", Some(error)))
}
)
}

def jsonEncoderOf[F[_], A](implicit serializer: Serializer[A, String]): EntityEncoder[F, A] =
EntityEncoder.encodeBy[F, A](Headers(`Content-Type`(MediaType.application.json))) {
EntityEncoder.stringEncoder[F].contramap(serializer.serialize).toEntity(_)
}

case class json[A](value: A)

def jsonAs[A] = new JsonAsPartialApplied[A]
class JsonAsPartialApplied[A] {
def apply[F[_]: Concurrent](media: Media[F])(implicit deserializer: Deserializer[A, String]): F[A] = media.as[A](implicitly, jsonDecoderOf[F, A])
}

implicit def jsonEntityDecoderJson[F[_]: Concurrent, A](implicit serializer: Deserializer[A, String]): EntityDecoder[F, json[A]] =
jsonDecoderOf[F, A].map(json(_))

implicit def jsonEntityEncoderJson[F[_], A](implicit serializer: Serializer[A, String]): EntityEncoder[F, json[A]] =
jsonEncoderOf[F, A].contramap(_.value)

object auto {
implicit def jsonEntityDecoder[F[_] : Concurrent, A](implicit deserializer: Deserializer[A, String]): EntityDecoder[F, A] = jsonDecoderOf[F, A]

implicit def jsonEntityEncoder[F[_], A](implicit serializer: Serializer[A, String]): EntityEncoder[F, A] = jsonEncoderOf[F, A]
}
}
4 changes: 3 additions & 1 deletion project/Deps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ object Deps {
}
val scodec = new {
val core = dep("org.scodec" %%% "scodec-core" % "1.11.10")
val core2 = dep("org.scodec" %%% "scodec-core" % "2.2.2")
val core2 = dep("org.scodec" %%% "scodec-core" % "2.3.0")
val bits = dep("org.scodec" %%% "scodec-bits" % "1.2.0")
}
val upickle = dep("com.lihaoyi" %%% "upickle" % "3.3.1")
val jsoniter = dep("com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.30.1")
val zioJson = dep("dev.zio" %%% "zio-json" % "0.6.2")

val http4s = dep("org.http4s" %%% "http4s-core" % "0.23.24")
}

0 comments on commit 42e9ed4

Please sign in to comment.