diff --git a/tapir/shared/src/main/scala/works/iterative/tapir/BaseUri.scala b/tapir/shared/src/main/scala/works/iterative/tapir/BaseUri.scala index 278bfde..80fd017 100644 --- a/tapir/shared/src/main/scala/works/iterative/tapir/BaseUri.scala +++ b/tapir/shared/src/main/scala/works/iterative/tapir/BaseUri.scala @@ -2,6 +2,7 @@ import sttp.model.Uri import sttp.model.Uri.* +import sttp.capabilities.zio.ZioStreams case class BaseUri(value: Option[Uri]) @@ -10,9 +11,25 @@ def apply(u: Uri): BaseUri = BaseUri(Some(u)) extension (v: BaseUri) def toUri: Option[Uri] = v.value + def toWSUri: Option[Uri] = v.value.map(u => + u.scheme match + case Some("https") => u.scheme("wss") + case _ => u.scheme("ws") + ) def /(s: String): BaseUri = v.value match case Some(u) => BaseUri(Some(uri"$u/$s")) case None => BaseUri(Some(uri"$s")) def href: String = v.value match case Some(u) => u.toString case None => "#" + +trait BaseUriExtractor[-O]: + def extractBaseUri(using baseUri: BaseUri): Option[Uri] + +object BaseUriExtractor extends LowPriorityBaseUriImplicits: + given extractWSBaseUri[A, B]: BaseUriExtractor[ZioStreams.Pipe[A, B]] with + def extractBaseUri(using baseUri: BaseUri): Option[Uri] = baseUri.toWSUri + +trait LowPriorityBaseUriImplicits: + given extractBaseUri: BaseUriExtractor[Any] with + def extractBaseUri(using baseUri: BaseUri): Option[Uri] = baseUri.toUri diff --git a/tapir/shared/src/main/scala/works/iterative/tapir/BaseUri.scala b/tapir/shared/src/main/scala/works/iterative/tapir/BaseUri.scala index 278bfde..80fd017 100644 --- a/tapir/shared/src/main/scala/works/iterative/tapir/BaseUri.scala +++ b/tapir/shared/src/main/scala/works/iterative/tapir/BaseUri.scala @@ -2,6 +2,7 @@ import sttp.model.Uri import sttp.model.Uri.* +import sttp.capabilities.zio.ZioStreams case class BaseUri(value: Option[Uri]) @@ -10,9 +11,25 @@ def apply(u: Uri): BaseUri = BaseUri(Some(u)) extension (v: BaseUri) def toUri: Option[Uri] = v.value + def toWSUri: Option[Uri] = v.value.map(u => + u.scheme match + case Some("https") => u.scheme("wss") + case _ => u.scheme("ws") + ) def /(s: String): BaseUri = v.value match case Some(u) => BaseUri(Some(uri"$u/$s")) case None => BaseUri(Some(uri"$s")) def href: String = v.value match case Some(u) => u.toString case None => "#" + +trait BaseUriExtractor[-O]: + def extractBaseUri(using baseUri: BaseUri): Option[Uri] + +object BaseUriExtractor extends LowPriorityBaseUriImplicits: + given extractWSBaseUri[A, B]: BaseUriExtractor[ZioStreams.Pipe[A, B]] with + def extractBaseUri(using baseUri: BaseUri): Option[Uri] = baseUri.toWSUri + +trait LowPriorityBaseUriImplicits: + given extractBaseUri: BaseUriExtractor[Any] with + def extractBaseUri(using baseUri: BaseUri): Option[Uri] = baseUri.toUri diff --git a/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala b/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala index d9d8b30..bda22d1 100644 --- a/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala +++ b/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala @@ -5,70 +5,57 @@ import sttp.capabilities.zio.ZioStreams import sttp.capabilities.WebSockets -import scala.compiletime.{erasedValue, summonFrom} - -opaque type Client[I, E, O] = I => IO[E, O] - -object Client: - def apply[I, E, O](f: I => IO[E, O]): Client[I, E, O] = f - - extension [I, E, O](f: Client[I, E, O]) - def apply(i: I): IO[E, O] = f(i) - def toEffect: I => ZIO[Any, E, O] = i => f(i) - -type ClientError[E] = E match - case Unit => Nothing - case _ => E - -object ClientError: - inline def cause[E](e: Cause[E]): Cause[ClientError[E]] = - erasedValue[E] match - case _: Unit => - Cause.die(throw new IllegalStateException("Internal Server Error")) - case _ => e.asInstanceOf[Cause[ClientError[E]]] - - inline def apply[S, I, E, A]( - client: SecureClient[S, I, E, A] - ): SecureClient[S, I, ClientError[E], A] = - s => i => client(s)(i).mapErrorCause(cause(_)) - -opaque type SecureClient[S, I, E, O] = S => I => IO[E, O] - -object SecureClient: - def apply[S, I, E, O](f: S => I => IO[E, O]): SecureClient[S, I, E, O] = f - - extension [S, I, E, O](f: SecureClient[S, I, E, O]) - def apply(s: S): Client[I, E, O] = f(s) - def toEffect: S => I => ZIO[Any, E, O] = s => i => f(s)(i) - -type ClientResult[S, I, E, O] = S match - case Unit => I => IO[ClientError[E], O] - case _ => S => I => IO[ClientError[E], O] - /** Create effectful methods to perform the endpoint operation * * Just a useful way to have something that will derive the client from the * endpoint using other layers, like BaseUri and provided STTP Backend. + * + * The resulting type is either + * - `S => I => IO[E, O]` if the endpoint is secure and error prone + * - `S => I => UIO[O]` if the endpoint is secure and infallible + * - `I => IO[E, O]` if the endpoint is public and error prone + * - `I => UIO[O]` if the endpoint is public and infallible */ trait ClientEndpointFactory: - inline def makeSecure[S, I, E, O]( - endpoint: Endpoint[S, I, E, O, Any] - ): S => I => IO[ClientError[E], O] = - inline val isWebSocket = - erasedValue[O] match - case _: ZioStreams.Pipe[I, O] => true - case _ => false + def make[S, I, E, O]( + endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets] + )(using + b: BaseUriExtractor[O], + e: ClientErrorConstructor[E], + m: ClientResultConstructor[S, I, e.Error, O] + ): m.Result - s => i => makeSecureClient(endpoint, isWebSocket)(s)(i).mapErrorCause(ClientError.cause(_)) +trait ClientResultConstructor[S, I, E, O]: + type Result + def makeResult(effect: S => I => IO[E, O]): Result - transparent inline def make[S, I, E, O]( - endpoint: Endpoint[S, I, E, O, Any] - ): ClientResult[S, I, E, O] = - erasedValue[S] match - case _: Unit => makeSecure(endpoint)(().asInstanceOf[S]) - case _ => makeSecure(endpoint) +object ClientResultConstructor + extends LowProirityClientResultConstructorImplicits: + given publicResultConstructor[I, E, O]: ClientResultConstructor[Unit, I, E, O] + with + type Result = I => IO[E, O] + def makeResult(effect: Unit => I => IO[E, O]): Result = effect(()) - def makeSecureClient[S, I, E, O]( - endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets], - isWebSocket: Boolean = false - ): S => I => IO[E, O] +trait LowProirityClientResultConstructorImplicits: + given secureResultConstructor[S, I, E, O]: ClientResultConstructor[S, I, E, O] + with + type Result = S => I => IO[E, O] + def makeResult(effect: S => I => IO[E, O]): Result = effect + +trait ClientErrorConstructor[-E]: + type Error + def mapErrorCause[A](effect: IO[E, A]): IO[Error, A] + +object ClientErrorConstructor + extends LowPriorityClientErrorConstructorImplicits: + given noErrorConstructor: ClientErrorConstructor[Unit] with + type Error = Nothing + def mapErrorCause[A](effect: IO[Unit, A]): IO[Nothing, A] = + effect.mapErrorCause(_ => + Cause.die(throw new IllegalStateException("Internal Server Error")) + ) + +trait LowPriorityClientErrorConstructorImplicits: + given errorConstructor[E]: ClientErrorConstructor[E] with + type Error = E + def mapErrorCause[A](effect: IO[E, A]): IO[E, A] = effect diff --git a/tapir/shared/src/main/scala/works/iterative/tapir/BaseUri.scala b/tapir/shared/src/main/scala/works/iterative/tapir/BaseUri.scala index 278bfde..80fd017 100644 --- a/tapir/shared/src/main/scala/works/iterative/tapir/BaseUri.scala +++ b/tapir/shared/src/main/scala/works/iterative/tapir/BaseUri.scala @@ -2,6 +2,7 @@ import sttp.model.Uri import sttp.model.Uri.* +import sttp.capabilities.zio.ZioStreams case class BaseUri(value: Option[Uri]) @@ -10,9 +11,25 @@ def apply(u: Uri): BaseUri = BaseUri(Some(u)) extension (v: BaseUri) def toUri: Option[Uri] = v.value + def toWSUri: Option[Uri] = v.value.map(u => + u.scheme match + case Some("https") => u.scheme("wss") + case _ => u.scheme("ws") + ) def /(s: String): BaseUri = v.value match case Some(u) => BaseUri(Some(uri"$u/$s")) case None => BaseUri(Some(uri"$s")) def href: String = v.value match case Some(u) => u.toString case None => "#" + +trait BaseUriExtractor[-O]: + def extractBaseUri(using baseUri: BaseUri): Option[Uri] + +object BaseUriExtractor extends LowPriorityBaseUriImplicits: + given extractWSBaseUri[A, B]: BaseUriExtractor[ZioStreams.Pipe[A, B]] with + def extractBaseUri(using baseUri: BaseUri): Option[Uri] = baseUri.toWSUri + +trait LowPriorityBaseUriImplicits: + given extractBaseUri: BaseUriExtractor[Any] with + def extractBaseUri(using baseUri: BaseUri): Option[Uri] = baseUri.toUri diff --git a/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala b/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala index d9d8b30..bda22d1 100644 --- a/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala +++ b/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala @@ -5,70 +5,57 @@ import sttp.capabilities.zio.ZioStreams import sttp.capabilities.WebSockets -import scala.compiletime.{erasedValue, summonFrom} - -opaque type Client[I, E, O] = I => IO[E, O] - -object Client: - def apply[I, E, O](f: I => IO[E, O]): Client[I, E, O] = f - - extension [I, E, O](f: Client[I, E, O]) - def apply(i: I): IO[E, O] = f(i) - def toEffect: I => ZIO[Any, E, O] = i => f(i) - -type ClientError[E] = E match - case Unit => Nothing - case _ => E - -object ClientError: - inline def cause[E](e: Cause[E]): Cause[ClientError[E]] = - erasedValue[E] match - case _: Unit => - Cause.die(throw new IllegalStateException("Internal Server Error")) - case _ => e.asInstanceOf[Cause[ClientError[E]]] - - inline def apply[S, I, E, A]( - client: SecureClient[S, I, E, A] - ): SecureClient[S, I, ClientError[E], A] = - s => i => client(s)(i).mapErrorCause(cause(_)) - -opaque type SecureClient[S, I, E, O] = S => I => IO[E, O] - -object SecureClient: - def apply[S, I, E, O](f: S => I => IO[E, O]): SecureClient[S, I, E, O] = f - - extension [S, I, E, O](f: SecureClient[S, I, E, O]) - def apply(s: S): Client[I, E, O] = f(s) - def toEffect: S => I => ZIO[Any, E, O] = s => i => f(s)(i) - -type ClientResult[S, I, E, O] = S match - case Unit => I => IO[ClientError[E], O] - case _ => S => I => IO[ClientError[E], O] - /** Create effectful methods to perform the endpoint operation * * Just a useful way to have something that will derive the client from the * endpoint using other layers, like BaseUri and provided STTP Backend. + * + * The resulting type is either + * - `S => I => IO[E, O]` if the endpoint is secure and error prone + * - `S => I => UIO[O]` if the endpoint is secure and infallible + * - `I => IO[E, O]` if the endpoint is public and error prone + * - `I => UIO[O]` if the endpoint is public and infallible */ trait ClientEndpointFactory: - inline def makeSecure[S, I, E, O]( - endpoint: Endpoint[S, I, E, O, Any] - ): S => I => IO[ClientError[E], O] = - inline val isWebSocket = - erasedValue[O] match - case _: ZioStreams.Pipe[I, O] => true - case _ => false + def make[S, I, E, O]( + endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets] + )(using + b: BaseUriExtractor[O], + e: ClientErrorConstructor[E], + m: ClientResultConstructor[S, I, e.Error, O] + ): m.Result - s => i => makeSecureClient(endpoint, isWebSocket)(s)(i).mapErrorCause(ClientError.cause(_)) +trait ClientResultConstructor[S, I, E, O]: + type Result + def makeResult(effect: S => I => IO[E, O]): Result - transparent inline def make[S, I, E, O]( - endpoint: Endpoint[S, I, E, O, Any] - ): ClientResult[S, I, E, O] = - erasedValue[S] match - case _: Unit => makeSecure(endpoint)(().asInstanceOf[S]) - case _ => makeSecure(endpoint) +object ClientResultConstructor + extends LowProirityClientResultConstructorImplicits: + given publicResultConstructor[I, E, O]: ClientResultConstructor[Unit, I, E, O] + with + type Result = I => IO[E, O] + def makeResult(effect: Unit => I => IO[E, O]): Result = effect(()) - def makeSecureClient[S, I, E, O]( - endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets], - isWebSocket: Boolean = false - ): S => I => IO[E, O] +trait LowProirityClientResultConstructorImplicits: + given secureResultConstructor[S, I, E, O]: ClientResultConstructor[S, I, E, O] + with + type Result = S => I => IO[E, O] + def makeResult(effect: S => I => IO[E, O]): Result = effect + +trait ClientErrorConstructor[-E]: + type Error + def mapErrorCause[A](effect: IO[E, A]): IO[Error, A] + +object ClientErrorConstructor + extends LowPriorityClientErrorConstructorImplicits: + given noErrorConstructor: ClientErrorConstructor[Unit] with + type Error = Nothing + def mapErrorCause[A](effect: IO[Unit, A]): IO[Nothing, A] = + effect.mapErrorCause(_ => + Cause.die(throw new IllegalStateException("Internal Server Error")) + ) + +trait LowPriorityClientErrorConstructorImplicits: + given errorConstructor[E]: ClientErrorConstructor[E] with + type Error = E + def mapErrorCause[A](effect: IO[E, A]): IO[E, A] = effect diff --git a/tapir/shared/src/main/scala/works/iterative/tapir/LiveClientEndpointFactory.scala b/tapir/shared/src/main/scala/works/iterative/tapir/LiveClientEndpointFactory.scala index 58de66c..992feac 100644 --- a/tapir/shared/src/main/scala/works/iterative/tapir/LiveClientEndpointFactory.scala +++ b/tapir/shared/src/main/scala/works/iterative/tapir/LiveClientEndpointFactory.scala @@ -12,37 +12,39 @@ ) extends ClientEndpointFactory with CustomTapir: - override def makeSecureClient[S, I, E, O]( - endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets], - isWebSocket: Boolean = false - ): S => I => IO[E, O] = (securityInput: S) => (input: I) => - val req = toSecureRequest( - endpoint, - if isWebSocket then - baseUri.toUri.map(b => - b.scheme match - case Some("https") => b.scheme("wss") - case _ => b.scheme("ws") - ) - else baseUri.toUri - ) + def makeRequest[S, I, E, O]( + endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets] + )(using ext: BaseUriExtractor[O]) = toSecureRequest( + endpoint, + ext.extractBaseUri + ) - val fetch = req(securityInput)(input).followRedirects(false).send(backend) + override def make[S, I, E, O]( + endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets] + )(using + ext: BaseUriExtractor[O], + em: ClientErrorConstructor[E], + m: ClientResultConstructor[S, I, em.Error, O] + ): m.Result = m.makeResult((securityInput: S) => + (input: I) => + val req = makeRequest(endpoint) + val fetch = req(securityInput)(input).followRedirects(false).send(backend) - val result = for - resp <- fetch.orDie - body <- resp.body match - case DecodeResult.Value(v) => ZIO.succeed(v) - case err: DecodeResult.Failure => - ZIO.die( - new RuntimeException( - s"Unexpected response status: ${resp.code} ${resp.statusText} - ${err}" + val result = for + resp <- fetch.orDie + body <- resp.body match + case DecodeResult.Value(v) => ZIO.succeed(v) + case err: DecodeResult.Failure => + ZIO.die( + new RuntimeException( + s"Unexpected response status: ${resp.code} ${resp.statusText} - ${err}" + ) ) - ) - v <- ZIO.fromEither(body) - yield v + v <- ZIO.fromEither(body) + yield v - result + em.mapErrorCause(result) + ) object LiveClientEndpointFactory: val layer: URLayer[BaseUri & CustomTapir.Backend, ClientEndpointFactory] = diff --git a/tapir/shared/src/main/scala/works/iterative/tapir/BaseUri.scala b/tapir/shared/src/main/scala/works/iterative/tapir/BaseUri.scala index 278bfde..80fd017 100644 --- a/tapir/shared/src/main/scala/works/iterative/tapir/BaseUri.scala +++ b/tapir/shared/src/main/scala/works/iterative/tapir/BaseUri.scala @@ -2,6 +2,7 @@ import sttp.model.Uri import sttp.model.Uri.* +import sttp.capabilities.zio.ZioStreams case class BaseUri(value: Option[Uri]) @@ -10,9 +11,25 @@ def apply(u: Uri): BaseUri = BaseUri(Some(u)) extension (v: BaseUri) def toUri: Option[Uri] = v.value + def toWSUri: Option[Uri] = v.value.map(u => + u.scheme match + case Some("https") => u.scheme("wss") + case _ => u.scheme("ws") + ) def /(s: String): BaseUri = v.value match case Some(u) => BaseUri(Some(uri"$u/$s")) case None => BaseUri(Some(uri"$s")) def href: String = v.value match case Some(u) => u.toString case None => "#" + +trait BaseUriExtractor[-O]: + def extractBaseUri(using baseUri: BaseUri): Option[Uri] + +object BaseUriExtractor extends LowPriorityBaseUriImplicits: + given extractWSBaseUri[A, B]: BaseUriExtractor[ZioStreams.Pipe[A, B]] with + def extractBaseUri(using baseUri: BaseUri): Option[Uri] = baseUri.toWSUri + +trait LowPriorityBaseUriImplicits: + given extractBaseUri: BaseUriExtractor[Any] with + def extractBaseUri(using baseUri: BaseUri): Option[Uri] = baseUri.toUri diff --git a/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala b/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala index d9d8b30..bda22d1 100644 --- a/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala +++ b/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala @@ -5,70 +5,57 @@ import sttp.capabilities.zio.ZioStreams import sttp.capabilities.WebSockets -import scala.compiletime.{erasedValue, summonFrom} - -opaque type Client[I, E, O] = I => IO[E, O] - -object Client: - def apply[I, E, O](f: I => IO[E, O]): Client[I, E, O] = f - - extension [I, E, O](f: Client[I, E, O]) - def apply(i: I): IO[E, O] = f(i) - def toEffect: I => ZIO[Any, E, O] = i => f(i) - -type ClientError[E] = E match - case Unit => Nothing - case _ => E - -object ClientError: - inline def cause[E](e: Cause[E]): Cause[ClientError[E]] = - erasedValue[E] match - case _: Unit => - Cause.die(throw new IllegalStateException("Internal Server Error")) - case _ => e.asInstanceOf[Cause[ClientError[E]]] - - inline def apply[S, I, E, A]( - client: SecureClient[S, I, E, A] - ): SecureClient[S, I, ClientError[E], A] = - s => i => client(s)(i).mapErrorCause(cause(_)) - -opaque type SecureClient[S, I, E, O] = S => I => IO[E, O] - -object SecureClient: - def apply[S, I, E, O](f: S => I => IO[E, O]): SecureClient[S, I, E, O] = f - - extension [S, I, E, O](f: SecureClient[S, I, E, O]) - def apply(s: S): Client[I, E, O] = f(s) - def toEffect: S => I => ZIO[Any, E, O] = s => i => f(s)(i) - -type ClientResult[S, I, E, O] = S match - case Unit => I => IO[ClientError[E], O] - case _ => S => I => IO[ClientError[E], O] - /** Create effectful methods to perform the endpoint operation * * Just a useful way to have something that will derive the client from the * endpoint using other layers, like BaseUri and provided STTP Backend. + * + * The resulting type is either + * - `S => I => IO[E, O]` if the endpoint is secure and error prone + * - `S => I => UIO[O]` if the endpoint is secure and infallible + * - `I => IO[E, O]` if the endpoint is public and error prone + * - `I => UIO[O]` if the endpoint is public and infallible */ trait ClientEndpointFactory: - inline def makeSecure[S, I, E, O]( - endpoint: Endpoint[S, I, E, O, Any] - ): S => I => IO[ClientError[E], O] = - inline val isWebSocket = - erasedValue[O] match - case _: ZioStreams.Pipe[I, O] => true - case _ => false + def make[S, I, E, O]( + endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets] + )(using + b: BaseUriExtractor[O], + e: ClientErrorConstructor[E], + m: ClientResultConstructor[S, I, e.Error, O] + ): m.Result - s => i => makeSecureClient(endpoint, isWebSocket)(s)(i).mapErrorCause(ClientError.cause(_)) +trait ClientResultConstructor[S, I, E, O]: + type Result + def makeResult(effect: S => I => IO[E, O]): Result - transparent inline def make[S, I, E, O]( - endpoint: Endpoint[S, I, E, O, Any] - ): ClientResult[S, I, E, O] = - erasedValue[S] match - case _: Unit => makeSecure(endpoint)(().asInstanceOf[S]) - case _ => makeSecure(endpoint) +object ClientResultConstructor + extends LowProirityClientResultConstructorImplicits: + given publicResultConstructor[I, E, O]: ClientResultConstructor[Unit, I, E, O] + with + type Result = I => IO[E, O] + def makeResult(effect: Unit => I => IO[E, O]): Result = effect(()) - def makeSecureClient[S, I, E, O]( - endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets], - isWebSocket: Boolean = false - ): S => I => IO[E, O] +trait LowProirityClientResultConstructorImplicits: + given secureResultConstructor[S, I, E, O]: ClientResultConstructor[S, I, E, O] + with + type Result = S => I => IO[E, O] + def makeResult(effect: S => I => IO[E, O]): Result = effect + +trait ClientErrorConstructor[-E]: + type Error + def mapErrorCause[A](effect: IO[E, A]): IO[Error, A] + +object ClientErrorConstructor + extends LowPriorityClientErrorConstructorImplicits: + given noErrorConstructor: ClientErrorConstructor[Unit] with + type Error = Nothing + def mapErrorCause[A](effect: IO[Unit, A]): IO[Nothing, A] = + effect.mapErrorCause(_ => + Cause.die(throw new IllegalStateException("Internal Server Error")) + ) + +trait LowPriorityClientErrorConstructorImplicits: + given errorConstructor[E]: ClientErrorConstructor[E] with + type Error = E + def mapErrorCause[A](effect: IO[E, A]): IO[E, A] = effect diff --git a/tapir/shared/src/main/scala/works/iterative/tapir/LiveClientEndpointFactory.scala b/tapir/shared/src/main/scala/works/iterative/tapir/LiveClientEndpointFactory.scala index 58de66c..992feac 100644 --- a/tapir/shared/src/main/scala/works/iterative/tapir/LiveClientEndpointFactory.scala +++ b/tapir/shared/src/main/scala/works/iterative/tapir/LiveClientEndpointFactory.scala @@ -12,37 +12,39 @@ ) extends ClientEndpointFactory with CustomTapir: - override def makeSecureClient[S, I, E, O]( - endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets], - isWebSocket: Boolean = false - ): S => I => IO[E, O] = (securityInput: S) => (input: I) => - val req = toSecureRequest( - endpoint, - if isWebSocket then - baseUri.toUri.map(b => - b.scheme match - case Some("https") => b.scheme("wss") - case _ => b.scheme("ws") - ) - else baseUri.toUri - ) + def makeRequest[S, I, E, O]( + endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets] + )(using ext: BaseUriExtractor[O]) = toSecureRequest( + endpoint, + ext.extractBaseUri + ) - val fetch = req(securityInput)(input).followRedirects(false).send(backend) + override def make[S, I, E, O]( + endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets] + )(using + ext: BaseUriExtractor[O], + em: ClientErrorConstructor[E], + m: ClientResultConstructor[S, I, em.Error, O] + ): m.Result = m.makeResult((securityInput: S) => + (input: I) => + val req = makeRequest(endpoint) + val fetch = req(securityInput)(input).followRedirects(false).send(backend) - val result = for - resp <- fetch.orDie - body <- resp.body match - case DecodeResult.Value(v) => ZIO.succeed(v) - case err: DecodeResult.Failure => - ZIO.die( - new RuntimeException( - s"Unexpected response status: ${resp.code} ${resp.statusText} - ${err}" + val result = for + resp <- fetch.orDie + body <- resp.body match + case DecodeResult.Value(v) => ZIO.succeed(v) + case err: DecodeResult.Failure => + ZIO.die( + new RuntimeException( + s"Unexpected response status: ${resp.code} ${resp.statusText} - ${err}" + ) ) - ) - v <- ZIO.fromEither(body) - yield v + v <- ZIO.fromEither(body) + yield v - result + em.mapErrorCause(result) + ) object LiveClientEndpointFactory: val layer: URLayer[BaseUri & CustomTapir.Backend, ClientEndpointFactory] = diff --git a/tapir/shared/src/test/scala/works/iterative/tapir/LiveClientEndpointFactorySpec.scala b/tapir/shared/src/test/scala/works/iterative/tapir/LiveClientEndpointFactorySpec.scala new file mode 100644 index 0000000..4558fc1 --- /dev/null +++ b/tapir/shared/src/test/scala/works/iterative/tapir/LiveClientEndpointFactorySpec.scala @@ -0,0 +1,60 @@ +package works.iterative.tapir + +import zio.* +import zio.test.* +import sttp.capabilities.zio.ZioStreams + +object LiveClientEndpointFactorySpec extends ZIOSpecDefault with CustomTapir: + + val testFactory = ZIO + .service[ClientEndpointFactory] + .map(_.asInstanceOf[LiveClientEndpointFactory]) + + override def spec = suite("ClientEndpointFactory")( + test("can make a request for a secure endpoint") { + for factory <- testFactory + yield + factory.makeRequest(endpoint.get.in("api" / "test")) + assertCompletes + }, + test("can make a request for a WS secure endpoint") { + for factory <- testFactory + yield + factory.makeRequest( + endpoint.get + .in("api" / "test") + .out( + webSocketBodyRaw(ZioStreams) + ) + ) + assertCompletes + }, + test("can make an effect for a infallible endpoint") { + for factory <- testFactory + yield + factory.make(endpoint.get.in("api" / "test")) + assertCompletes + }, + test("can make an effect for a fallible endpoint") { + for factory <- testFactory + yield + factory.make( + endpoint.get.in("api" / "test").errorOut(stringBody) + ) + assertCompletes + }, + test("can make an effect for secure endpoint") { + for factory <- testFactory + yield + factory.make( + endpoint.get + .securityIn(auth.apiKey(header[String]("X-Access-Token"))) + .in("api" / "test") + .errorOut(stringBody) + ) + assertCompletes + } + ).provideShared( + ZLayer.succeed(BaseUri("http://localhost:8080")), + LiveClientEndpointFactory.default + ) diff --git a/tapir/shared/src/main/scala/works/iterative/tapir/BaseUri.scala b/tapir/shared/src/main/scala/works/iterative/tapir/BaseUri.scala index 278bfde..80fd017 100644 --- a/tapir/shared/src/main/scala/works/iterative/tapir/BaseUri.scala +++ b/tapir/shared/src/main/scala/works/iterative/tapir/BaseUri.scala @@ -2,6 +2,7 @@ import sttp.model.Uri import sttp.model.Uri.* +import sttp.capabilities.zio.ZioStreams case class BaseUri(value: Option[Uri]) @@ -10,9 +11,25 @@ def apply(u: Uri): BaseUri = BaseUri(Some(u)) extension (v: BaseUri) def toUri: Option[Uri] = v.value + def toWSUri: Option[Uri] = v.value.map(u => + u.scheme match + case Some("https") => u.scheme("wss") + case _ => u.scheme("ws") + ) def /(s: String): BaseUri = v.value match case Some(u) => BaseUri(Some(uri"$u/$s")) case None => BaseUri(Some(uri"$s")) def href: String = v.value match case Some(u) => u.toString case None => "#" + +trait BaseUriExtractor[-O]: + def extractBaseUri(using baseUri: BaseUri): Option[Uri] + +object BaseUriExtractor extends LowPriorityBaseUriImplicits: + given extractWSBaseUri[A, B]: BaseUriExtractor[ZioStreams.Pipe[A, B]] with + def extractBaseUri(using baseUri: BaseUri): Option[Uri] = baseUri.toWSUri + +trait LowPriorityBaseUriImplicits: + given extractBaseUri: BaseUriExtractor[Any] with + def extractBaseUri(using baseUri: BaseUri): Option[Uri] = baseUri.toUri diff --git a/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala b/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala index d9d8b30..bda22d1 100644 --- a/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala +++ b/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala @@ -5,70 +5,57 @@ import sttp.capabilities.zio.ZioStreams import sttp.capabilities.WebSockets -import scala.compiletime.{erasedValue, summonFrom} - -opaque type Client[I, E, O] = I => IO[E, O] - -object Client: - def apply[I, E, O](f: I => IO[E, O]): Client[I, E, O] = f - - extension [I, E, O](f: Client[I, E, O]) - def apply(i: I): IO[E, O] = f(i) - def toEffect: I => ZIO[Any, E, O] = i => f(i) - -type ClientError[E] = E match - case Unit => Nothing - case _ => E - -object ClientError: - inline def cause[E](e: Cause[E]): Cause[ClientError[E]] = - erasedValue[E] match - case _: Unit => - Cause.die(throw new IllegalStateException("Internal Server Error")) - case _ => e.asInstanceOf[Cause[ClientError[E]]] - - inline def apply[S, I, E, A]( - client: SecureClient[S, I, E, A] - ): SecureClient[S, I, ClientError[E], A] = - s => i => client(s)(i).mapErrorCause(cause(_)) - -opaque type SecureClient[S, I, E, O] = S => I => IO[E, O] - -object SecureClient: - def apply[S, I, E, O](f: S => I => IO[E, O]): SecureClient[S, I, E, O] = f - - extension [S, I, E, O](f: SecureClient[S, I, E, O]) - def apply(s: S): Client[I, E, O] = f(s) - def toEffect: S => I => ZIO[Any, E, O] = s => i => f(s)(i) - -type ClientResult[S, I, E, O] = S match - case Unit => I => IO[ClientError[E], O] - case _ => S => I => IO[ClientError[E], O] - /** Create effectful methods to perform the endpoint operation * * Just a useful way to have something that will derive the client from the * endpoint using other layers, like BaseUri and provided STTP Backend. + * + * The resulting type is either + * - `S => I => IO[E, O]` if the endpoint is secure and error prone + * - `S => I => UIO[O]` if the endpoint is secure and infallible + * - `I => IO[E, O]` if the endpoint is public and error prone + * - `I => UIO[O]` if the endpoint is public and infallible */ trait ClientEndpointFactory: - inline def makeSecure[S, I, E, O]( - endpoint: Endpoint[S, I, E, O, Any] - ): S => I => IO[ClientError[E], O] = - inline val isWebSocket = - erasedValue[O] match - case _: ZioStreams.Pipe[I, O] => true - case _ => false + def make[S, I, E, O]( + endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets] + )(using + b: BaseUriExtractor[O], + e: ClientErrorConstructor[E], + m: ClientResultConstructor[S, I, e.Error, O] + ): m.Result - s => i => makeSecureClient(endpoint, isWebSocket)(s)(i).mapErrorCause(ClientError.cause(_)) +trait ClientResultConstructor[S, I, E, O]: + type Result + def makeResult(effect: S => I => IO[E, O]): Result - transparent inline def make[S, I, E, O]( - endpoint: Endpoint[S, I, E, O, Any] - ): ClientResult[S, I, E, O] = - erasedValue[S] match - case _: Unit => makeSecure(endpoint)(().asInstanceOf[S]) - case _ => makeSecure(endpoint) +object ClientResultConstructor + extends LowProirityClientResultConstructorImplicits: + given publicResultConstructor[I, E, O]: ClientResultConstructor[Unit, I, E, O] + with + type Result = I => IO[E, O] + def makeResult(effect: Unit => I => IO[E, O]): Result = effect(()) - def makeSecureClient[S, I, E, O]( - endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets], - isWebSocket: Boolean = false - ): S => I => IO[E, O] +trait LowProirityClientResultConstructorImplicits: + given secureResultConstructor[S, I, E, O]: ClientResultConstructor[S, I, E, O] + with + type Result = S => I => IO[E, O] + def makeResult(effect: S => I => IO[E, O]): Result = effect + +trait ClientErrorConstructor[-E]: + type Error + def mapErrorCause[A](effect: IO[E, A]): IO[Error, A] + +object ClientErrorConstructor + extends LowPriorityClientErrorConstructorImplicits: + given noErrorConstructor: ClientErrorConstructor[Unit] with + type Error = Nothing + def mapErrorCause[A](effect: IO[Unit, A]): IO[Nothing, A] = + effect.mapErrorCause(_ => + Cause.die(throw new IllegalStateException("Internal Server Error")) + ) + +trait LowPriorityClientErrorConstructorImplicits: + given errorConstructor[E]: ClientErrorConstructor[E] with + type Error = E + def mapErrorCause[A](effect: IO[E, A]): IO[E, A] = effect diff --git a/tapir/shared/src/main/scala/works/iterative/tapir/LiveClientEndpointFactory.scala b/tapir/shared/src/main/scala/works/iterative/tapir/LiveClientEndpointFactory.scala index 58de66c..992feac 100644 --- a/tapir/shared/src/main/scala/works/iterative/tapir/LiveClientEndpointFactory.scala +++ b/tapir/shared/src/main/scala/works/iterative/tapir/LiveClientEndpointFactory.scala @@ -12,37 +12,39 @@ ) extends ClientEndpointFactory with CustomTapir: - override def makeSecureClient[S, I, E, O]( - endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets], - isWebSocket: Boolean = false - ): S => I => IO[E, O] = (securityInput: S) => (input: I) => - val req = toSecureRequest( - endpoint, - if isWebSocket then - baseUri.toUri.map(b => - b.scheme match - case Some("https") => b.scheme("wss") - case _ => b.scheme("ws") - ) - else baseUri.toUri - ) + def makeRequest[S, I, E, O]( + endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets] + )(using ext: BaseUriExtractor[O]) = toSecureRequest( + endpoint, + ext.extractBaseUri + ) - val fetch = req(securityInput)(input).followRedirects(false).send(backend) + override def make[S, I, E, O]( + endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets] + )(using + ext: BaseUriExtractor[O], + em: ClientErrorConstructor[E], + m: ClientResultConstructor[S, I, em.Error, O] + ): m.Result = m.makeResult((securityInput: S) => + (input: I) => + val req = makeRequest(endpoint) + val fetch = req(securityInput)(input).followRedirects(false).send(backend) - val result = for - resp <- fetch.orDie - body <- resp.body match - case DecodeResult.Value(v) => ZIO.succeed(v) - case err: DecodeResult.Failure => - ZIO.die( - new RuntimeException( - s"Unexpected response status: ${resp.code} ${resp.statusText} - ${err}" + val result = for + resp <- fetch.orDie + body <- resp.body match + case DecodeResult.Value(v) => ZIO.succeed(v) + case err: DecodeResult.Failure => + ZIO.die( + new RuntimeException( + s"Unexpected response status: ${resp.code} ${resp.statusText} - ${err}" + ) ) - ) - v <- ZIO.fromEither(body) - yield v + v <- ZIO.fromEither(body) + yield v - result + em.mapErrorCause(result) + ) object LiveClientEndpointFactory: val layer: URLayer[BaseUri & CustomTapir.Backend, ClientEndpointFactory] = diff --git a/tapir/shared/src/test/scala/works/iterative/tapir/LiveClientEndpointFactorySpec.scala b/tapir/shared/src/test/scala/works/iterative/tapir/LiveClientEndpointFactorySpec.scala new file mode 100644 index 0000000..4558fc1 --- /dev/null +++ b/tapir/shared/src/test/scala/works/iterative/tapir/LiveClientEndpointFactorySpec.scala @@ -0,0 +1,60 @@ +package works.iterative.tapir + +import zio.* +import zio.test.* +import sttp.capabilities.zio.ZioStreams + +object LiveClientEndpointFactorySpec extends ZIOSpecDefault with CustomTapir: + + val testFactory = ZIO + .service[ClientEndpointFactory] + .map(_.asInstanceOf[LiveClientEndpointFactory]) + + override def spec = suite("ClientEndpointFactory")( + test("can make a request for a secure endpoint") { + for factory <- testFactory + yield + factory.makeRequest(endpoint.get.in("api" / "test")) + assertCompletes + }, + test("can make a request for a WS secure endpoint") { + for factory <- testFactory + yield + factory.makeRequest( + endpoint.get + .in("api" / "test") + .out( + webSocketBodyRaw(ZioStreams) + ) + ) + assertCompletes + }, + test("can make an effect for a infallible endpoint") { + for factory <- testFactory + yield + factory.make(endpoint.get.in("api" / "test")) + assertCompletes + }, + test("can make an effect for a fallible endpoint") { + for factory <- testFactory + yield + factory.make( + endpoint.get.in("api" / "test").errorOut(stringBody) + ) + assertCompletes + }, + test("can make an effect for secure endpoint") { + for factory <- testFactory + yield + factory.make( + endpoint.get + .securityIn(auth.apiKey(header[String]("X-Access-Token"))) + .in("api" / "test") + .errorOut(stringBody) + ) + assertCompletes + } + ).provideShared( + ZLayer.succeed(BaseUri("http://localhost:8080")), + LiveClientEndpointFactory.default + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/ReloadableComponent.scala b/ui/js/src/main/scala/works/iterative/ui/components/ReloadableComponent.scala index c5aca29..7b6c8fa 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/ReloadableComponent.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/ReloadableComponent.scala @@ -8,6 +8,9 @@ import works.iterative.ui.model.Computable import zio.* import zio.stream.* +import works.iterative.tapir.ClientErrorConstructor +import works.iterative.tapir.ClientResultConstructor +import works.iterative.tapir.BaseUriExtractor case class ReloadableComponent[A, I]( fetch: I => IO[UserMessage, A], @@ -120,10 +123,4 @@ for given Runtime[Any] <- ZIO.runtime[Any] factory <- ZIO.service[ClientEndpointFactory] - yield - val client = factory.makeSecureClient(endpoint)(()) - new ReloadableComponent( - client(_).mapErrorCause(_ => - Cause.die(IllegalStateException("Internal Server Error")) - ) - ) + yield new ReloadableComponent(factory.make(endpoint))