Skip to content

Commit

Permalink
Generate Mime types (#2918)
Browse files Browse the repository at this point in the history
* generate media types

* remove unnecessarry settings

* regenerate github workflow

* format

* scalafix

---------

Co-authored-by: John A. De Goes <john@degoes.net>
  • Loading branch information
kitlangton and jdegoes authored Jun 19, 2024
1 parent 3bc7796 commit 8de4a41
Show file tree
Hide file tree
Showing 6 changed files with 10,094 additions and 7,536 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ jobs:
run: sbt '++ ${{ matrix.scala }}' zioHttpShadedTests/test

- name: Compress target directories
run: tar cf targets.tar sbt-zio-http-grpc/target zio-http-cli/target target zio-http/jvm/target zio-http-docs/target sbt-zio-http-grpc-tests/target zio-http-gen/target zio-http-benchmarks/target zio-http-example/target zio-http-testkit/target zio-http/js/target zio-http-htmx/target project/target
run: tar cf targets.tar sbt-zio-http-grpc/target zio-http-cli/target target zio-http/jvm/target zio-http-docs/target sbt-zio-http-grpc-tests/target zio-http-gen/target zio-http-benchmarks/target zio-http-tools/target zio-http-example/target zio-http-testkit/target zio-http/js/target zio-http-htmx/target project/target

- name: Upload target directories
uses: actions/upload-artifact@v4
Expand Down
1 change: 1 addition & 0 deletions aliases.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ addCommandAlias("fmt", "scalafmt; Test / scalafmt; sFix;")
addCommandAlias("fmtCheck", "scalafmtCheck; Test / scalafmtCheck; sFixCheck")
addCommandAlias("sFix", "scalafix OrganizeImports; Test / scalafix OrganizeImports")
addCommandAlias("sFixCheck", "scalafix --check OrganizeImports; Test / scalafix --check OrganizeImports")
addCommandAlias("generateMediaTypes", "zioHttpTools/runMain zio.http.tools.GenerateMediaTypes")

onLoadMessage := {
import scala.Console._
Expand Down
7 changes: 7 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ lazy val aggregatedProjects: Seq[ProjectReference] =
zioHttpHtmx,
zioHttpExample,
zioHttpTestkit,
zioHttpTools,
docs,
)
}
Expand Down Expand Up @@ -290,6 +291,12 @@ lazy val zioHttpExample = (project in file("zio-http-example"))
)
.dependsOn(zioHttpJVM, zioHttpCli, zioHttpGen)

lazy val zioHttpTools = (project in file("zio-http-tools"))
.settings(stdSettings("zio-http-tools"))
.settings(publishSetting(false))
.settings(runSettings(Debug.Main))
.dependsOn(zioHttpJVM)

lazy val zioHttpGen = (project in file("zio-http-gen"))
.settings(stdSettings("zio-http-gen"))
.settings(publishSetting(true))
Expand Down
173 changes: 173 additions & 0 deletions zio-http-tools/src/main/scala/zio/http/tools/GenerateMediaTypes.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package zio.http.tools

import scala.io.Source

import zio._
import zio.json._

/////////////////////////
// READING AND PARSING //
/////////////////////////

case class MimeType(
source: Option[String],
extensions: Option[List[String]] = None,
compressible: Option[Boolean] = None,
charset: Option[String] = None,
)

object MimeType {
implicit val decoder: JsonDecoder[MimeType] = DeriveJsonDecoder.gen[MimeType]
}

case class MimeDb(mimeTypes: Map[String, MimeType]) extends AnyVal {
def extend(extraTypes: Map[String, MimeType]): MimeDb = {
MimeDb(mimeTypes ++ extraTypes)
}
}

object MimeDb {
implicit val decoder: JsonDecoder[MimeDb] = JsonDecoder.map[String, MimeType].map(MimeDb(_))

// These types are not in the mime-db json
val extraTypes = Map(
"text/event-stream" -> MimeType(None, None, Some(true), None),
)

// Fetches the MIME types database from the jshttp/mime-db repository and
// returns a MimeDb object
def fetch: Task[MimeDb] = ZIO.attemptBlocking {
val url = "https://raw.githubusercontent.com/jshttp/mime-db/master/db.json"

val source = Source.fromURL(url)
val jsonData =
try { source.mkString }
finally { source.close() }

jsonData.fromJson[MimeDb] match {
case Right(db) => db.extend(extraTypes)
case Left(error) => throw new RuntimeException(s"Failed to parse JSON: $error")
}
}
}

///////////////
// RENDERING //
///////////////

object RenderUtils {
def snakeCase(s: String): String = {
s.toLowerCase.replace("-", "_")
}

// hello -> "hello"
def renderString(s: String): String = {
"\"" + s + "\""
}

// hello there -> `hello there`
def renderEscaped(s: String): String = {
"`" + s + "`"
}
}

case class MediaTypeInfo(
mainType: String,
subType: String,
compressible: Boolean,
extensions: List[String],
) {

def binary: Boolean = {
// If the main type is "image", "video", "audio", or "application", it is likely binary.
// Additionally, if the MIME type is not compressible, it is likely binary.
mainType match {
case "image" | "video" | "audio" | "font" | "model" | "multipart" => true
case "application" => !subType.startsWith("xml") && !subType.endsWith("json") && !subType.endsWith("javascript")
case "text" => false
case _ => !compressible
}
}

// Renders the media type info as a Scala code snippet
def render: String = {
val extensionsString =
if (extensions.isEmpty) ""
else s", ${extensions.map { string => s""""$string"""" }.mkString("List(", ", ", ")")}"

s"""
lazy val `${subType}`: MediaType =
new MediaType("$mainType", "$subType", compressible = $compressible, binary = $binary$extensionsString)
"""
}
}

object MediaTypeInfo {
def fromMimeDb(mimeDb: MimeDb): List[MediaTypeInfo] = {
mimeDb.mimeTypes.map { case (mimeType, details) =>
val Array(mainType, subType) = mimeType.split('/')
MediaTypeInfo(mainType, subType, details.compressible.getOrElse(false), details.extensions.getOrElse(List.empty))
}.toList
}
}

// Renders a group of media types as a Scala code snippet
case class MediaTypeGroup(
mainType: String,
subTypes: List[MediaTypeInfo],
) {
def render: String = {

s"""
object ${RenderUtils.snakeCase(mainType)} {
${subTypes.map(_.render).mkString("\n")}
lazy val all: List[MediaType] = List(${subTypes.map(t => RenderUtils.renderEscaped(t.subType)).mkString(", ")})
lazy val any: MediaType = new MediaType("$mainType", "*")
}
"""
}
}

object GenerateMediaTypes extends ZIOAppDefault {
val run =
for {
mimeDb <- MimeDb.fetch
mediaTypes = MediaTypeInfo.fromMimeDb(mimeDb)
mediaTypeGroups =
mediaTypes
.groupBy(_.mainType)
.map { case (mainType, subTypes) =>
MediaTypeGroup(mainType, subTypes)
}
.toList
file = MediaTypeFile(mediaTypeGroups)
mediaTypesPath = "../zio-http/shared/src/main/scala/zio/http/MediaTypes.scala"
_ <- ZIO.writeFile(mediaTypesPath, file.render)
} yield ()
}

// Renders a list of media type groups as a Scala code snippet
case class MediaTypeFile(
groups: List[MediaTypeGroup],
) {
def render: String = {
s"""
// ⚠️ HEY YOU! IMPORTANT MESSAGE ⚠️
// ==============================
//
// THIS FILE IS AUTOMATICALLY GENERATED BY `GenerateMediaTypes.scala`
// So don't go editing it now, you hear? Otherwise your changes will
// be overwritten the next time someone runs `sbt generateMediaTypes`

package zio.http

private[zio] trait MediaTypes {
private[zio] lazy val allMediaTypes: List[MediaType] =
${groups.map(main => s"${RenderUtils.snakeCase(main.mainType)}.all").mkString(" ++ ")}
lazy val any: MediaType = new MediaType("*", "*")

${groups.map(_.render).mkString("\n\n")}
}
"""
}
}
4 changes: 2 additions & 2 deletions zio-http/jvm/src/test/scala/zio/http/ContentTypeSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ object ContentTypeSpec extends HttpRunnableSpec {
val res =
Handler.fromResource("TestFile3.js").sandbox.toRoutes.deploy(Request()).map(_.header(Header.ContentType))
assertZIO(res)(
isSome(equalTo(Header.ContentType(MediaType.application.`javascript`, charset = Some(Charsets.Utf8)))),
isSome(equalTo(Header.ContentType(MediaType.text.`javascript`, charset = Some(Charsets.Utf8)))),
)
},
test("no extension") {
Expand All @@ -50,7 +50,7 @@ object ContentTypeSpec extends HttpRunnableSpec {
test("mp3") {
val res =
Handler.fromResource("TestFile6.mp3").sandbox.toRoutes.deploy(Request()).map(_.header(Header.ContentType))
assertZIO(res)(isSome(equalTo(Header.ContentType(MediaType.audio.`mpeg`))))
assertZIO(res)(isSome(equalTo(Header.ContentType(MediaType.audio.`mp3`))))
},
test("unidentified extension") {
val res =
Expand Down
Loading

0 comments on commit 8de4a41

Please sign in to comment.