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

smithy4s.http.Metadata.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
package http

import smithy4s.http.internals.HttpResponseCodeSchemaVisitor
import smithy4s.http.internals.MetaEncode._
import smithy4s.http.internals.SchemaVisitorMetadataReader
import smithy4s.http.internals.SchemaVisitorMetadataWriter
import smithy4s.schema.CachedSchemaCompiler
import smithy4s.schema.CompilationCache

/**
  * Datatype containing metadata associated to a http message.
  *
  * The metadata is what is found in the http headers, and can be
  * derived from the http path, the query parameters, or the headers.
  *
  * Associated to it are a pair of Encoder/Decoder typeclasses, that
  * can be derived from a schema.
  *
  * @param path the path parameters of the http message
  * @param query the query parameters of the http message
  * @param headers the header parameters of the http message
  */
case class Metadata(
    path: Map[String, String] = Map.empty,
    query: Map[String, Seq[String]] = Map.empty,
    headers: Map[CaseInsensitive, Seq[String]] = Map.empty,
    statusCode: Option[Int] = None
) { self =>

  def headersFlattened: Vector[(CaseInsensitive, String)] =
    headers.toVector.flatMap { case (k, v) =>
      v.map(k -> _)
    }

  def queryFlattened: Vector[(String, String)] = query.toVector.flatMap {
    case (k, v) => v.map(k -> _)
  }

  def addHeader(ciKey: CaseInsensitive, value: String): Metadata = {
    headers.get(ciKey) match {
      case Some(existing) =>
        copy(headers = headers + (ciKey -> (value +: existing)))
      case None =>
        copy(headers = headers + (ciKey -> List(value)))
    }
  }
  def addHeader(str: String, value: String): Metadata = {
    addHeader(CaseInsensitive(str), value)
  }
  def addPathParam(key: String, value: String): Metadata =
    copy(path = path + (key -> value))
  def addQueryParam(key: String, value: String): Metadata =
    query.get(key) match {
      case Some(existing) =>
        copy(query = query + (key -> (existing :+ value)))
      case None => copy(query = query + (key -> List(value)))
    }
  def addQueryParamsIfNoExist(key: String, values: String*): Metadata =
    query.get(key) match {
      case Some(_) => self
      case None    => copy(query = query + (key -> values.toList))
    }
  def addMultipleHeaders(
      ciKey: CaseInsensitive,
      value: List[String]
  ): Metadata = {
    headers.get(ciKey) match {
      case Some(existing) =>
        copy(headers = headers + (ciKey -> (value ++ existing)))
      case None => copy(headers = headers + (ciKey -> value))
    }
  }
  def addMultipleHeaders(key: String, value: List[String]): Metadata =
    addMultipleHeaders(CaseInsensitive(key), value)

  def addMultipleQueryParams(key: String, value: List[String]): Metadata =
    query.get(key) match {
      case Some(existing) =>
        copy(query = query + (key -> (existing ++ value)))
      case None => copy(query = query + (key -> value))
    }

  def ++(other: Metadata): Metadata = {
    def mergeMaps[K, V](
        left: Map[K, Seq[V]],
        right: Map[K, Seq[V]]
    ): Map[K, Seq[V]] = {
      val m = for {
        (k, v) <- left
        (k2, v2) <- right if k == k2
      } yield (k, v ++ v2)
      val l = left.filterNot { case (k, _) => right.contains(k) }
      val r = right.filterNot { case (k, _) => left.contains(k) }
      l ++ r ++ m
    }

    Metadata(
      this.path ++ other.path,
      mergeMaps(this.query, other.query),
      mergeMaps(this.headers, other.headers),
      this.statusCode.orElse(other.statusCode)
    )
  }

  def find(location: HttpBinding): Option[(String, List[String])] =
    location match {
      case HttpBinding.HeaderBinding(httpName) =>
        headers.get(httpName).flatMap {
          case head :: tl => Some((head, tl))
          case Nil        => None
        }
      case HttpBinding.QueryBinding(httpName) =>
        query.get(httpName).flatMap {
          case head :: tl => Some((head, tl))
          case Nil        => None
        }
      case HttpBinding.PathBinding(httpName) => path.get(httpName).map(_ -> Nil)
      case _                                 => None
    }
}

object Metadata {

  def fold[A](i: Iterable[A])(f: A => Metadata): Metadata =
    i.foldLeft(empty)((acc, a) => acc ++ f(a))

  val empty = Metadata(Map.empty, Map.empty, Map.empty)

  trait Access {
    def metadata: Metadata
  }

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

  implicit def encoderFromSchema[A: Schema]: Encoder[A] =
    Encoder.derivedImplicitInstance

  /**
    * If possible, decode the data from fields that are bound to http metadata.
    */
  def decode[A](metadata: Metadata)(implicit
      decoder: Decoder[A]
  ): Either[MetadataError, A] =
    decoder.decode(metadata)

  /**
    * Reads metadata and produces a map that contains values extracted from it, labelled
    * by field names.
    */
  type Decoder[A] =
    smithy4s.codecs.Decoder[Either[MetadataError, *], Metadata, A]

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

  object Decoder extends CachedDecoderCompilerImpl(awsHeaderEncoding = false) {
    type Compiler = CachedSchemaCompiler[Decoder]
  }

  private[smithy4s] object AwsDecoder
      extends CachedDecoderCompilerImpl(awsHeaderEncoding = true)

  private[http] class CachedDecoderCompilerImpl(awsHeaderEncoding: Boolean)
      extends CachedSchemaCompiler.DerivingImpl[Decoder] {
    type Aux[A] = internals.MetaDecode[A]

    def apply[A](implicit instance: Decoder[A]): Decoder[A] =
      instance

    def fromSchema[A](
        schema: Schema[A],
        cache: CompilationCache[internals.MetaDecode]
    ): Decoder[A] = {
      val metaDecode =
        new SchemaVisitorMetadataReader(cache, awsHeaderEncoding)(schema)
      metaDecode match {
        case internals.MetaDecode.StructureMetaDecode(decodeFunction) =>
          decodeFunction(_: Metadata)
        case _ =>
          (_: Metadata) =>
            Left(
              MetadataError.ImpossibleDecoding(
                "Impossible to formulate a decoder for the data"
              )
            )
      }
    }
  }

  /**
    * Reads metadata and produces a map that contains values extracted from it, labelled
    * by field names.
    */
  type Encoder[A] = smithy4s.codecs.Encoder[Metadata, A]

  trait EncoderCompiler extends CachedSchemaCompiler[Metadata.Encoder] {
    def withExplicitDefaultsEncoding(explicitDefaults: Boolean): EncoderCompiler
  }

  object Encoder
      extends CachedEncoderCompilerImpl(
        awsHeaderEncoding = false,
        explicitDefaultsEncoding = false
      ) {
    type Compiler = CachedSchemaCompiler[Encoder]
  }

  private[smithy4s] object AwsEncoder
      extends CachedEncoderCompilerImpl(
        awsHeaderEncoding = true,
        explicitDefaultsEncoding = false
      )

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

    type Aux[A] = internals.MetaEncode[A]

    def apply[A](implicit instance: Encoder[A]): Encoder[A] = instance

    def withExplicitDefaultsEncoding(
        explicitDefaultsEncoding: Boolean
    ): EncoderCompiler = new CachedEncoderCompilerImpl(
      awsHeaderEncoding = awsHeaderEncoding,
      explicitDefaultsEncoding = explicitDefaultsEncoding
    )

    def fromSchema[A](
        schema: Schema[A],
        cache: Cache
    ): Encoder[A] = {
      val toStatusCode: A => Option[Int] =
        schema.compile(new HttpResponseCodeSchemaVisitor()).toFunction

      val schemaVisitor = new SchemaVisitorMetadataWriter(
        cache,
        commaDelimitedEncoding = awsHeaderEncoding,
        explicitDefaultsEncoding = explicitDefaultsEncoding
      )
      schemaVisitor(schema) match {
        case StructureMetaEncode(f) if awsHeaderEncoding => { (a: A) =>
          val metadata = f(a)
          // AWS does not want to receive empty header values.
          val trimmedHeaders = metadata.headers
            .map { case (key, values) => (key, values.filterNot(_.isEmpty)) }
            .filterNot(_._2.isEmpty)
            .toMap
          metadata.copy(statusCode = toStatusCode(a), headers = trimmedHeaders)
        }
        case StructureMetaEncode(f) => { (a: A) =>
          val struct = f(a)
          struct.copy(statusCode = toStatusCode(a))
        }
        case _ => (_: A) => Metadata.empty
      }
    }
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy