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

smithy4s.Document.scala Maven / Gradle / Ivy

There is a newer version: 0.19.0-41-91762fb
Show newest version
/*
 *  Copyright 2021-2024 Disney Streaming
 *
 *  Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *     https://disneystreaming.github.io/TOST-1.0.txt
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package smithy4s

import smithy4s.Document._
import smithy4s.schema.CachedSchemaCompiler
import internals.DocumentDecoderSchemaVisitor
import internals.DocumentEncoderSchemaVisitor
import smithy4s.codecs.PayloadError

/**
  * A json-like free-form structure serving as a model for
  * the Document datatype in smithy.
  */
sealed trait Document extends Product with Serializable {

  def decode[A](implicit
      decoder: Document.Decoder[A]
  ): Either[PayloadError, A] =
    decoder.decode(this)

  override def toString(): String = this.show

  def name: String = this match {
    case DNumber(_)  => "Number"
    case DString(_)  => "String"
    case DBoolean(_) => "Boolean"
    case DNull       => "Null"
    case DArray(_)   => "Array"
    case DObject(_)  => "Object"
  }

  /**
    * Toy renderer that does not comply the json specification :
    * strings aren't escaped and keys aren't quoted.
    * Do not use for any real purpose other than debugging.
    */
  def show: String = this match {
    case DNumber(value) => {
      if (value.isValidLong) value.toLong.toString()
      else value.toString()
    }
    case DBoolean(value) => value.toString
    case DString(value)  => s""""$value""""
    case DNull           => "null"
    case DArray(value)   => value.map(_.show).mkString("[", ", ", "]")
    case DObject(value) =>
      value.map { case (k, v) => k + "=" + v.show }.mkString("{", ", ", "}")
  }

}

object Document {

  def encode[A](a: A)(implicit encoder: Encoder[A]): Document =
    encoder.encode(a)

  def decode[A](document: Document)(implicit
      decoder: Decoder[A]
  ): Either[PayloadError, A] =
    decoder.decode(document)

  case class DNumber(value: BigDecimal) extends Document
  case class DString(value: String) extends Document
  case class DBoolean(value: Boolean) extends Document
  case object DNull extends Document
  case class DArray(value: IndexedSeq[Document]) extends Document
  case class DObject(value: Map[String, Document]) extends Document

  def fromString(str: String): Document = DString(str)
  def fromInt(int: Int): Document = DNumber(BigDecimal(int))
  def fromLong(long: Long): Document = DNumber(BigDecimal(long))
  def fromDouble(double: Double): Document = DNumber(BigDecimal(double))
  def fromBigDecimal(bigDecimal: BigDecimal): Document = DNumber(bigDecimal)
  def fromBoolean(bool: Boolean): Document = DBoolean(bool)
  def array(values: Document*): Document = DArray(values.toIndexedSeq)
  def array(values: Iterable[Document]): Document = DArray(
    IndexedSeq.newBuilder.++=(values).result()
  )
  def obj(kv: Iterable[(String, Document)]): Document = DObject(
    Map(kv.toSeq: _*)
  )
  def obj(kv: (String, Document)*): Document = DObject(Map(kv: _*))
  def nullDoc: Document = DNull

  trait Encoder[A] {
    def encode(a: A): Document
  }

  trait EncoderCompiler extends CachedSchemaCompiler[Encoder] {
    def withExplicitDefaultsEncoding(
        explicitDefaultsEncoding: Boolean
    ): EncoderCompiler
  }

  object Encoder
      extends CachedEncoderCompilerImpl(explicitDefaultsEncoding = false)

  private[smithy4s] class CachedEncoderCompilerImpl(
      explicitDefaultsEncoding: Boolean
  ) extends CachedSchemaCompiler.DerivingImpl[Encoder]
      with EncoderCompiler {

    protected type Aux[A] = internals.DocumentEncoder[A]

    def fromSchema[A](
        schema: Schema[A],
        cache: Cache
    ): Encoder[A] = {
      val makeEncoder =
        schema.compile(
          new DocumentEncoderSchemaVisitor(cache, explicitDefaultsEncoding)
        )
      new Encoder[A] {
        def encode(a: A): Document = {
          makeEncoder.apply(a)
        }
      }
    }

    def withExplicitDefaultsEncoding(
        explicitDefaultsEncoding: Boolean
    ): EncoderCompiler = new CachedEncoderCompilerImpl(
      explicitDefaultsEncoding = explicitDefaultsEncoding
    )
  }

  type Decoder[A] =
    smithy4s.codecs.Decoder[Either[PayloadError, *], Document, A]

  implicit def decoderFromSchema[A: Schema]: Decoder[A] =
    Decoder.derivedImplicitInstance

  object Decoder extends CachedSchemaCompiler.DerivingImpl[Decoder] {

    protected type Aux[A] = internals.DocumentDecoder[A]

    def fromSchema[A](
        schema: Schema[A],
        cache: Cache
    ): Decoder[A] = {
      val decodeFunction =
        schema.compile(new DocumentDecoderSchemaVisitor(cache))
      new Decoder[A] {
        def decode(a: Document): Either[PayloadError, A] =
          try { Right(decodeFunction(Nil, a)) }
          catch {
            case e: PayloadError => Left(e)
          }
      }
    }
  }

  // scalafmt: { maxColumn = 120 }
  object syntax {
    implicit def intConversion(int: Int): Document = Document.fromInt(int)
    implicit def longConversion(long: Long): Document = Document.fromLong(long)
    implicit def booleanConversion(boolean: Boolean): Document = Document.fromBoolean(boolean)
    implicit def doubleConversion(double: Double): Document = Document.fromDouble(double)
    implicit def fromString(string: String): Document = Document.fromString(string)
    implicit def iterableConversion(iterable: Iterable[Document]): Document = Document.array(iterable)
    implicit def fromSchema[A: Schema](a: A): Document = Document.Encoder.fromSchema(implicitly[Schema[A]]).encode(a)
    def obj(kv: (String, Document)*): Document = Document.obj(kv)
    def array(kv: Document*): Document = Document.array(kv)
    def nullDoc: Document = Document.nullDoc
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy