Skip to content

Commit

Permalink
New static content API (#2837)
Browse files Browse the repository at this point in the history
  • Loading branch information
kciesielski authored Apr 27, 2023
1 parent d7d0ce0 commit 7a9e887
Show file tree
Hide file tree
Showing 55 changed files with 1,413 additions and 317 deletions.
18 changes: 16 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ lazy val rawAllAggregates = core.projectRefs ++
monixNewtype.projectRefs ++
zioPrelude.projectRefs ++
circeJson.projectRefs ++
files.projectRefs ++
jsoniterScala.projectRefs ++
prometheusMetrics.projectRefs ++
opentelemetryMetrics.projectRefs ++
Expand Down Expand Up @@ -422,6 +423,19 @@ lazy val core: ProjectMatrix = (projectMatrix in file("core"))
)
//.enablePlugins(spray.boilerplate.BoilerplatePlugin)

lazy val files: ProjectMatrix = (projectMatrix in file("files"))
.settings(commonJvmSettings)
.settings(
name := "tapir-files",
libraryDependencies ++= Seq(
scalaTest.value % Test
)
)
.jvmPlatform(scalaVersions = scala2And3Versions)
.jsPlatform(scalaVersions = scala2And3Versions)
.nativePlatform(scalaVersions = scala2And3Versions)
.dependsOn(core)

lazy val testing: ProjectMatrix = (projectMatrix in file("testing"))
.settings(commonSettings)
.settings(
Expand Down Expand Up @@ -453,7 +467,7 @@ lazy val tests: ProjectMatrix = (projectMatrix in file("tests"))
scalaVersions = scala2And3Versions,
settings = commonNativeSettings
)
.dependsOn(core, circeJson, cats)
.dependsOn(core, files, circeJson, cats)

val akkaHttpVanilla = taskKey[Unit]("akka-http-vanilla")
val akkaHttpTapir = taskKey[Unit]("akka-http-tapir")
Expand Down Expand Up @@ -1029,7 +1043,7 @@ lazy val swaggerUi: ProjectMatrix = (projectMatrix in file("docs/swagger-ui"))
libraryDependencies ++= Seq("org.webjars" % "swagger-ui" % Versions.swaggerUi)
)
.jvmPlatform(scalaVersions = scala2And3Versions)
.dependsOn(core)
.dependsOn(core, files)

lazy val swaggerUiBundle: ProjectMatrix = (projectMatrix in file("docs/swagger-ui-bundle"))
.settings(commonJvmSettings)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package sttp.tapir.client.http4s

import cats.Applicative
import cats.effect.Async
import cats.effect.{Async, Sync}
import cats.implicits._
import fs2.Chunk
import fs2.io.file.Files
Expand All @@ -23,14 +23,16 @@ import sttp.tapir.{
EndpointInput,
EndpointOutput,
FileRange,
InputStreamRange,
Mapping,
RawBodyType,
StreamBodyIO,
WebSocketBodyOutput
}

import java.io.{ByteArrayInputStream, InputStream}
import java.io.{InputStream}
import java.nio.ByteBuffer
import java.io.ByteArrayInputStream

private[http4s] class EndpointToHttp4sClient(clientOptions: Http4sClientOptions) {

Expand Down Expand Up @@ -147,6 +149,9 @@ private[http4s] class EndpointToHttp4sClient(clientOptions: Http4sClientOptions)
case RawBodyType.InputStreamBody =>
val entityEncoder = EntityEncoder.inputStreamEncoder[F, InputStream]
req.withEntity(Applicative[F].pure(encoded.asInstanceOf[InputStream]))(entityEncoder)
case RawBodyType.InputStreamRangeBody =>
val entityEncoder = EntityEncoder.inputStreamEncoder[F, InputStream]
req.withEntity(Sync[F].blocking(encoded.asInstanceOf[InputStreamRange].inputStream()))(entityEncoder)
case RawBodyType.FileBody =>
val entityEncoder = EntityEncoder.fileEncoder[F]
req.withEntity(encoded.asInstanceOf[FileRange].file)(entityEncoder)
Expand Down Expand Up @@ -226,6 +231,12 @@ private[http4s] class EndpointToHttp4sClient(clientOptions: Http4sClientOptions)
response.body.compile.toVector.map(_.toArray).map(java.nio.ByteBuffer.wrap).map(_.asInstanceOf[Any])
case RawBodyType.InputStreamBody =>
response.body.compile.toVector.map(_.toArray).map(new ByteArrayInputStream(_)).map(_.asInstanceOf[Any])
case RawBodyType.InputStreamRangeBody =>
response.body.compile.toVector
.map(_.toArray)
.map(new ByteArrayInputStream(_))
.map(stream => InputStreamRange(() => stream))
.map(_.asInstanceOf[Any])
case RawBodyType.FileBody =>
val file = clientOptions.createFile()
response.body.through(Files[F].writeAll(file.toPath)).compile.drain.map(_ => FileRange(file))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import sttp.tapir.{
EndpointInput,
EndpointOutput,
FileRange,
InputStreamRange,
Mapping,
RawBodyType,
StreamBodyIO,
Expand Down Expand Up @@ -189,6 +190,9 @@ private[play] class EndpointToPlayClient(clientOptions: PlayClientOptions, ws: S
// For some reason, Play comes with a Writeable for Supplier[InputStream] but not InputStream directly
val inputStreamSupplier: Supplier[InputStream] = () => encoded.asInstanceOf[InputStream]
req.withBody(inputStreamSupplier)
case RawBodyType.InputStreamRangeBody =>
val inputStreamSupplier: Supplier[InputStream] = new Supplier[InputStream] { override def get(): InputStream = encoded.inputStream() }
req.withBody(inputStreamSupplier)
case RawBodyType.FileBody => req.withBody(encoded.asInstanceOf[FileRange].file)
case _: RawBodyType.MultipartBody => throw new IllegalArgumentException("Multipart body aren't supported")
}
Expand Down Expand Up @@ -224,6 +228,8 @@ private[play] class EndpointToPlayClient(clientOptions: PlayClientOptions, ws: S
case RawBodyType.ByteArrayBody => response.body[Array[Byte]]
case RawBodyType.ByteBufferBody => response.body[ByteBuffer]
case RawBodyType.InputStreamBody => new ByteArrayInputStream(response.body[Array[Byte]])
case RawBodyType.InputStreamRangeBody =>
InputStreamRange(() => new ByteArrayInputStream(response.body[Array[Byte]]))
case RawBodyType.FileBody =>
// TODO Consider using bodyAsSource to not load the whole content in memory
val f = clientOptions.createFile()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,12 @@ private[sttp] class EndpointToSttpClient[R](clientOptions: SttpClientOptions, ws
): PartialAnyRequest = {
val encoded = codec.encode(v)
val req2 = bodyType match {
case RawBodyType.StringBody(charset) => req.body(encoded, charset.name())
case RawBodyType.ByteArrayBody => req.body(encoded)
case RawBodyType.ByteBufferBody => req.body(encoded)
case RawBodyType.InputStreamBody => req.body(encoded)
case RawBodyType.FileBody => req.body(encoded.asInstanceOf[FileRange].file)
case RawBodyType.StringBody(charset) => req.body(encoded, charset.name())
case RawBodyType.ByteArrayBody => req.body(encoded)
case RawBodyType.ByteBufferBody => req.body(encoded)
case RawBodyType.InputStreamBody => req.body(encoded)
case RawBodyType.FileBody => req.body(encoded.file)
case RawBodyType.InputStreamRangeBody => req.body(encoded.inputStream())
case m: RawBodyType.MultipartBody =>
val parts: Seq[Part[RequestBody[Any]]] = (encoded: Seq[RawPart]).flatMap { p =>
m.partType(p.name).map { partType =>
Expand All @@ -189,12 +190,13 @@ private[sttp] class EndpointToSttpClient[R](clientOptions: SttpClientOptions, ws

private def partToSttpPart[T](p: Part[T], bodyType: RawBodyType[T]): Part[RequestBody[Any]] =
bodyType match {
case RawBodyType.StringBody(charset) => multipart(p.name, p.body, charset.toString)
case RawBodyType.ByteArrayBody => multipart(p.name, p.body)
case RawBodyType.ByteBufferBody => multipart(p.name, p.body)
case RawBodyType.InputStreamBody => multipart(p.name, p.body)
case RawBodyType.FileBody => multipartFile(p.name, p.body.asInstanceOf[FileRange].file)
case RawBodyType.MultipartBody(_, _) => throw new IllegalArgumentException("Nested multipart bodies aren't supported")
case RawBodyType.StringBody(charset) => multipart(p.name, p.body, charset.toString)
case RawBodyType.ByteArrayBody => multipart(p.name, p.body)
case RawBodyType.ByteBufferBody => multipart(p.name, p.body)
case RawBodyType.InputStreamBody => multipart(p.name, p.body)
case RawBodyType.FileBody => multipartFile(p.name, p.body.asInstanceOf[FileRange].file)
case RawBodyType.InputStreamRangeBody => multipart(p.name, p.body.inputStream())
case RawBodyType.MultipartBody(_, _) => throw new IllegalArgumentException("Nested multipart bodies aren't supported")
}

private def responseAsFromOutputs(out: EndpointOutput[_], isWebSocket: Boolean): ResponseAs[Any, Any] = {
Expand All @@ -204,12 +206,13 @@ private[sttp] class EndpointToSttpClient[R](clientOptions: SttpClientOptions, ws
case (None, false) =>
out.bodyType
.map {
case RawBodyType.StringBody(charset) => asStringAlways(charset.name())
case RawBodyType.ByteArrayBody => asByteArrayAlways
case RawBodyType.ByteBufferBody => asByteArrayAlways.map(ByteBuffer.wrap)
case RawBodyType.InputStreamBody => asByteArrayAlways.map(new ByteArrayInputStream(_))
case RawBodyType.FileBody => asFileAlways(clientOptions.createFile()).map(d => FileRange(d))
case RawBodyType.MultipartBody(_, _) => throw new IllegalArgumentException("Multipart bodies aren't supported in responses")
case RawBodyType.StringBody(charset) => asStringAlways(charset.name())
case RawBodyType.ByteArrayBody => asByteArrayAlways
case RawBodyType.ByteBufferBody => asByteArrayAlways.map(ByteBuffer.wrap)
case RawBodyType.InputStreamBody => asByteArrayAlways.map(new ByteArrayInputStream(_))
case RawBodyType.FileBody => asFileAlways(clientOptions.createFile()).map(d => FileRange(d))
case RawBodyType.InputStreamRangeBody => asByteArrayAlways.map(b => InputStreamRange(() => new ByteArrayInputStream(b)))
case RawBodyType.MultipartBody(_, _) => throw new IllegalArgumentException("Multipart bodies aren't supported in responses")
}
.getOrElse(ignore)
}).asInstanceOf[ResponseAs[Any, Any]]
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/sttp/tapir/Codec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ object Codec extends CodecExtensions with CodecExtensions2 with FormCodecMacros
id[Array[Byte], OctetStream](OctetStream(), Schema.schemaForByteArray)
implicit val inputStream: Codec[InputStream, InputStream, OctetStream] =
id[InputStream, OctetStream](OctetStream(), Schema.schemaForInputStream)
implicit val inputStreamRange: Codec[InputStreamRange, InputStreamRange, OctetStream] =
id[InputStreamRange, OctetStream](OctetStream(), Schema.schemaForInputStreamRange)
implicit val byteBuffer: Codec[ByteBuffer, ByteBuffer, OctetStream] =
id[ByteBuffer, OctetStream](OctetStream(), Schema.schemaForByteBuffer)
implicit val fileRange: Codec[FileRange, FileRange, OctetStream] =
Expand Down Expand Up @@ -735,6 +737,7 @@ object RawBodyType {
case object ByteBufferBody extends Binary[ByteBuffer]
case object InputStreamBody extends Binary[InputStream]
case object FileBody extends Binary[FileRange]
case object InputStreamRangeBody extends Binary[InputStreamRange]

case class MultipartBody(partTypes: Map[String, RawBodyType[_]], defaultType: Option[RawBodyType[_]]) extends RawBodyType[Seq[RawPart]] {
def partType(name: String): Option[RawBodyType[_]] = partTypes.get(name).orElse(defaultType)
Expand Down
17 changes: 17 additions & 0 deletions core/src/main/scala/sttp/tapir/InputStreamRange.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package sttp.tapir

import java.io.InputStream

case class InputStreamRange(inputStream: () => InputStream, range: Option[RangeValue] = None) {
def inputStreamFromRangeStart: () => InputStream = range.flatMap(_.start) match {
case Some(start) if start > 0 =>
() =>
val openedStream = inputStream()
val skipped = openedStream.skip(start)
if (skipped == start)
openedStream
else
throw new IllegalArgumentException(s"Illegal range start: $start, could skip only $skipped bytes")
case _ => inputStream
}
}
1 change: 1 addition & 0 deletions core/src/main/scala/sttp/tapir/Schema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ object Schema extends LowPrioritySchema with SchemaCompanionMacros {
implicit val schemaForByteArray: Schema[Array[Byte]] = Schema(SBinary())
implicit val schemaForByteBuffer: Schema[ByteBuffer] = Schema(SBinary())
implicit val schemaForInputStream: Schema[InputStream] = Schema(SBinary())
implicit val schemaForInputStreamRange: Schema[InputStreamRange] = Schema(SchemaType.SBinary())
implicit val schemaForInstant: Schema[Instant] = Schema(SDateTime())
implicit val schemaForZonedDateTime: Schema[ZonedDateTime] = Schema(SDateTime())
implicit val schemaForOffsetDateTime: Schema[OffsetDateTime] = Schema(SDateTime())
Expand Down
1 change: 1 addition & 0 deletions core/src/main/scala/sttp/tapir/Tapir.scala
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ trait Tapir extends TapirExtensions with TapirComputedInputs with TapirStaticCon
def byteArrayBody: EndpointIO.Body[Array[Byte], Array[Byte]] = rawBinaryBody(RawBodyType.ByteArrayBody)
def byteBufferBody: EndpointIO.Body[ByteBuffer, ByteBuffer] = rawBinaryBody(RawBodyType.ByteBufferBody)
def inputStreamBody: EndpointIO.Body[InputStream, InputStream] = rawBinaryBody(RawBodyType.InputStreamBody)
def inputStreamRangeBody: EndpointIO.Body[InputStreamRange, InputStreamRange] = rawBinaryBody(RawBodyType.InputStreamRangeBody)
def fileRangeBody: EndpointIO.Body[FileRange, FileRange] = rawBinaryBody(RawBodyType.FileBody)
def fileBody: EndpointIO.Body[FileRange, TapirFile] = rawBinaryBody(RawBodyType.FileBody).map(_.file)(d => FileRange(d))

Expand Down
2 changes: 2 additions & 0 deletions core/src/main/scalajvm/sttp/tapir/static/Files.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import java.nio.file.{LinkOption, Path, Paths}
import java.time.Instant
import scala.annotation.tailrec

@deprecated("Use sttp.tapir.files.Files", since = "1.3.0")
object Files {

// inspired by org.http4s.server.staticcontent.FileService
Expand Down Expand Up @@ -157,6 +158,7 @@ object Files {
* path segments (relative to the system path from which files are read) of the file to return in case the one requested by the user
* isn't found. This is useful for SPA apps, where the same main application file needs to be returned regardless of the path.
*/
@deprecated("Use sttp.tapir.files.FilesOptions", since = "1.3.0")
case class FilesOptions[F[_]](
calculateETag: MonadError[F] => File => F[Option[ETag]],
fileFilter: List[String] => Boolean,
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/scalajvm/sttp/tapir/static/Resources.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import java.io.{File, FileNotFoundException, InputStream}
import java.net.URL
import java.time.Instant

@deprecated("Use sttp.tapir.files.Resources", since = "1.3.0")
object Resources {
def apply[F[_]: MonadError](
classLoader: ClassLoader,
Expand Down Expand Up @@ -100,6 +101,7 @@ object Resources {
* the user isn't found. This is useful for SPA apps, where the same main application resource needs to be returned regardless of the
* path.
*/
@deprecated("Use new files API and FileOptions, see sttp.tapir.files.Resources", since = "1.3.0")
case class ResourcesOptions[F[_]](
useETags: Boolean,
useGzippedIfAvailable: Boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,18 @@ trait TapirStaticContentEndpoints {
)
}

@deprecated("Use sttp.tapir.files.staticFilesGetEndpoint", since = "1.3.0")
lazy val filesGetEndpoint: PublicEndpoint[StaticInput, StaticErrorOutput, StaticOutput[FileRange], Any] = staticGetEndpoint(fileRangeBody)

@deprecated("Use sttp.tapir.files.staticResourcesGetEndpoint", since = "1.3.0")
lazy val resourcesGetEndpoint: PublicEndpoint[StaticInput, StaticErrorOutput, StaticOutput[InputStream], Any] =
staticGetEndpoint(inputStreamBody)

@deprecated("Use sttp.tapir.files.staticFilesGetEndpoint", since = "1.3.0")
def filesGetEndpoint(prefix: EndpointInput[Unit]): PublicEndpoint[StaticInput, StaticErrorOutput, StaticOutput[FileRange], Any] =
filesGetEndpoint.prependIn(prefix)

@deprecated("Use sttp.tapir.files.staticResourcesGetEndpoint", since = "1.3.0")
def resourcesGetEndpoint(prefix: EndpointInput[Unit]): PublicEndpoint[StaticInput, StaticErrorOutput, StaticOutput[InputStream], Any] =
resourcesGetEndpoint.prependIn(prefix)

Expand All @@ -161,6 +168,7 @@ trait TapirStaticContentEndpoints {
*
* A request to `/static/files/css/styles.css` will try to read the `/home/app/static/css/styles.css` file.
*/
@deprecated("Use sttp.tapir.files.staticFilesGetServerEndpoint", since = "1.3.0")
def filesGetServerEndpoint[F[_]](
prefix: EndpointInput[Unit]
)(systemPath: String, options: FilesOptions[F] = FilesOptions.default[F]): ServerEndpoint[Any, F] =
Expand All @@ -169,6 +177,7 @@ trait TapirStaticContentEndpoints {
/** A server endpoint, used to verify if sever supports range requests for file under particular path Additionally it verify file
* existence and returns its size
*/
@deprecated("Use sttp.tapir.files.staticFilesHeadServerEndpoint", since = "1.3.0")
def filesHeadServerEndpoint[F[_]](
prefix: EndpointInput[Unit]
)(systemPath: String, options: FilesOptions[F] = FilesOptions.default[F]): ServerEndpoint[Any, F] =
Expand All @@ -183,6 +192,7 @@ trait TapirStaticContentEndpoints {
*
* A request to `/static/files/css/styles.css` will try to read the `/home/app/static/css/styles.css` file.
*/
@deprecated("Use sttp.tapir.files.staticFilesServerEndpoints", since = "1.3.0")
def filesServerEndpoints[F[_]](
prefix: EndpointInput[Unit]
)(systemPath: String, options: FilesOptions[F] = FilesOptions.default[F]): List[ServerEndpoint[Any, F]] =
Expand All @@ -194,6 +204,7 @@ trait TapirStaticContentEndpoints {
* fileGetServerEndpoint("static" / "hello.html")("/home/app/static/data.html")
* }}}
*/
@deprecated("Use sttp.tapir.files.staticFileGetServerEndpoint", since = "1.3.0")
def fileGetServerEndpoint[F[_]](prefix: EndpointInput[Unit])(systemPath: String): ServerEndpoint[Any, F] =
ServerEndpoint.public(removePath(filesGetEndpoint(prefix)), (m: MonadError[F]) => Files.get(systemPath)(m))

Expand All @@ -206,6 +217,7 @@ trait TapirStaticContentEndpoints {
*
* A request to `/static/files/css/styles.css` will try to read the `/app/css/styles.css` resource.
*/
@deprecated("Use sttp.tapir.files.staticResourcesGetServerEndpoint", since = "1.3.0")
def resourcesGetServerEndpoint[F[_]](prefix: EndpointInput[Unit])(
classLoader: ClassLoader,
resourcePrefix: String,
Expand All @@ -222,6 +234,7 @@ trait TapirStaticContentEndpoints {
* resourceGetServerEndpoint("static" / "hello.html")(classOf[App].getClassLoader, "app/data.html")
* }}}
*/
@deprecated("Use sttp.tapir.files.staticResourceGetServerEndpoint", since = "1.3.0")
def resourceGetServerEndpoint[F[_]](prefix: EndpointInput[Unit])(
classLoader: ClassLoader,
resourcePath: String,
Expand Down
Loading

0 comments on commit 7a9e887

Please sign in to comment.