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

smithy4s.internals.DocumentKeyDecoder.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.internals

import java.util.Base64
import java.util.UUID

import smithy4s._
import smithy4s.Document._
import smithy4s.schema.EnumValue
import smithy4s.schema.Primitive
import smithy4s.schema.Primitive._
import smithy4s.schema.SchemaVisitor
import smithy4s.schema.EnumTag

trait DocumentKeyDecoder[A] { self =>
  def apply(v: Document): Either[DocumentKeyDecoder.DecodeError, A] =
    try { Right(unsafeDecode(v)) }
    catch { case ex: DocumentKeyDecoder.DecodeError => Left(ex) }

  def unsafeDecode(v: Document): A

  def map[B](f: A => B): DocumentKeyDecoder[B] = new DocumentKeyDecoder[B] {
    def unsafeDecode(v: Document): B = f(self.unsafeDecode(v))
  }
}

object DocumentKeyDecoder {
  case class DecodeError(expectedType: String)
      extends RuntimeException("Cannot decode a key.", null)

  type OptDocumentKeyDecoder[A] = Option[DocumentKeyDecoder[A]]
  val trySchemaVisitor: SchemaVisitor[OptDocumentKeyDecoder] =
    new SchemaVisitor.Default[OptDocumentKeyDecoder] {
      def default[A]: OptDocumentKeyDecoder[A] = None

      def from[A](
          expectedType: String
      )(f: PartialFunction[Document, A]): OptDocumentKeyDecoder[A] =
        Some { doc =>
          if (f.isDefinedAt(doc)) f(doc)
          else throw DecodeError(expectedType)
        }
      def fromUnsafe[A](
          expectedType: String
      )(f: PartialFunction[Document, A]): OptDocumentKeyDecoder[A] =
        Some { doc =>
          if (f.isDefinedAt(doc)) {
            f(doc)
          } else {
            throw DecodeError(expectedType)
          }
        }

      override def primitive[P](
          shapeId: ShapeId,
          hints: Hints,
          tag: Primitive[P]
      ): OptDocumentKeyDecoder[P] = {
        val shortDesc = tag.schema(shapeId).compile(SchemaDescription)
        tag match {
          case PShort =>
            from(shortDesc) {
              case FlexibleNumber(bd) if bd.isValidShort => bd.shortValue
            }
          case PString => from(shortDesc) { case DString(value) => value }
          case PFloat =>
            from(shortDesc) { case FlexibleNumber(bd) =>
              bd.toFloat
            }
          case PDouble =>
            from(shortDesc) {
              case FlexibleNumber(bd) if bd.isDecimalDouble => bd.toDouble
            }

          case PTimestamp => None // not sure because we can encode

          case PBlob =>
            fromUnsafe(shortDesc) { case DString(string) =>
              Blob(Base64.getDecoder().decode(string))
            }
          case PBigInt =>
            from(shortDesc) {
              case FlexibleNumber(bd) if bd.isWhole => bd.toBigInt
            }
          case PUUID =>
            fromUnsafe(shortDesc) { case DString(string) =>
              UUID.fromString(string)
            }
          case PInt =>
            from(shortDesc) {
              case FlexibleNumber(bd) if bd.isValidInt => bd.intValue
            }
          case PBigDecimal =>
            from(shortDesc) { case FlexibleNumber(bd) =>
              bd
            }
          case PBoolean =>
            from(shortDesc) {
              case DBoolean(value)  => value
              case DString("true")  => true
              case DString("false") => false
            }
          case PLong =>
            from(shortDesc) {
              case FlexibleNumber(bd) if bd.isValidLong => bd.longValue
            }
          case PByte =>
            from(shortDesc) {
              case FlexibleNumber(bd) if bd.isValidByte => bd.toByte
            }

          case PDocument => None
        }
      }
      override def enumeration[E](
          shapeId: ShapeId,
          hints: Hints,
          tag: EnumTag[E],
          values: List[EnumValue[E]],
          total: E => EnumValue[E]
      ): OptDocumentKeyDecoder[E] = {
        val fromName = values.map(e => e.stringValue -> e.value).toMap
        val fromNum = values.map(e => e.intValue -> e.value).toMap
        val intVal = s"value in [${fromNum.keySet.mkString(", ")}]"
        val stringVal = s"value in [${fromName.keySet.mkString(", ")}]"
        tag match {
          case EnumTag.OpenIntEnum(unknown) =>
            from(intVal) {
              case DString(value) if value.toIntOption.isDefined =>
                val i = value.toInt
                fromNum.getOrElse(i, unknown(i))
            }
          case EnumTag.ClosedIntEnum =>
            from(intVal) {
              case DString(value)
                  if value.toIntOption.exists(fromNum.contains(_)) =>
                fromNum(value.toInt)
            }
          case EnumTag.OpenStringEnum(unknown) =>
            from(stringVal) { case DString(value) =>
              fromName.getOrElse(value, unknown(value))
            }
          case EnumTag.ClosedStringEnum =>
            from(stringVal) {
              case DString(value) if fromName.contains(value) => fromName(value)
            }
        }
      }

      override def biject[A, B](
          schema: Schema[A],
          bijection: Bijection[A, B]
      ): OptDocumentKeyDecoder[B] =
        apply(schema).map(_.map(bijection.to))

      override def refine[A, B](
          schema: Schema[A],
          to: Refinement[A, B]
      ): OptDocumentKeyDecoder[B] =
        apply(schema).map(_.map(to.asThrowingFunction))
    }

  object FlexibleNumber {
    def unapply(doc: Document): Option[BigDecimal] = doc match {
      case DNumber(value) => Some(value)
      case DString(value) =>
        try { Some(BigDecimal(value)) }
        catch { case _: Throwable => None }
      case _ => None
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy