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

0.4.0 release #6

Merged
merged 9 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Fixing dependencies for cross projects, removing unnecessary ones.
Fixing DebuggingDirectives that would fail on large input/outputs.
  • Loading branch information
luksow committed Oct 9, 2024
commit 8403336533ff1c037977d15e9804fe438fd269b1
73 changes: 43 additions & 30 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ ThisBuild / scalaVersion := mainScalaVersion
ThisBuild / githubWorkflowJavaVersions := Seq(JavaSpec.temurin("11"), JavaSpec.temurin("17"))
ThisBuild / githubWorkflowPublishTargetBranches := Seq(RefPredicate.StartsWith(Ref.Tag("v")),
RefPredicate.Equals(Ref.Branch("master")))
ThisBuild / tlBaseVersion := "0.3"
ThisBuild / tlBaseVersion := "0.4"
ThisBuild / tlCiHeaderCheck := false
ThisBuild / tlSonatypeUseLegacyHost := true

Expand Down Expand Up @@ -53,65 +53,78 @@ lazy val baseSettings = Seq(
crossScalaVersions := supportedScalaVersions,
scalafmtOnCompile := true)

val http4s = Seq(
"org.http4s" %% "http4s-dsl" % "0.23.28",
"org.http4s" %% "http4s-ember-server" % "0.23.28")
val http4sDsl = Def.setting("org.http4s" %%% "http4s-dsl" % "0.23.28")
val http4sEmber = Def.setting("org.http4s" %%% "http4s-ember-server" % "0.23.28")

val http4sClient = Seq(
"org.http4s" %% "http4s-ember-client" % "0.23.28")
val fs2Core = Def.setting("co.fs2" %%% "fs2-core" % "3.11.0")
val fs2Io = Def.setting("co.fs2" %%% "fs2-io" % "3.11.0")

val circe = Seq(
"io.circe" %% "circe-core" % "0.14.10",
"io.circe" %% "circe-generic" % "0.14.10",
"io.circe" %% "circe-parser" % "0.14.10",
"org.http4s" %% "http4s-circe" % "0.23.28")
val http4sClient = Def.setting(
"org.http4s" %%% "http4s-ember-client" % "0.23.28")

val logback = Seq(
"ch.qos.logback" % "logback-classic" % "1.5.8")
val circeCore = Def.setting("io.circe" %%% "circe-core" % "0.14.8")
val circeGeneric = Def.setting("io.circe" %%% "circe-generic" % "0.14.8")
val circeParser = Def.setting("io.circe" %%% "circe-parser" % "0.14.8")
val http4sCirce = Def.setting("org.http4s" %%% "http4s-circe" % "0.23.28")

lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform)
val scalatest = Def.setting("org.scalatest" %%% "scalatest" % "3.2.18")
val specs2 = Def.setting("org.specs2" %%% "specs2-core" % "4.20.6")

val scalaXml = Def.setting("org.scala-lang.modules" %%% "scala-xml" % "2.2.0")

lazy val core = crossProject(JVMPlatform, NativePlatform, JSPlatform)
.withoutSuffixFor(JVMPlatform)
.crossType(CrossType.Pure)
.in(file("core"))
.settings(baseSettings *)
.settings(
name := "http4s-stir",
libraryDependencies ++= http4s,
libraryDependencies ++= Seq(http4sDsl.value, http4sEmber.value) ++ Seq(fs2Core.value,
fs2Io.value) ++ Seq(scalaXml.value),
Compile / doc / scalacOptions -= "-Xfatal-warnings")

lazy val coreTests = project
lazy val coreTests = crossProject(JVMPlatform)
.withoutSuffixFor(JVMPlatform)
.crossType(CrossType.Pure)
.in(file("core-tests"))
.settings(baseSettings *)
.settings(noPublishSettings *)
.settings(
name := "http4s-stir-tests",
libraryDependencies ++= http4s ++ circe ++ Seq(
"org.scalatest" %% "scalatest" % "3.2.19" % Test,
"org.specs2" %% "specs2-core" % "4.20.8" % Test)).dependsOn(
testkit.jvm % "test",
core.jvm % "test->test")
libraryDependencies ++= Seq(http4sDsl.value, http4sEmber.value) ++
Seq(circeCore.value, circeGeneric.value, circeParser.value, http4sCirce.value) ++
Seq(
scalatest.value % Test,
specs2.value % Test))
.dependsOn(
testkit % "test",
core % "test->test")

lazy val testkit = crossProject(JSPlatform, JVMPlatform, NativePlatform)
lazy val testkit = crossProject(JVMPlatform)
.withoutSuffixFor(JVMPlatform)
.crossType(CrossType.Pure)
.in(file("testkit"))
.settings(baseSettings *)
.settings(
name := "http4s-stir-testkit",
libraryDependencies ++= http4s ++ http4sClient ++ Seq(
"org.scalatest" %% "scalatest" % "3.2.19" % "provided",
"org.specs2" %% "specs2-core" % "4.20.8" % "provided")).dependsOn(core)
libraryDependencies ++= Seq(http4sClient.value) ++ Seq(
scalatest.value % "provided",
specs2.value % "provided"))
.dependsOn(core)

lazy val examples = project
lazy val examples = crossProject(JVMPlatform)
.withoutSuffixFor(JVMPlatform)
.crossType(CrossType.Pure)
.in(file("examples"))
.settings(baseSettings *)
.settings(noPublishSettings *)
.settings(
name := "http4s-stir-examples",
libraryDependencies ++= http4s ++ circe ++ logback ++ Seq(
"org.specs2" %% "specs2-core" % "4.20.8" % Test,
"org.scalatest" %% "scalatest" % "3.2.19" % Test))
.dependsOn(core.jvm, testkit.jvm % Test)
libraryDependencies ++= Seq(http4sDsl.value, http4sEmber.value) ++ Seq(circeCore.value, circeGeneric.value,
circeParser.value, http4sCirce.value) ++ Seq(
specs2.value % Test,
scalatest.value % Test))
.dependsOn(core, testkit % Test)

lazy val root = tlCrossRootProject.aggregate(core, testkit, examples, coreTests)
.settings(baseSettings *)
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package pl.iterators.stir.server

import cats.effect.IO
import cats.effect.std.Console
import org.http4s.Status.InternalServerError
import org.typelevel.log4cats

import scala.util.control.NonFatal

Expand Down Expand Up @@ -43,7 +43,7 @@ object ExceptionHandler {
*/
def default(logAction: Option[(Throwable, String) => IO[Unit]] = None): ExceptionHandler = {
val log = logAction.getOrElse { (t: Throwable, s: String) =>
log4cats.slf4j.Slf4jFactory.create[IO].getLogger.error(t)(s)
Console[IO].errorln(s) *> Console[IO].printStackTrace(t)
}
apply(knownToBeSealed = true) {
case NonFatal(e) => ctx => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package pl.iterators.stir.server.directives

import cats.effect.IO
import fs2.{ Chunk, Stream }
import cats.effect.std.Console
import cats.implicits.toFlatMapOps
import fs2.{ Chunk, Pull, Stream }
import org.http4s.server.middleware.Logger
import org.http4s.{ Headers, Request, Response }
import org.http4s.{ EntityBody, Headers, Request, Response }
import org.typelevel.ci.CIString
import org.typelevel.log4cats
import pl.iterators.stir.server.{ Directive, Directive0, RouteResult }

trait DebuggingDirectives {
Expand All @@ -17,27 +18,33 @@ trait DebuggingDirectives {
*/
def logRequest(logHeaders: Boolean = true, logBody: Boolean = true,
redactHeadersWhen: CIString => Boolean = Headers.SensitiveHeaders.contains,
maxLogLength: Int = Int.MaxValue,
maxBodyBytes: Int = DebuggingDirectives.DefaultLogLength,
logAction: Option[String => IO[Unit]] = None): Directive0 = {
val log = trimLog(maxLogLength).andThen(logAction.getOrElse { (s: String) =>
DebuggingDirectives.logger.info(s)
})

Directive { inner => ctx =>
val log = logAction.getOrElse { (s: String) =>
DebuggingDirectives.logger(s)
}
val logWithTrimmingIndicator = indicateTrimming(maxBodyBytes, ctx.request.contentLength).andThen(log)
if (logBody && !ctx.request.isChunked) {
IO.ref(Vector.empty[Chunk[Byte]])
.flatMap { vec =>
val newBody = Stream.eval(vec.get)
.flatMap(chunks => Stream.emits(chunks).covary[IO])
.flatMap(chunks => Stream.chunk(chunks).covary[IO])
val newRequest = ctx.request.withBodyStream(
ctx.request.body.observe(_.chunks.flatMap(chunk => Stream.eval(vec.update(_ :+ chunk)).drain)))

val newCtx = ctx.copy(request = ctx.request.withBodyStream(newBody))
Logger.logMessage[IO, Request[IO]](newRequest)(logHeaders, logBody = true, redactHeadersWhen)(log).flatMap(
_ =>
inner(())(newCtx))
}
ctx.request.body.pull.unconsN(maxBodyBytes).flatMap {
case Some((head, tail)) =>
Pull.eval {
Logger.logMessage[IO, Request[IO]](ctx.request.withBodyStream(Stream.chunk(head)))(logHeaders,
logBody = true,
redactHeadersWhen)(logWithTrimmingIndicator).flatMap { _ =>
val newBody = Stream.chunk(head) ++ tail
val newRequest = ctx.request.withBodyStream(newBody)
val newCtx = ctx.copy(request = newRequest)
inner(())(newCtx)
}
}.flatMap(r => Pull.output1(r))
case None =>
Pull.eval {
Logger.logMessage[IO, Request[IO]](ctx.request)(logHeaders, logBody = false, redactHeadersWhen)(
log).flatMap(_ =>
inner(())(ctx))
}.flatMap(r => Pull.output1(r))
}.stream.compile.onlyOrError
} else {
Logger.logMessage[IO, Request[IO]](ctx.request)(logHeaders, logBody = false, redactHeadersWhen)(log).flatMap(
_ =>
Expand All @@ -53,18 +60,21 @@ trait DebuggingDirectives {
*/
def logResult(logHeaders: Boolean = true, logBody: Boolean = true,
redactHeadersWhen: CIString => Boolean = Headers.SensitiveHeaders.contains,
maxLogLength: Int = Int.MaxValue,
maxBodyBytes: Int = DebuggingDirectives.DefaultLogLength,
logAction: Option[String => IO[Unit]] = None): Directive0 = {
val log = trimLog(maxLogLength).andThen(logAction.getOrElse { (s: String) =>
DebuggingDirectives.logger.info(s)
})

Directive { inner => ctx =>
val log = logAction.getOrElse { (s: String) =>
DebuggingDirectives.logger(s)
}
inner(())(ctx).flatMap {
case RouteResult.Complete(response) =>
val logWithTrimmingIndicator = indicateTrimming(maxBodyBytes, response.contentLength).andThen(log)
if (logBody && !response.isChunked) {
Logger.logMessage[IO, Response[IO]](response)(logHeaders, logBody = true, redactHeadersWhen)(log).as(
RouteResult.Complete(response))
val bodyToLog = response.body.take(maxBodyBytes.toLong).chunks.flatMap(Stream.chunk)
Logger.logMessage[IO, Response[IO]](response.withBodyStream(bodyToLog))(
logHeaders,
logBody = true,
redactHeadersWhen)(logWithTrimmingIndicator).as(RouteResult.Complete(response))
} else {
Logger.logMessage[IO, Response[IO]](response)(logHeaders, logBody = false, redactHeadersWhen)(log).as(
RouteResult.Complete(response))
Expand All @@ -83,19 +93,27 @@ trait DebuggingDirectives {
*/
def logRequestResult(logHeaders: Boolean = true, logBody: Boolean = true,
redactHeadersWhen: CIString => Boolean = Headers.SensitiveHeaders.contains,
maxLogLength: Int = Int.MaxValue,
maxBodyBytes: Int = DebuggingDirectives.DefaultLogLength,
logAction: Option[String => IO[Unit]] = None): Directive0 = {
logResult(logHeaders, logBody, redactHeadersWhen, maxLogLength, logAction) & logRequest(logHeaders, logBody,
logResult(logHeaders, logBody, redactHeadersWhen, maxBodyBytes, logAction) & logRequest(logHeaders, logBody,
redactHeadersWhen,
maxLogLength,
maxBodyBytes,
logAction)
}

private def trimLog(maxLogLength: Int): String => String = { log =>
if (log.length > maxLogLength) log.take(maxLogLength) + "..." else log
private def indicateTrimming(maxBodyBytes: Int, contentLength: Option[Long]): String => String = { log =>
contentLength match {
case Some(length) if length > maxBodyBytes =>
s"$log ... ($length bytes total)"
case None =>
s"$log ... (??? bytes total)"
case _ =>
log
}
}
}

object DebuggingDirectives extends DebuggingDirectives {
private val logger = log4cats.slf4j.Slf4jFactory.create[IO].getLogger
private def logger[A](a: A) = Console[IO].println(a)
private val DefaultLogLength: Int = 4096
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ trait FileAndResourceDirectives {
if (file.isFile && file.canRead) {
extractRequest { request =>
complete {
StaticFile.fromPath(Path.fromNioPath(file.toPath), Some(request)).getOrElse(
StaticFile.fromPath(Path(file.getAbsolutePath), Some(request)).getOrElse(
Response[IO](InternalServerError))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ trait FileUploadDirectives {
fileUpload(fieldName).flatMap {
case (fileInfo, bytes) =>
val dest = destFn(fileInfo)
val path = Path.fromNioPath(dest.toPath)
val path = Path(dest.getAbsolutePath)
val uploadedF: IO[(FileInfo, File)] =
bytes.through(Files[IO].writeAll(path)).compile.drain.map(_ => (fileInfo, dest)).onError(_ =>
IO.delay(dest.delete()).as(()))
Expand All @@ -54,7 +54,7 @@ trait FileUploadDirectives {
val fileInfo = FileInfo(part.name.getOrElse(""), part.filename.get,
part.contentType.getOrElse(throw new IllegalStateException(s"Missing content type for part $fieldName")))
val dest = destFn(fileInfo)
val path = Path.fromNioPath(dest.toPath)
val path = Path(dest.getAbsolutePath)
part.body.through(Files[IO].writeAll(path)).compile.drain.map(_ => (fileInfo, dest)).onError(_ =>
IO.delay(dest.delete()).as(()))
}.parSequence
Expand Down Expand Up @@ -102,7 +102,7 @@ trait FileUploadDirectives {
storeUploadedFiles(fieldName, tempDest).map { files =>
files.map {
case (fileInfo, src) =>
val path = Path.fromNioPath(src.toPath)
val path = Path(src.getAbsolutePath)
val byteSource: Stream[IO, Byte] = Files[IO].readAll(path).onFinalize(IO.delay(src.delete()).as(()))
(fileInfo, byteSource)
}
Expand Down
2 changes: 1 addition & 1 deletion examples/src/main/scala/Service.scala
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ object Main extends IOApp.Simple {
path("file") {
getFromFile("project/plugins.sbt")
} ~ pathPrefix("dir") {
getFromDirectory("src/main")
getFromDirectory("core/src/main")
}
}
} ~ path("ws") {
Expand Down
2 changes: 1 addition & 1 deletion project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0")
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2")
addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2")
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.5")
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17")
addSbtPlugin("org.typelevel" % "sbt-typelevel-ci-release" % "0.7.2")
addSbtPlugin("org.jmotor.sbt" % "sbt-dependency-updates" % "1.2.9")
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@ trait RouteTestResultComponent {
// this
// }

private[this] lazy val entityRecreator: IORuntime => EntityBody[IO] = runtime =>
private[this] lazy val entityRecreator: IORuntime => EntityBody[IO] = implicit runtime =>
rawResponse.body.compile.toVector.map { bytes =>
Stream.emits(bytes): Stream[IO, Byte]
}.unsafeRunSync()(runtime)
}.unsafeRunSync()

private def failNeitherCompletedNorRejected(): Nothing =
failTest("Request was neither completed nor rejected" /*within " + timeout */ )
Expand Down
Loading