All Downloads are FREE. Search and download functionalities are using the official Maven repository.

muffin.interop.json.zio.codec.scala Maven / Gradle / Ivy

The newest version!
package muffin.interop.json.zio

import java.time.{Instant, LocalDateTime, ZoneId}

import zio.json.*
import zio.json.JsonDecoder.{JsonError, UnsafeJson}
import zio.json.ast.Json
import zio.json.internal.*

import muffin.codec.*
import muffin.error.MuffinError
import muffin.http.Body

object codec extends CodecLow {
  given NothingTo: JsonEncoder[Nothing] = UnitTo.asInstanceOf[JsonEncoder[Nothing]]

  given NothingFrom: JsonDecoder[Nothing] = UnitFrom.asInstanceOf[JsonDecoder[Nothing]]
}

trait CodecLow extends CodecSupport[JsonEncoder, JsonDecoder] {

  given EncodeTo[A: JsonEncoder]: Encode[A] = _.toJson

  given DecodeFrom[A: JsonDecoder]: Decode[A] = _.fromJson.left.map(MuffinError.Decoding(_))

  given UnitTo: JsonEncoder[Unit] = (_, _, _) => ()

  given UnitFrom: JsonDecoder[Unit] = (_, _) => ()

  given StringTo: JsonEncoder[String] = JsonEncoder.string

  given StringFrom: JsonDecoder[String] = JsonDecoder.string

  given ListTo[A: JsonEncoder]: JsonEncoder[List[A]] = JsonEncoder.list[A]

  given ListFrom[A: JsonDecoder]: JsonDecoder[List[A]] = JsonDecoder.list[A]

  given MapFrom[A: JsonDecoder]: JsonDecoder[Map[String, A]] = JsonDecoder.map[String, A]

  given BoolTo: JsonEncoder[Boolean] = JsonEncoder.boolean

  given BoolFrom: JsonDecoder[Boolean] = JsonDecoder.boolean

  given LocalDateTimeTo(using zone: ZoneId): JsonEncoder[LocalDateTime] =
    JsonEncoder[Long].contramap[LocalDateTime](
      _.atZone(zone).toEpochSecond
    )

  given LocalDateTimeFrom(using zone: ZoneId): JsonDecoder[LocalDateTime] =
    JsonDecoder[Long].map[LocalDateTime](t =>
      LocalDateTime.ofInstant(Instant.ofEpochSecond(t), zone)
    )

  given IntTo: JsonEncoder[Int] = JsonEncoder.int

  given IntFrom: JsonDecoder[Int] = JsonDecoder.int

  given LongTo: JsonEncoder[Long] = JsonEncoder.long

  given LongFrom: JsonDecoder[Long] = JsonDecoder.long

  given AnyTo: JsonEncoder[Any] = UnitTo.asInstanceOf[JsonEncoder[Any]]

  given AnyFrom: JsonDecoder[Any] = UnitFrom.asInstanceOf[JsonDecoder[Any]]

  given MapTo[K: JsonEncoder, V: JsonEncoder]: JsonEncoder[Map[K, V]] =
    (a: Map[K, V], indent: Option[Int], out: Write) => {
      given JsonFieldEncoder[K] = JsonEncoder[K].encodeJson(_, None).toString

      JsonEncoder.map[K, V].unsafeEncode(a, indent, out)
    }

  given OptionTo[A: JsonEncoder]: JsonEncoder[Option[A]] = JsonEncoder.option[A]

  given OptionFrom[A: JsonDecoder]: JsonDecoder[Option[A]] = JsonDecoder.option[A]

  def jsonRaw: JsonRequestRawBuilder[JsonEncoder, Body.RawJson] = new ZioBodyRawBuilder(Nil)

  def seal[T](f: T => JsonEncoder[T]): JsonEncoder[T] =
    (a: T, indent: Option[Int], out: Write) => f(a).unsafeEncode(a, indent, out)

  def json[T, X: JsonEncoder](f: T => X): JsonEncoder[T] =
    (a: T, indent: Option[Int], out: Write) => JsonEncoder[X].unsafeEncode(f(a), indent, out)

  def json[T]: JsonRequestBuilder[T, JsonEncoder] = new ZioJsonBuilder(Nil)

  def parsing[X: JsonDecoder, T](f: X => T): JsonDecoder[T] =
    (trace: List[JsonError], in: RetractReader) => f(JsonDecoder[X].unsafeDecode(trace, in))

  def parsing: JsonResponseBuilder[JsonDecoder, EmptyTuple] = new ZioResponseBuilder(Nil, Nil)

  private class ZioJsonBuilder[T](funs: List[T => String]) extends JsonRequestBuilder[T, JsonEncoder] {

    def field[X: JsonEncoder](fieldName: String, fieldValue: T => X): JsonRequestBuilder[T, JsonEncoder] = {
      val fun = (st: T) => s""""$fieldName": ${fieldValue(st).toJson}"""
      ZioJsonBuilder(fun :: funs)
    }

    def rawField(fieldName: String, fieldValue: T => String): JsonRequestBuilder[T, JsonEncoder] = {
      val fun = (st: T) => s""""$fieldName": ${fieldValue(st)}"""
      ZioJsonBuilder(fun :: funs)
    }

    def build: JsonEncoder[T] =
      (a: T, _: Option[Int], out: Write) => out.write(funs.map(_.apply(a)).mkString("{", ",", "}"))

  }

  private class ZioBodyRawBuilder(state: List[String]) extends JsonRequestRawBuilder[JsonEncoder, Body.RawJson] {

    def field[T: JsonEncoder](fieldName: String, fieldValue: T): JsonRequestRawBuilder[JsonEncoder, Body.RawJson] =
      ZioBodyRawBuilder(s""""$fieldName":${fieldValue.toJson}""" :: state)

    def build: Body.RawJson = Body.RawJson(state.mkString("{", ",", "}"))

  }

  private class ZioResponseBuilder[Params <: Tuple](stateNames: List[String], decoders: List[JsonDecoder[Any]])
    extends JsonResponseBuilder[JsonDecoder, Params] {

    override def field[X: JsonDecoder](name: String): JsonResponseBuilder[JsonDecoder, X *: Params] =
      new ZioResponseBuilder[X *: Params](
        name :: stateNames,
        summon[JsonDecoder[X]].asInstanceOf[JsonDecoder[Any]] :: decoders
      )

    override def internal[X: JsonDecoder](name: String): JsonResponseBuilder[JsonDecoder, X *: Params] =
      new ZioResponseBuilder[X *: Params](
        name :: stateNames,
        JsonDecoder.string.mapOrFail(_.fromJson).asInstanceOf[JsonDecoder[Any]] :: decoders
      )

    override def rawField(name: String): JsonResponseBuilder[JsonDecoder, Option[String] *: Params] =
      new ZioResponseBuilder[Option[String] *: Params](
        name :: stateNames,
        JsonDecoder[Option[zio.json.ast.Json]].map(_.map(_.toJson)).asInstanceOf[JsonDecoder[Any]] :: decoders
      )

    override def select[X](f: PartialFunction[Params, JsonDecoder[X]]): JsonDecoder[X] =
      decoder[X](rewind = true) {
        (x, trace, in) => f(x).unsafeDecode(trace, in)
      }

    override def build[X](f: PartialFunction[Params, X]): JsonDecoder[X] =
      decoder[X](rewind = false) {
        (x, _, _) => f(x)
      }

    private def decoder[X](rewind: Boolean)(f: (Params, List[JsonError], zio.MuffinRetract.Reader) => X) =
      new JsonDecoder[X] {

        val names: Array[String] = stateNames.toArray

        val len: Int = names.length

        val matrix: StringMatrix = new StringMatrix(names)

        val spans: Array[JsonError] = names.map(JsonError.ObjectAccess(_))

        lazy val tcs: Array[JsonDecoder[Any]] = decoders.toArray

        def unsafeDecode(trace: List[JsonError], _in: RetractReader): X = {
          val in = zio.MuffinRetract.reader(_in)

          Lexer.char(trace, in, '{')

          val ps: Array[Any] = Array.ofDim(len)

          if (Lexer.firstField(trace, in))
            while ({
              var trace_ = trace
              val field  = Lexer.field(trace, in, matrix)
              if (field != -1) {
                trace_ = spans(field) :: trace
                if (ps(field) != null)
                  throw UnsafeJson(JsonError.Message("duplicate") :: trace)
                ps(field) = tcs(field).unsafeDecode(trace_, in)
              } else
                Lexer.skipValue(trace_, in)

              Lexer.nextField(trace, in)
            })
              ()

          var i = 0
          while (i < len) {
            if (ps(i) == null) {
              tcs(i).unsafeDecodeMissing(spans(i) :: trace)
            }
            i += 1
          }

          val x = Tuple.fromArray(ps.map {
            case null => None
            case x    => x
          }).asInstanceOf[Params]

          if (rewind)
            in.rewind()

          f(x, trace, in)
        }

      }

  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy