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

smithy4s.Hints.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

/**
  * A hint is an arbitrary piece of data that can be added to a schema,
  * at the struct level, or at the field/member level.
  *
  * You can think of it as an annotation that can communicate
  * additional information to encoders/decoders (for instance, a change
  * in a label, a regex pattern some string should abide by, a range, etc)
  *
  * This `Hints` interface is a container for hints.
  *
  * Under the hood, the hints are composed of two maps : one for member-level hints,
  * one for target-level hints. Member-level hints typically hold values corresponding
  * to member traits, whereas target hints hold values corresponding to normal data shapes.
  */
trait Hints {
  def isEmpty: Boolean
  def all: Iterable[Hints.Binding]

  def memberHintsMap: Map[ShapeId, Hints.Binding]
  def targetHintsMap: Map[ShapeId, Hints.Binding]

  /**
    * Returns a map of hints from both level, the member-level having priority
    * over the target-level one.
    */
  def toMap: Map[ShapeId, Hints.Binding]

  def get[A](implicit key: ShapeTag[A]): Option[A]
  final def has[A](implicit key: ShapeTag[A]): Boolean = this.get[A].isDefined
  final def get[A](key: ShapeTag.Has[A]): Option[A] = get(key.getTag)
  final def get[T](nt: Newtype[T]): Option[nt.Type] = get(nt.tag)
  final def filter(predicate: Hint => Boolean): Hints =
    Hints.fromSeq(all.filter(predicate).toSeq)
  final def filterNot(predicate: Hint => Boolean): Hints =
    filter(hint => !predicate(hint))

  /**
    *  Concatenates two set of hints. The levels are concatenated independently.
    */
  def ++(other: Hints): Hints

  /**
    * Add hints to the member-level.
    */
  def addMemberHints(hints: Hints): Hints

  /**
    * Add hints to the member-level.
    */
  final def addMemberHints(hints: Hint*): Hints = addMemberHints(
    Hints(hints: _*)
  )

  /**
   *  Add hints to the member level
   */
  final def add(hints: Hint*): Hints = addMemberHints(hints: _*)

  /**
    * Add hints to the target-level.
    */
  def addTargetHints(hints: Hints): Hints

  /**
    * Add hints to the target-level.
    */
  final def addTargetHints(hints: Hint*): Hints = addTargetHints(
    Hints(hints: _*)
  )

  /**
    * Provides an instance of hints containing only the member-level hints.
    */
  def memberHints: Hints

  /**
    * Provides an instance of hints containing only the target-level hints.
    */
  def targetHints: Hints

  /**
   * Adds a new hint provided a specific hint is present
   */
  final def expand[A, B](f: A => Hint)(implicit key: ShapeTag[A]): Hints =
    get(key) match {
      case Some(a) => addMemberHints(f(a))
      case None    => this
    }

}

object Hints {

  val empty: Hints = new Impl(Map.empty, Map.empty)

  def apply(bindings: Hint*): Hints =
    fromSeq(bindings)

  def dynamic(bindings: (String, Document)*): Hints =
    fromSeq(bindings.map { case (k, v) => ShapeId.parse(k) -> v }.collect {
      case (Some(id), v) => Binding.DynamicBinding(id, v)
    })

  implicit final class HintsLazyOps(underlying: => Hints) {

    /**
     * Suspends the evaluation of the hints until they are needed.
     * This is needed to avoid a deadlock in case of concurrent initialization of the hints' classes: see #537.
     */
    def lazily: Hints = Hints.LazyHints(Lazy(underlying))
  }

  def member(bindings: Hint*): Hints =
    Impl(memberHintsMap = mapFromSeq(bindings), targetHintsMap = Map.empty)

  def fromSeq(bindings: Seq[Hint]): Hints =
    Impl(memberHintsMap = Map.empty, targetHintsMap = mapFromSeq(bindings))

  private def mapFromSeq(bindings: Seq[Hint]): Map[ShapeId, Hint] = {
    bindings.map {
      case b @ Binding.StaticBinding(k, _)  => k.id -> b
      case b @ Binding.DynamicBinding(k, _) => k -> b
    }.toMap
  }

  private[smithy4s] final case class Impl(
      memberHintsMap: Map[ShapeId, Hint],
      targetHintsMap: Map[ShapeId, Hint]
  ) extends Hints {
    val toMap = targetHintsMap ++ memberHintsMap
    def isEmpty = toMap.isEmpty
    def all: Iterable[Hint] = toMap.values
    def get[A](implicit key: ShapeTag[A]): Option[A] =
      toMap.get(key.id).flatMap {
        case Binding.StaticBinding(k, value) =>
          if (key.eq(k)) Some(value.asInstanceOf[A]) else None
        case Binding.DynamicBinding(_, value) =>
          Document.Decoder.fromSchema(key.schema).decode(value).toOption
      }
    def ++(other: Hints): Hints = concat(this, other)

    def targetHints: Hints = Impl(Map.empty, targetHintsMap)
    def memberHints: Hints = Impl(memberHintsMap, Map.empty)
    def addMemberHints(hints: Hints): Hints =
      Impl(
        memberHintsMap = memberHintsMap ++ hints.toMap,
        targetHintsMap = targetHintsMap
      )

    def addTargetHints(hints: Hints): Hints =
      Impl(
        memberHintsMap = memberHintsMap,
        targetHintsMap = targetHintsMap ++ hints.toMap
      )

    override def toString(): String =
      s"Hints(${all.mkString(", ")})"

    override def equals(obj: Any): Boolean = obj match {
      case h: Hints => toMap == h.toMap
      case _        => false
    }

    override def hashCode(): Int = toMap.hashCode()
  }

  private[smithy4s] final case class LazyHints(underlying: Lazy[Hints])
      extends Hints {
    override def isEmpty: Boolean = underlying.value.isEmpty

    override def all: Iterable[Binding] = underlying.value.all

    override def memberHintsMap: Map[ShapeId, Binding] =
      underlying.value.memberHintsMap

    override def targetHintsMap: Map[ShapeId, Binding] =
      underlying.value.targetHintsMap

    override def toMap: Map[ShapeId, Binding] = underlying.value.toMap

    override def get[A](implicit key: ShapeTag[A]): Option[A] =
      underlying.value.get(key)

    override def ++(other: Hints): Hints = concat(this, other)

    override def addMemberHints(hints: Hints): Hints =
      underlying.value.addMemberHints(hints)

    override def addTargetHints(hints: Hints): Hints =
      underlying.value.addTargetHints(hints)

    override def memberHints: Hints = underlying.value.memberHints

    override def targetHints: Hints = underlying.value.targetHints

    override def equals(obj: Any): Boolean = underlying.value.equals(obj)

    override def hashCode(): Int = underlying.value.hashCode()
  }

  private def concat(lhs: Hints, rhs: Hints): Hints = (lhs, rhs) match {
    case (LazyHints(lazyA), LazyHints(lazyB)) =>
      LazyHints(Lazy(lazyA.value ++ lazyB.value))
    case (LazyHints(lazyA), _) => LazyHints(Lazy(lazyA.value ++ rhs))
    case (_, LazyHints(lazyB)) => LazyHints(Lazy(lhs ++ lazyB.value))
    case _ => {
      Impl(
        memberHintsMap = lhs.memberHintsMap ++ rhs.memberHintsMap,
        targetHintsMap = lhs.targetHintsMap ++ rhs.targetHintsMap
      )
    }
  }

  sealed trait Binding extends Product with Serializable {
    def keyId: ShapeId
  }

  object Binding {
    final case class StaticBinding[A](key: ShapeTag[A], value: A)
        extends Binding {
      override def keyId: ShapeId = key.id
      override def toString: String = value.toString()
    }
    final case class DynamicBinding(keyId: ShapeId, value: Document)
        extends Binding {
      override def toString = Document.obj(keyId.show -> value).toString()
    }

    implicit def fromValue[A, AA <: A](value: AA)(implicit
        key: ShapeTag[A]
    ): Binding = StaticBinding(key, value)

    implicit def fromTuple(tup: (ShapeId, Document)): Binding =
      DynamicBinding(tup._1, tup._2)
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy