Skip to content

Commit

Permalink
Scala Native support for sttpClient with tests
Browse files Browse the repository at this point in the history
  • Loading branch information
fede0664 committed Oct 19, 2022
1 parent 34ce294 commit 30da7d7
Show file tree
Hide file tree
Showing 16 changed files with 235 additions and 24 deletions.
17 changes: 17 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,10 @@ lazy val tests: ProjectMatrix = (projectMatrix in file("tests"))
scalaVersions = scala2And3Versions,
settings = commonJsSettings
)
.nativePlatform(
scalaVersions = scala2And3Versions,
settings = commonNativeSettings
)
.dependsOn(core, circeJson, cats)

val akkaHttpVanilla = taskKey[Unit]("akka-http-vanilla")
Expand Down Expand Up @@ -1441,6 +1445,10 @@ lazy val clientTests: ProjectMatrix = (projectMatrix in file("client/tests"))
scalaVersions = scala2And3Versions,
settings = commonJsSettings
)
.nativePlatform(
scalaVersions = scala2And3Versions,
settings = commonNativeSettings
)
.dependsOn(tests)

lazy val clientCore: ProjectMatrix = (projectMatrix in file("client/core"))
Expand Down Expand Up @@ -1507,6 +1515,15 @@ lazy val sttpClient: ProjectMatrix = (projectMatrix in file("client/sttp-client"
)
)
)
.nativePlatform(
scalaVersions = scala2And3Versions,
settings = commonNativeSettings ++ Seq(
libraryDependencies ++= Seq(
"io.github.cquiroz" %%% "scala-java-time" % Versions.nativeScalaJavaTime % Test,
"io.github.cquiroz" %%% "scala-java-time-tzdb" % Versions.nativeScalaJavaTime % Test,
)
)
)
.dependsOn(clientCore, clientTests % Test)

lazy val sttpClientWsZio1: ProjectMatrix = (projectMatrix in file("client/sttp-client-ws-zio1"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package sttp.tapir.client.http4s

import cats.effect.IO
import cats.effect.unsafe.implicits.global

import fs2.text
import sttp.capabilities.fs2.Fs2Streams
import sttp.tapir.client.tests.ClientStreamingTests
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package sttp.tapir.client.sttp

import sttp.client3.CurlBackend
import sttp.model.Uri
import sttp.tapir.{Endpoint, PublicEndpoint}

trait SttpClientInterpreterExtensions { this: SttpClientInterpreter =>

// public

/** Interprets the public endpoint as a synchronous client call, using the given `baseUri` as the starting point to create the target uri.
* If `baseUri` is not provided, the request will be a relative one.
*
* Returns a function which, when applied to the endpoint's input parameters (given as a tuple), will encode them to appropriate request
* parameters: path, query, headers and body. The request is sent using a synchronous backend, and the result of decoding the response
* (error or success value) is returned. If decoding the result fails, an exception is thrown.
*/
def toQuickClient[I, E, O](e: PublicEndpoint[I, E, O, Any], baseUri: Option[Uri]): I => Either[E, O] = {
val backend = CurlBackend()
SttpClientInterpreter().toClientThrowDecodeFailures(e, baseUri, backend)
}

/** Interprets the public endpoint as a client call, using the given `baseUri` as the starting point to create the target uri. If
* `baseUri` is not provided, the request will be a relative one.
*
* Returns a function which, when applied to the endpoint's input parameters (given as a tuple), will encode them to appropriate request
* parameters: path, query, headers and body. The request is sent using a synchronous backend, and the result (success value) is
* returned. If decoding the result fails, or if the response corresponds to an error value, an exception is thrown.
*/
def toQuickClientThrowErrors[I, E, O](e: PublicEndpoint[I, E, O, Any], baseUri: Option[Uri]): I => O = {
val backend = CurlBackend()
SttpClientInterpreter().toClientThrowErrors(e, baseUri, backend)
}

// secure

/** Interprets the secure endpoint as a synchronous client call, using the given `baseUri` as the starting point to create the target uri.
* If `baseUri` is not provided, the request will be a relative one.
*
* Returns a function which, when applied to the endpoint's security and regular input parameters (given as tuples), will encode them to
* appropriate request parameters: path, query, headers and body. The request is sent using a synchronous backend, and the result of
* decoding the response (error or success value) is returned. If decoding the result fails, an exception is thrown.
*/
def toQuickSecureClient[A, I, E, O](e: Endpoint[A, I, E, O, Any], baseUri: Option[Uri]): A => I => Either[E, O] = {
val backend = CurlBackend()
SttpClientInterpreter().toSecureClientThrowDecodeFailures(e, baseUri, backend)
}

/** Interprets the secure endpoint as a client call, using the given `baseUri` as the starting point to create the target uri. If
* `baseUri` is not provided, the request will be a relative one.
*
* Returns a function which, when applied to the endpoint's security and regular input parameters (given as tuples), will encode them to
* appropriate request parameters: path, query, headers and body. The request is sent using a synchronous backend, and the result
* (success value) is returned. If decoding the result fails, or if the response corresponds to an error value, an exception is thrown.
*/
def toQuickSecureClientThrowErrors[A, I, E, O](e: Endpoint[A, I, E, O, Any], baseUri: Option[Uri]): A => I => O = {
val backend = CurlBackend()
SttpClientInterpreter().toSecureClientThrowErrors(e, baseUri, backend)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package sttp.tapir.client.sttp

import cats.effect.IO
import cats.effect.unsafe.implicits.global
import cats.implicits._
import sttp.capabilities.fs2.Fs2Streams
import sttp.tapir.client.tests.ClientStreamingTests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package sttp.tapir.client.sttp

import cats.effect.IO
import cats.effect.std.Dispatcher
import cats.effect.unsafe.implicits.global
import sttp.capabilities.WebSockets
import sttp.capabilities.fs2.Fs2Streams
import sttp.client3._
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package sttp.tapir.client.sttp

import scala.util.Try
import cats.effect.IO

import sttp.tapir.{DecodeResult, Endpoint}
import sttp.tapir.client.tests.ClientTests
import sttp.client3._

object Backend {
val backend = CurlTryBackend(verbose = false)
}

abstract class SttpClientTests[R >: Any] extends ClientTests[R] {

val backend: SttpBackend[Try, R] = Backend.backend
def wsToPipe: WebSocketToPipe[R]

override def send[A, I, E, O](
e: Endpoint[A, I, E, O, R],
port: Port,
securityArgs: A,
args: I,
scheme: String = "http"
): IO[Either[E, O]] = {
implicit val wst: WebSocketToPipe[R] = wsToPipe
val response: Try[Either[E, O]] =
SttpClientInterpreter()
.toSecureRequestThrowDecodeFailures(e, Some(uri"$scheme://localhost:$port"))
.apply(securityArgs)
.apply(args)
.send(backend)
.map(_.body)
IO.fromTry(response)
}

override def safeSend[A, I, E, O](
e: Endpoint[A, I, E, O, R],
port: Port,
securityArgs: A,
args: I
): IO[DecodeResult[Either[E, O]]] = {
implicit val wst: WebSocketToPipe[R] = wsToPipe
def response: Try[DecodeResult[Either[E, O]]] =
SttpClientInterpreter()
.toSecureRequest(e, Some(uri"http://localhost:$port"))
.apply(securityArgs)
.apply(args)
.send(backend)
.map(_.body)
IO.fromTry(response)
}

override protected def afterAll(): Unit = {
backend.close()
super.afterAll()
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package sttp.tapir.client.tests

import cats.effect.unsafe.implicits.global
import sttp.model.{Header, MediaType, QueryParams, StatusCode}
import sttp.tapir._
import sttp.tapir.model.UsernamePassword
Expand Down Expand Up @@ -86,7 +85,10 @@ trait ClientBasicTests { this: ClientTests[Any] =>
// TODO: test root path
testClient(in_string_out_status, (), "apple", Right(StatusCode.Ok))

testClient(delete_endpoint, (), (), Right(()))
// DELETE fails in Scala Native. Not Supported by CurlBackend?
if (!platformIsScalaNative) {
testClient(delete_endpoint, (), (), Right(()))
}

testClient(
in_optional_json_out_optional_json.name("defined"),
Expand Down Expand Up @@ -133,7 +135,7 @@ trait ClientBasicTests { this: ClientTests[Any] =>
}

// the fetch API doesn't allow bodies in get requests
if (!platformIsScalaJS) {
if (!platformIsScalaJS && !platformIsScalaNative) {
test(in_json_out_headers.showDetail) {
send(in_json_out_headers, port, (), FruitAmount("apple", 10))
.unsafeToFuture()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package sttp.tapir.client.tests

import cats.effect.unsafe.implicits.global
import sttp.model.{MediaType, Part}
import sttp.tapir.tests.Multipart.{in_raw_multipart_out_string, in_simple_multipart_out_raw_string, in_simple_multipart_out_string}
import sttp.tapir.tests.data.{FruitAmount, FruitAmountWrapper}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package sttp.tapir.client.tests

import cats.effect.unsafe.implicits.global
import sttp.capabilities.Streams
import sttp.tapir.tests.Streaming.in_stream_out_stream

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,19 @@ package sttp.tapir.client.tests

import java.io.InputStream
import cats.effect._
import cats.effect.unsafe.implicits.global
import cats.effect.unsafe.IORuntime
import cats.implicits._
import org.scalatest.BeforeAndAfterAll
import org.scalatest.funsuite.AsyncFunSuite
import org.scalatest.matchers.should.Matchers
import sttp.tapir.tests.TestUtil._
import sttp.tapir.{DecodeResult, _}

import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.ExecutionContext

abstract class ClientTests[R] extends AsyncFunSuite with Matchers with BeforeAndAfterAll {
// Using the default ScalaTest execution context seems to cause issues on JS.
// https://github.com/scalatest/scalatest/issues/1039
implicit override val executionContext: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global
implicit val ioRT: IORuntime = ClientContext.ioRT
implicit override val executionContext: ExecutionContext = ioRT.compute

type Port = Int
var port: Port = 51823
Expand All @@ -26,24 +25,30 @@ abstract class ClientTests[R] extends AsyncFunSuite with Matchers with BeforeAnd
def testClient[A, I, E, O](e: Endpoint[A, I, E, O, R], securityArgs: A, args: I, expectedResult: Either[E, O]): Unit = {
test(e.showDetail) {
// adjust test result values to a form that is comparable by scalatest
def adjust(r: Either[Any, Any]): Future[Either[Any, Any]] = {
def adjust(r: Either[Any, Any]): IO[Either[Any, Any]] = {
def doAdjust(v: Any) =
v match {
case is: InputStream => Future.successful(inputStreamToByteArray(is).toList)
case a: Array[Byte] => Future.successful(a.toList)
case f: TapirFile => readFromFile(f)
case _ => Future.successful(v)
case is: InputStream => IO.pure(inputStreamToByteArray(is).toList)
case a: Array[Byte] => IO.pure(a.toList)
case f: TapirFile => IO.fromFuture(IO(readFromFile(f)))
case _ => IO.pure(v)
}

r.map(doAdjust).left.map(doAdjust).bisequence
}

for {
result <- send(e, port, securityArgs, args).unsafeToFuture()
(adjustedResult, adjustedExpectedResult) <- adjust(result).zip(adjust(expectedResult))
} yield adjustedResult shouldBe adjustedExpectedResult
val r = for {
result <- send(e, port, securityArgs, args)
adjustedResult <- adjust(result)
adjustedExpectedResult <- adjust(expectedResult)
} yield {
adjustedResult shouldBe adjustedExpectedResult
}

r.unsafeToFuture()
}
}

def platformIsScalaJS: Boolean = System.getProperty("java.vm.name") == "Scala.js"
def platformIsScalaJS: Boolean = ClientContext.platformIsScalaJS
def platformIsScalaNative: Boolean = ClientContext.platformIsScalaNative
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package sttp.tapir.client.tests

import cats.effect.IO
import cats.effect.unsafe.implicits.global
import sttp.capabilities.{Streams, WebSockets}
import sttp.tapir._
import sttp.tapir.json.circe._
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package sttp.tapir.client.tests

import cats.effect.unsafe.IORuntime
import scala.concurrent.ExecutionContext

object ClientContext {
// Using the default ScalaTest execution context seems to cause issues on JS.
// https://github.com/scalatest/scalatest/issues/1039
val executionContext: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global
val ioRT: IORuntime = cats.effect.unsafe.implicits.global

val platformIsScalaJS: Boolean = true
val platformIsScalaNative: Boolean = false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package sttp.tapir.client.tests

import cats.effect.unsafe.IORuntime
import scala.concurrent.ExecutionContext

object ClientContext {
// Using the default ScalaTest execution context seems to cause issues on JS.
// https://github.com/scalatest/scalatest/issues/1039
val executionContext: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global
val ioRT: IORuntime = cats.effect.unsafe.implicits.global

val platformIsScalaJS: Boolean = false
val platformIsScalaNative: Boolean = false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package sttp.tapir.client.tests

import cats.effect.unsafe.IORuntime
import scala.concurrent.ExecutionContext

object ClientContext {
// This two lines should be fine to define IO Runtime and ExcecutionContext on Scala Native, but ScalaTest AsyncFunSuite fails with Timeout exception
// val ioRT:IORuntime = cats.effect.unsafe.implicits.global
// val executionContext: ExecutionContext = ioRT.compute

// Inspired by the default ExecutionContext implementation in MUnit, MUnit works fine on Native
val executionContext: ExecutionContext = new ExecutionContext {
def execute(runnable: Runnable): Unit = runnable.run()
def reportFailure(cause: Throwable): Unit = cause.printStackTrace()
}
private val globalRT:IORuntime = cats.effect.unsafe.implicits.global
val ioRT:IORuntime = IORuntime.apply(executionContext,executionContext,globalRT.scheduler,globalRT.shutdown,globalRT.config)

val platformIsScalaJS: Boolean = false
val platformIsScalaNative: Boolean = true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package sttp.tapir.tests

import java.io.{File, PrintWriter}

import scala.concurrent.Future
import scala.io.Source

trait TestUtilExtensions {
def writeToFile(s: String): File = {
val f = File.createTempFile("test", "tapir")
new PrintWriter(f) { write(s); close() }
f.deleteOnExit()
f
}

def readFromFile(f: File): Future[String] = {
val s = Source.fromFile(f)
try {
Future.successful(s.mkString)
} finally {
s.close()
}
}
}
1 change: 1 addition & 0 deletions version.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ThisBuild/version := "1.1.3-SNAPSHOT"

0 comments on commit 30da7d7

Please sign in to comment.