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

playground.OperationCompiler.scala Maven / Gradle / Ivy

The newest version!
package playground

import cats.data.EitherNel
import cats.data.Ior
import cats.data.IorNel
import cats.data.Kleisli
import cats.data.NonEmptyList
import cats.syntax.all.*
import cats.~>
import playground.*
import playground.smithyql.Prelude
import playground.smithyql.QualifiedIdentifier
import playground.smithyql.Query
import playground.smithyql.WithSource
import smithy.api
import smithy4s.Endpoint
import smithy4s.Service
import smithy4s.dynamic.DynamicSchemaIndex
import smithyql.syntax.*
import types.*
import util.chaining.*

trait CompiledInput {
  type _Op[_, _, _, _, _]
  type E
  type O
  def catchError: Throwable => Option[E]
  def writeError: Option[NodeEncoder[E]]
  def writeOutput: NodeEncoder[O]
  def op: _Op[_, E, O, _, _]
}

object CompiledInput {

  type Aux[_E, _O, Op[_, _, _, _, _]] =
    CompiledInput {
      type _Op[__I, __E, __O, __SE, __SO] = Op[__I, __E, __O, __SE, __SO]
      type E = _E
      type O = _O
    }

}

trait OperationCompiler[F[_]] { self =>

  def compile(
    q: Query[WithSource]
  ): F[CompiledInput]

  def mapK[G[_]](
    fk: F ~> G
  ): OperationCompiler[G] =
    new OperationCompiler[G] {

      def compile(
        q: Query[WithSource]
      ): G[CompiledInput] = fk(self.compile(q))

    }

}

object OperationCompiler {

  final case class Context(
    prelude: Prelude[WithSource]
  )

  type EffF[F[_], A] = Kleisli[F, Context, A]
  type Eff[A] = EffF[IorNel[CompilationError, *], A]

  object Eff {

    val getContext: Eff[Context] = Kleisli.ask

    def perform(
      prelude: Prelude[WithSource]
    ): Eff ~> IorThrow = Kleisli
      .liftFunctionK(CompilationFailed.wrapK)
      .andThen(Kleisli.applyK(Context(prelude)))

  }

  def fromSchemaIndex(
    dsi: DynamicSchemaIndex
  ): OperationCompiler[Eff] = fromServices(dsi.allServices.toList)

  def fromServices(
    services: List[DynamicSchemaIndex.ServiceWrapper]
  ): OperationCompiler[Eff] = {
    val compilers: Map[QualifiedIdentifier, OperationCompiler[IorNel[CompilationError, *]]] =
      services.map { svc =>
        QualifiedIdentifier
          .forService(svc.service) -> OperationCompiler.fromService(svc.service)
      }.toMap

    new MultiServiceCompiler(
      compilers,
      ServiceIndex.fromServices(services),
    )
  }

  def fromService[Alg[_[_, _, _, _, _]]](
    service: Service[Alg]
  ): OperationCompiler[IorNel[CompilationError, *]] = new ServiceCompiler(service)

}

final case class CompilationFailed(
  errors: NonEmptyList[CompilationError]
) extends Throwable

object CompilationFailed {

  def one(
    e: CompilationError
  ): CompilationFailed = CompilationFailed(NonEmptyList.one(e))

  // this is a bit overused
  // https://github.com/kubukoz/smithy-playground/issues/157
  val wrapK: IorNel[CompilationError, *] ~> IorThrow =
    new (IorNel[CompilationError, *] ~> IorThrow) {

      def apply[A](
        fa: Ior[NonEmptyList[CompilationError], A]
      ): IorThrow[A] = seal(fa).leftMap(CompilationFailed(_))

      // https://github.com/kubukoz/smithy-playground/issues/157
      private def seal[A](
        result: IorNel[CompilationError, A]
      ): IorNel[CompilationError, A] = result.fold(
        Ior.left(_),
        Ior.right(_),
        (
          e,
          a,
        ) =>
          if (e.exists(_.isError))
            Ior.left(e)
          else
            Ior.both(e, a),
      )

    }

}

private class ServiceCompiler[Alg[_[_, _, _, _, _]]](
  service: Service[Alg]
) extends OperationCompiler[IorNel[CompilationError, *]] {

  private def compileEndpoint[In, Err, Out](
    e: Endpoint[service.Operation, In, Err, Out, _, _]
  ): QueryCompiler[CompiledInput] = {
    val inputCompiler = e.input.compile(QueryCompilerVisitor.full)
    val outputEncoder = NodeEncoder.derive(e.output)
    val errorEncoder = e.error.map(e => NodeEncoder.derive(e.schema))

    ast =>
      inputCompiler
        .compile(ast)
        .map { compiled =>
          new CompiledInput {
            type _Op[_I, _E, _O, _SE, _SO] = service.Operation[_I, _E, _O, _SE, _SO]
            type E = Err
            type O = Out

            val op: _Op[_, Err, Out, _, _] = e.wrap(compiled)
            val writeOutput: NodeEncoder[Out] = outputEncoder
            val writeError: Option[NodeEncoder[Err]] = errorEncoder
            val catchError: Throwable => Option[Err] = e.Error.unapply(_).map(_._2)
          }
        }
  }

  // https://github.com/kubukoz/smithy-playground/issues/154
  // map of endpoint names to (endpoint, input compiler)
  private val endpoints = service
    .endpoints
    .toList
    .groupByNel(_.name)
    .map(_.map(_.head).map(e => (e, compileEndpoint(e))))

  // Checks the explicit service reference (if any).
  // Note that the reference should be valid thanks to MultiServiceResolver's checks.
  private def deprecatedServiceCheck(
    q: Query[WithSource]
  ): IorNel[CompilationError, Unit] =
    q.operationName
      .value
      .identifier
      .flatMap { ref =>
        service
          .hints
          .get(api.Deprecated)
          .map(DeprecatedInfo.fromHint)
          .map(CompilationError.deprecation(_, ref.range))
      }
      .toBothLeft(())
      .toIorNel

  private def deprecatedOperationCheck(
    q: Query[WithSource],
    endpoint: Endpoint[service.Operation, _, _, _, _, _],
  ): IorNel[CompilationError, Unit] =
    endpoint
      .hints
      .get(api.Deprecated)
      .map { info =>
        CompilationError.deprecation(
          DeprecatedInfo.fromHint(info),
          q.operationName.value.operationName.range,
        )
      }
      .toBothLeft(())
      .toIorNel

  def compile(
    q: Query[WithSource]
  ): IorNel[CompilationError, CompiledInput] = {
    val (endpoint, inputCompiler) = endpoints
      .get(q.operationName.value.operationName.value.text)
      // https://github.com/kubukoz/smithy-playground/issues/154
      .getOrElse(
        sys.error(
          "Impossible! OperationCompiler running a query that doesn't belong to its operation."
        )
      )

    deprecatedServiceCheck(q) &>
      deprecatedOperationCheck(q, endpoint) &>
      inputCompiler.compile(q.input).leftMap(_.toNonEmptyList)
  }

}

private class MultiServiceCompiler[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _]](
  compilers: Map[QualifiedIdentifier, OperationCompiler[IorNel[CompilationError, *]]],
  serviceIndex: ServiceIndex,
) extends OperationCompiler[OperationCompiler.Eff] {

  private def getService(
    ctx: OperationCompiler.Context,
    q: Query[WithSource],
  ): EitherNel[CompilationError, OperationCompiler[IorNel[CompilationError, *]]] =
    MultiServiceResolver
      .resolveService(
        q.operationName.value,
        serviceIndex,
        useClauses = ctx.prelude.useClauses.map(_.value),
      )
      .map(compilers(_))

  def compile(
    q: Query[WithSource]
  ): OperationCompiler.Eff[CompiledInput] = OperationCompiler
    .Eff
    .getContext
    .flatMapF(getService(_, q).pipe(Ior.fromEither(_)))
    .flatMapF(_.compile(q))

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy