Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: Content Type Fixes #1008

Merged
merged 8 commits into from
Feb 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 12 additions & 13 deletions zio-http/src/main/scala/zhttp/http/Http.scala
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,12 @@ sealed trait Http[-R, +E, -A, +B] extends (A => ZIO[R, Option[E], B]) { self =>
final def contentLength(implicit eb: IsResponse[B]): Http[R, E, A, Option[Long]] =
headers.map(_.contentLength)

/**
* Extracts the value of ContentType header
*/
final def contentType(implicit eb: IsResponse[B]): Http[R, E, A, Option[CharSequence]] =
headerValue(HttpHeaderNames.CONTENT_TYPE)

/**
* Transforms the input of the http before passing it on to the current Http
*/
Expand Down Expand Up @@ -205,13 +211,6 @@ sealed trait Http[-R, +E, -A, +B] extends (A => ZIO[R, Option[E], B]) { self =>
final def headerValue(name: CharSequence)(implicit eb: IsResponse[B]): Http[R, E, A, Option[CharSequence]] =
headers.map(_.headerValue(name))

/**
* Extracts the value of ContentType header
*/
final def contentType(implicit eb: IsResponse[B]): Http[R, E, A, Option[CharSequence]] = headerValue(
HttpHeaderNames.CONTENT_TYPE,
)

/**
* Extracts the `Headers` from the type `B` if possible
*/
Expand Down Expand Up @@ -247,12 +246,6 @@ sealed trait Http[-R, +E, -A, +B] extends (A => ZIO[R, Option[E], B]) { self =>
final def orElse[R1 <: R, E1, A1 <: A, B1 >: B](other: Http[R1, E1, A1, B1]): Http[R1, E1, A1, B1] =
self.catchAll(_ => other)

/**
* Provides the environment to Http.
*/
final def provideEnvironment(r: R)(implicit ev: NeedsEnv[R]): Http[Any, E, A, B] =
Http.fromOptionFunction[A](a => self(a).provide(r))

/**
* Provide part of the environment to HTTP that is not part of ZEnv
*/
Expand All @@ -261,6 +254,12 @@ sealed trait Http[-R, +E, -A, +B] extends (A => ZIO[R, Option[E], B]) { self =>
)(implicit ev: ZEnv with R1 <:< R, tagged: Tag[R1]): Http[ZEnv, E1, A, B] =
Http.fromOptionFunction[A](a => self(a).provideCustomLayer(layer.mapError(Option(_))))

/**
* Provides the environment to Http.
*/
final def provideEnvironment(r: R)(implicit ev: NeedsEnv[R]): Http[Any, E, A, B] =
Http.fromOptionFunction[A](a => self(a).provide(r))

/**
* Provides layer to Http.
*/
Expand Down
26 changes: 15 additions & 11 deletions zio-http/src/main/scala/zhttp/http/MediaType.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,36 @@ package zhttp.http

import java.util

final case class MediaType private[zhttp] (
final case class MediaType(
mainType: String,
subType: String,
compressible: Boolean = false,
binary: Boolean = false,
fileExtensions: List[String] = Nil,
extensions: Map[String, String] = Map.empty,
)
) {
def fullType: String = s"$mainType/$subType"
}

object MediaType extends MimeDB {

val memoiseMap: util.HashMap[String, Option[String]] = new util.HashMap()
private val memoizeMap: util.HashMap[String, Option[String]] = new util.HashMap()

def forExtention(ext: String): Option[MediaType] = extensionMap.get(ext.toLowerCase)
private val extensionMap: Map[String, MediaType] = allMediaTypes.flatMap(m => m.fileExtensions.map(_ -> m)).toMap

val extensionMap: Map[String, MediaType] = allMediaTypes.flatMap(m => m.fileExtensions.map(_ -> m)).toMap
def probe(ext: String): Option[MediaType] = extensionMap.get(ext.toLowerCase)

def probeContentType(name: String): Option[String] = {
if (memoiseMap.containsKey(name))
memoiseMap.get(name)
else {
def probeContentType(name: String, cache: Boolean = false): Option[String] = {
if (memoizeMap.containsKey(name) && cache) {
memoizeMap.get(name)
} else {
val contentType = name.lastIndexOf(".") match {
case -1 => None
case i => forExtention(name.substring(i + 1)).map(m => m.mainType + "/" + m.subType)
case i => probe(name.substring(i + 1)).map(_.fullType)
}
if (cache) {
memoizeMap.put(name, contentType)
}
memoiseMap.put(name, contentType)
contentType
}
}
Expand Down
10 changes: 5 additions & 5 deletions zio-http/src/main/scala/zhttp/http/MimeDB.scala
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package zhttp.http

private[zhttp] trait MimeDB {
lazy val allMediaTypes: List[MediaType] =
private[zhttp] lazy val allMediaTypes: List[MediaType] =
Nil ++ x_shader.all ++ x_conference.all ++ video.all ++ text.all ++ multipart.all ++ model.all ++ message.all ++ image.all ++ font.all ++ chemical.all ++ audio.all ++ application.all
val Compressible: Boolean = true
val Uncompressible: Boolean = false
val Binary: Boolean = true
val NotBinary: Boolean = false
private val Compressible: Boolean = true
private val Uncompressible: Boolean = false
private val Binary: Boolean = true
private val NotBinary: Boolean = false
private[zhttp] object application_parts {
trait application_0 {
lazy val `1d-interleaved-parityfec`: MediaType =
Expand Down
15 changes: 12 additions & 3 deletions zio-http/src/main/scala/zhttp/http/Response.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ final case class Response private (
def setAttribute(attribute: Response.Attribute): Response =
self.copy(attribute = attribute)

/**
* Sets the MediaType of the response using the `Content-Type` header.
*/
def setMediaType(mediaType: MediaType): Response = self.addHeader(HttpHeaderNames.CONTENT_TYPE, mediaType.fullType)

/**
* Sets the status of the response
*/
Expand Down Expand Up @@ -82,9 +87,13 @@ final case class Response private (
case HttpData.BinaryStream(_) => null
case HttpData.Empty => Unpooled.EMPTY_BUFFER
case HttpData.File(file) =>
MediaType.probeContentType(file.toPath.toString) match {
case Some(cType) => jHeaders.set(HttpHeaderNames.CONTENT_TYPE, cType)
case None => ()
if (!jHeaders.contains(HttpHeaderNames.CONTENT_TYPE)) {

// TODO: content-type probing cache should be configurable at server level
MediaType.probeContentType(file.toPath.toString) match {
case Some(cType) => jHeaders.set(HttpHeaderNames.CONTENT_TYPE, cType)
case None => ()
}
}
jHeaders.set(HttpHeaderNames.CONTENT_LENGTH, file.length())
null
Expand Down
60 changes: 13 additions & 47 deletions zio-http/src/test/scala/zhttp/http/ContentTypeSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,76 +8,42 @@ import zio.test.Assertion.{equalTo, isNone, isSome}
import zio.test.TestAspect.timeout
import zio.test.assertM

import java.io.File

object ContentTypeSpec extends HttpRunnableSpec {

private val env = EventLoopGroup.nio() ++ ChannelFactory.nio ++ ServerChannelFactory.nio ++ DynamicServer.live

val contentSpec = suite("Content type header on file response") {
testM("mp4") {
val file = new File(getClass.getResource("/TestFile2.mp4").getPath)
val res = Http
.fromFile(file)
.deploy
.contentType
.run()

val res = Http.fromResource("/TestFile2.mp4").deploy.contentType.run()
assertM(res)(isSome(equalTo("video/mp4")))
} +
testM("js") {
val file = new File(getClass.getResource("/TestFile3.js").getPath)
val res = Http
.fromFile(file)
.deploy
.contentType
.run()

val res = Http.fromResource("/TestFile3.js").deploy.contentType.run()
assertM(res)(isSome(equalTo("application/javascript")))
} +
testM("no extension") {
val file = new File(getClass.getResource("/TestFile4").getPath)
val res = Http
.fromFile(file)
.deploy
.contentType
.run()

val res = Http.fromResource("/TestFile4").deploy.contentType.run()
assertM(res)(isNone)

} +
testM("css") {
val file = new File(getClass.getResource("/TestFile5.css").getPath)
val res = Http
.fromFile(file)
.deploy
.contentType
.run()

val res = Http.fromResource("/TestFile5.css").deploy.contentType.run()
assertM(res)(isSome(equalTo("text/css")))
} +
testM("mp3") {
val file = new File(getClass.getResource("/TestFile6.mp3").getPath)
val res = Http
.fromFile(file)
.deploy
.contentType
.run()

val res = Http.fromResource("/TestFile6.mp3").deploy.contentType.run()
assertM(res)(isSome(equalTo("audio/mpeg")))
} +
testM("unidentified extension") {
val file = new File(getClass.getResource("/truststore.jks").getPath)
val res = Http
.fromFile(file)
.deploy
.contentType
.run()

val res = Http.fromResource("/truststore.jks").deploy.contentType.run()
assertM(res)(isNone)
} +
testM("already set content-type") {
val expected = MediaType.application.`json`
val res = Http.fromResource("/TestFile6.mp3").map(_.setMediaType(expected)).deploy.contentType.run()
assertM(res)(isSome(equalTo(expected.fullType)))
}
}

private val env = EventLoopGroup.nio() ++ ChannelFactory.nio ++ ServerChannelFactory.nio ++ DynamicServer.live

override def spec = {
suiteM("Content-type") {
serve(DynamicServer.app).as(List(contentSpec)).useNow
Expand Down
77 changes: 37 additions & 40 deletions zio-http/src/test/scala/zhttp/service/ServerSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import zio.test.Assertion._
import zio.test.TestAspect._
import zio.test._

import java.io.File
import java.nio.file.Paths

object ServerSpec extends HttpRunnableSpec {
Expand Down Expand Up @@ -116,6 +115,40 @@ object ServerSpec extends HttpRunnableSpec {
}
}

def nonZIOSpec = suite("NonZIOSpec") {
testM("200 response") {
checkAllM(HttpGen.method) { method =>
val actual = status(method, !! / "HExitSuccess")
assertM(actual)(equalTo(Status.OK))
}
} +
testM("500 response") {
val methodGenWithoutHEAD: Gen[Any, Method] = Gen.fromIterable(
List(
Method.OPTIONS,
Method.GET,
Method.POST,
Method.PUT,
Method.PATCH,
Method.DELETE,
Method.TRACE,
Method.CONNECT,
),
)
checkAllM(methodGenWithoutHEAD) { method =>
val actual = status(method, !! / "HExitFailure")
assertM(actual)(equalTo(Status.INTERNAL_SERVER_ERROR))
}
} +
testM("404 response ") {
checkAllM(HttpGen.method) { method =>
val actual = status(method, !! / "A")
assertM(actual)(equalTo(Status.NOT_FOUND))
}
}

}

def requestSpec = suite("RequestSpec") {
val app: HttpApp[Any, Nothing] = Http.collect[Request] { case req =>
Response.text(req.contentLength.getOrElse(-1).toString)
Expand All @@ -141,15 +174,13 @@ object ServerSpec extends HttpRunnableSpec {
}
} +
testM("data from file") {
val file = new File(getClass.getResource("/TestFile.txt").getPath)
val res = Http.fromFile(file).deploy.bodyAsString.run()
val res = Http.fromResource("/TestFile.txt").deploy.bodyAsString.run()
assertM(res)(equalTo("abc\nfoo"))
} +
testM("content-type header on file response") {
val file = new File(getClass.getResource("/TestFile2.mp4").getPath)
val res =
val res =
Http
.fromFile(file)
.fromResource("/TestFile2.mp4")
.deploy
.headerValue(HeaderNames.contentType)
.run()
Expand Down Expand Up @@ -271,38 +302,4 @@ object ServerSpec extends HttpRunnableSpec {
} yield assertTrue(data == Status.OK)
}
}

def nonZIOSpec = suite("NonZIOSpec") {
testM("200 response") {
checkAllM(HttpGen.method) { method =>
val actual = status(method, !! / "HExitSuccess")
assertM(actual)(equalTo(Status.OK))
}
} +
testM("500 response") {
val methodGenWithoutHEAD: Gen[Any, Method] = Gen.fromIterable(
List(
Method.OPTIONS,
Method.GET,
Method.POST,
Method.PUT,
Method.PATCH,
Method.DELETE,
Method.TRACE,
Method.CONNECT,
),
)
checkAllM(methodGenWithoutHEAD) { method =>
val actual = status(method, !! / "HExitFailure")
assertM(actual)(equalTo(Status.INTERNAL_SERVER_ERROR))
}
} +
testM("404 response ") {
checkAllM(HttpGen.method) { method =>
val actual = status(method, !! / "A")
assertM(actual)(equalTo(Status.NOT_FOUND))
}
}

}
}