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

smithy4s.Endpoint.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.schema._
import schema.ErrorSchema
import smithy4s.kinds.PolyFunction5
import smithy4s.kinds._

/**
  * A representation of a smithy operation.
  *
  * @tparam Op: the GADT of all operations in a service
  * @tparam I: the input type of the operation (Unit if N/A)
  * @tparam E: the error ADT of the operation (Nothing if N/A)
  * @tparam O: the output of the operation (Unit if N/A)
  * @tparam SI: the Streamed input of the operaton (Nothing if N/A)
  * @tparam SO: the Streamed output of the operaton (Nothing if N/A)
  *
  * This type carries references to the Schemas of the various types involved,
  * allowing to compile corresponding codecs.
  *
  * Optionally, an endpoint can have an `ErrorSchema` which allows for matching
  * throwables against the errors the operation knows about (which form an ADT
  * in the Scala representation)
  *
  * NB: SI an SO respectively are derived from the @streaming trait in smithy.
  * If this trait is present in one on one of the members of Input/Output, the
  * member is removed from the Scala representation, in order to avoid polluting
  * datatypes that typically fit in memory with concerns of streaming (which can
  * be encoded a great many ways, using a great many libraries)
  */
// scalafmt: {maxColumn = 120}
trait Endpoint[Op[_, _, _, _, _], I, E, O, SI, SO] { self =>

  final def mapSchema(
      f: OperationSchema[I, E, O, SI, SO] => OperationSchema[I, E, O, SI, SO]
  ): Endpoint[Op, I, E, O, SI, SO] = Endpoint(f(schema), wrap)

  def schema: OperationSchema[I, E, O, SI, SO]
  def wrap(input: I): Op[I, E, O, SI, SO]

  final def id: ShapeId = schema.id
  final def name: String = schema.id.name
  final def hints: Hints = schema.hints
  final def input: Schema[I] = schema.input
  final def output: Schema[O] = schema.output
  final def error: Option[ErrorSchema[E]] = schema.error
  @deprecated("Use .error instead", since = "0.18")
  final def errorschema: Option[ErrorSchema[E]] = schema.error
  final def streamedInput: Option[StreamingSchema[SI]] = schema.streamedInput
  final def streamedOutput: Option[StreamingSchema[SO]] = schema.streamedOutput

  object Error {
    def unapply(throwable: Throwable): Option[(ErrorSchema[E], E)] =
      error.flatMap { err =>
        err.liftError(throwable).map(err -> _)
      }
  }

  /**
    * Allows the creation of a handler via lifting a function that returns some functor.
    */
  def handler[F[_]](f: I => F[O]): EndpointHandler[Op, Kind1[F]#toKind5] = new Handler[F] {
    def run(input: I): F[O] = f(input)
  }

  /**
    * Allows the creation of a handler via lifting a function that returns some bi-functor.
    */
  def errorAwareHandler[F[_, _]](f: I => F[E, O]): EndpointHandler[Op, Kind2[F]#toKind5] = new ErrorAwareHandler[F] {
    def run(input: I): F[E, O] = f(input)
  }

  /**
    * Allows the creation of a hander via object-oriented inheritance.
    */
  abstract class Handler[F[_]] extends EndpointHandler[Op, Kind1[F]#toKind5] {
    def run(input: I): F[O]
    protected[smithy4s] def lift[Alg[_[_, _, _, _, _]]](
        service: Service.Aux[Alg, Op]
    ): PolyFunction5[Op, Kind1[F]#optional5] = new PolyFunction5[Op, Kind1[F]#optional5] {
      val ord = service.endpoints.indexOf(self)

      def apply[I_, E_, O_, SI_, SO_](op: Op[I_, E_, O_, SI_, SO_]): Option[F[O_]] = if (service.ordinal(op) == ord) {
        Some(run(service.input(op).asInstanceOf[I]).asInstanceOf[F[O_]])
      } else None
    }
  }

  abstract class ErrorAwareHandler[F[_, _]] extends EndpointHandler[Op, Kind2[F]#toKind5] {
    def run(input: I): F[E, O]
    protected[smithy4s] def lift[Alg[_[_, _, _, _, _]]](
        service: Service.Aux[Alg, Op]
    ): PolyFunction5[Op, Kind2[F]#optional5] = new PolyFunction5[Op, Kind2[F]#optional5] {
      val ord = service.endpoints.indexOf(self)

      def apply[I_, E_, O_, SI_, SO_](op: Op[I_, E_, O_, SI_, SO_]): Option[F[E_, O_]] = if (
        service.ordinal(op) == ord
      ) {
        Some(run(service.input(op).asInstanceOf[I]).asInstanceOf[F[E_, O_]])
      } else None
    }
  }
}

object Endpoint {

  trait Middleware[A] { self =>
    def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])(endpoint: service.Endpoint[_, _, _, _, _]): A => A
    final def biject[B](to: A => B)(from: B => A): Middleware[B] = new Middleware[B] {
      def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])(endpoint: service.Endpoint[_, _, _, _, _]): B => B =
        self.prepare(service)(endpoint).compose(from).andThen(to)
    }

    final def andThen(other: Middleware[A]): Middleware[A] =
      if (this == Middleware.NoopMiddleware) { other }
      else if (other == Middleware.NoopMiddleware) { this }
      else
        new Middleware[A] {
          def prepare[Alg[_[_, _, _, _, _]]](
              service: Service[Alg]
          )(endpoint: service.Endpoint[_, _, _, _, _]): A => A =
            self
              .prepare(service)(endpoint)
              .andThen(other.prepare(service)(endpoint))
        }

  }
// format: on

  object Middleware {

    trait Simple[Construct] extends Middleware[Construct] {
      def prepareWithHints(serviceHints: Hints, endpointHints: Hints): Construct => Construct

      final def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])(
          endpoint: service.Endpoint[_, _, _, _, _]
      ): Construct => Construct =
        prepareWithHints(service.hints, endpoint.hints)
    }

    trait Standard[Construct] extends Middleware[Construct] {
      def prepare(
          serviceId: ShapeId,
          endpointId: ShapeId,
          serviceHints: Hints,
          endpointHints: Hints
      ): Construct => Construct

      final def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])(
          endpoint: service.Endpoint[_, _, _, _, _]
      ): Construct => Construct =
        prepare(service.id, endpoint.id, service.hints, endpoint.hints)
    }

    def noop[Construct]: Middleware[Construct] =
      NoopMiddleware.asInstanceOf[Middleware[Construct]]

    private case object NoopMiddleware extends Middleware[Any] {
      def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])(
          endpoint: service.Endpoint[_, _, _, _, _]
      ): Any => Any = identity[Any]
    }

  }
  def apply[Op[_, _, _, _, _], I, E, O, SI, SO](
      operationSchema: OperationSchema[I, E, O, SI, SO],
      wrapFunction: I => Op[I, E, O, SI, SO]
  ): Endpoint[Op, I, E, O, SI, SO] =
    new Endpoint[Op, I, E, O, SI, SO] {
      def schema = operationSchema
      def wrap(i: I): Op[I, E, O, SI, SO] = wrapFunction(i)
    }

  type ForOperation[Op[_, _, _, _, _]] = {
    type e[I, E, O, SI, SO] = Endpoint[Op, I, E, O, SI, SO]
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy