Skip to content

Commit

Permalink
Merge pull request #71 from Dwolla/lift-routes-with-local
Browse files Browse the repository at this point in the history
Add natchez-http4s-mtl: server middleware with Local[F, Span[F]] semantics
  • Loading branch information
bpholt authored Oct 28, 2024
2 parents f76caa8 + 488496c commit df876e6
Show file tree
Hide file tree
Showing 11 changed files with 453 additions and 97 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: mkdir -p modules/http4s/.jvm/target modules/http4s/.js/target modules/http4s/.native/target project/target
run: mkdir -p modules/mtl/.native/target modules/http4s/.jvm/target modules/http4s/.js/target modules/core/.native/target modules/core/.js/target modules/core/.jvm/target modules/http4s/.native/target modules/mtl/.js/target modules/mtl/.jvm/target project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: tar cf targets.tar modules/http4s/.jvm/target modules/http4s/.js/target modules/http4s/.native/target project/target
run: tar cf targets.tar modules/mtl/.native/target modules/http4s/.jvm/target modules/http4s/.js/target modules/core/.native/target modules/core/.js/target modules/core/.jvm/target modules/http4s/.native/target modules/mtl/.js/target modules/mtl/.jvm/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
Expand Down
4 changes: 4 additions & 0 deletions .jvmopts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

-Xms1G
-Xmx4G
-XX:+UseG1GC
16 changes: 16 additions & 0 deletions .mergify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ pull_request_rules:
- status-success=Build and Test (ubuntu-latest, 3, temurin@17, rootNative)
actions:
merge: {}
- name: Label core PRs
conditions:
- files~=^modules/core/
actions:
label:
add:
- core
remove: []
- name: Label docs PRs
conditions:
- files~=^modules/docs/
Expand All @@ -47,3 +55,11 @@ pull_request_rules:
add:
- http4s
remove: []
- name: Label mtl PRs
conditions:
- files~=^modules/mtl/
actions:
label:
add:
- mtl
remove: []
40 changes: 37 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ val scala3Version = "3.3.4"
val slf4jVersion = "2.0.16"
val munitCEVersion = "2.0.0"
val scalacheckEffectVersion = "2.0.0-M2"
val catsMtlVersion = "1.4.0"

ThisBuild / organization := "org.tpolecat"
ThisBuild / tlSonatypeUseLegacyHost := false
ThisBuild / sonatypeCredentialHost := xerial.sbt.Sonatype.sonatypeLegacy
ThisBuild / licenses := Seq(("MIT", url("http://opensource.org/licenses/MIT")))
ThisBuild / homepage := Some(url("https://github.com/tpolecat/natchez-http4s"))
ThisBuild / developers := List(
Expand Down Expand Up @@ -46,11 +47,27 @@ ThisBuild / scalaVersion := scala213Version
ThisBuild / crossScalaVersions := Seq(scala212Version, scala213Version, scala3Version)

lazy val root = tlCrossRootProject.aggregate(
core,
http4s,
mtl,
examples,
docs
)

lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.crossType(CrossType.Pure)
.in(file("modules/core"))
.settings(commonSettings)
.settings(
name := "natchez-http4s-core",
description := "Natchez middleware for http4s.",
libraryDependencies ++= Seq(
"org.tpolecat" %%% "natchez-core" % natchezVersion,
"org.http4s" %%% "http4s-core" % http4sVersion,
),
tlVersionIntroduced := List("2.12", "2.13", "3").map(_ -> "0.6.1").toMap
)

lazy val http4s = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.crossType(CrossType.Pure)
.in(file("modules/http4s"))
Expand All @@ -59,13 +76,30 @@ lazy val http4s = crossProject(JSPlatform, JVMPlatform, NativePlatform)
name := "natchez-http4s",
description := "Natchez middleware for http4s.",
libraryDependencies ++= Seq(
"org.tpolecat" %%% "natchez-core" % natchezVersion,
"org.http4s" %%% "http4s-core" % http4sVersion,
"org.http4s" %%% "http4s-client" % http4sVersion,
"org.http4s" %%% "http4s-server" % http4sVersion,
"org.tpolecat" %%% "natchez-testkit" % natchezVersion % Test,
)
)
.dependsOn(core)

lazy val mtl = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.crossType(CrossType.Pure)
.in(file("modules/mtl"))
.settings(commonSettings)
.settings(
name := "natchez-http4s-mtl",
description := "Natchez middleware for http4s with cats-mtl Local[F, Span[F]] semantics.",
libraryDependencies ++= Seq(
"org.tpolecat" %%% "natchez-mtl" % natchezVersion,
"org.http4s" %%% "http4s-core" % http4sVersion,
"org.http4s" %%% "http4s-server" % http4sVersion,
"org.typelevel" %%% "cats-mtl" % catsMtlVersion,
"org.tpolecat" %%% "natchez-testkit" % natchezVersion % Test,
),
tlVersionIntroduced := List("2.12", "2.13", "3").map(_ -> "0.6.1").toMap
)
.dependsOn(core, http4s % "test->test")

lazy val examples = project
.in(file("modules/examples"))
Expand Down
29 changes: 29 additions & 0 deletions modules/core/src/main/scala/natchez/http4s/DefaultValues.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) 2021 by Rob Norris
// This software is licensed under the MIT License (MIT).
// For more information see LICENSE or https://opensource.org/licenses/MIT

package natchez
package http4s

import org.http4s.headers.*
import org.typelevel.ci.*

private[natchez] object DefaultValues {
private[natchez] val ExcludedHeaders: Set[CIString] = {
val payload = Set(
`Content-Length`.name,
ci"Content-Type",
`Content-Range`.name,
ci"Trailer",
`Transfer-Encoding`.name,
)

val security = Set(
Authorization.name,
Cookie.name,
`Set-Cookie`.name,
)

payload ++ security
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
package natchez.http4s.syntax

import cats.~>
import cats.data.{ Kleisli, OptionT }
import cats.data.{Kleisli, OptionT}
import cats.data.Kleisli.applyK
import cats.effect.MonadCancel
import natchez.{ EntryPoint, Kernel, Span }
import natchez.{EntryPoint, Kernel, Span}
import org.http4s.HttpRoutes
import cats.effect.Resource
import natchez.http4s.DefaultValues
import org.http4s.server.websocket.WebSocketBuilder2
import org.typelevel.ci.CIString

Expand Down Expand Up @@ -120,26 +121,7 @@ trait EntryPointOps[F[_]] { outer =>
}

object EntryPointOps {
val ExcludedHeaders: Set[CIString] = {
import org.http4s.headers._
import org.typelevel.ci._

val payload = Set(
`Content-Length`.name,
ci"Content-Type",
`Content-Range`.name,
ci"Trailer",
`Transfer-Encoding`.name,
)

val security = Set(
Authorization.name,
Cookie.name,
`Set-Cookie`.name,
)

payload ++ security
}
val ExcludedHeaders: Set[CIString] = DefaultValues.ExcludedHeaders
}

trait ToEntryPointOps {
Expand Down
21 changes: 14 additions & 7 deletions modules/http4s/src/test/scala/natchez/http4s/InMemory.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@ package http4s
import cats.data.Kleisli
import cats.effect.{IO, MonadCancelThrow}
import munit.CatsEffectSuite
import org.typelevel.ci.*

trait InMemorySuite extends CatsEffectSuite {
trait InMemorySuite
extends CatsEffectSuite
with Arbitraries {
type Lineage = InMemory.Lineage
val Lineage = InMemory.Lineage
val Lineage: InMemory.Lineage.type = InMemory.Lineage
type NatchezCommand = InMemory.NatchezCommand
val NatchezCommand = InMemory.NatchezCommand
val NatchezCommand: InMemory.NatchezCommand.type = InMemory.NatchezCommand

trait TraceTest {
def program[F[_]: MonadCancelThrow: Trace]: F[Unit]
def expectedHistory: List[(Lineage, NatchezCommand)]
}

def traceTest(name: String, tt: TraceTest) = {
def traceTest(name: String, tt: TraceTest): Unit = {
test(s"$name - Kleisli")(
testTraceKleisli(tt.program[Kleisli[IO, Span[IO], *]](implicitly, _), tt.expectedHistory)
)
Expand All @@ -30,7 +33,7 @@ trait InMemorySuite extends CatsEffectSuite {
def testTraceKleisli(
traceProgram: Trace[Kleisli[IO, Span[IO], *]] => Kleisli[IO, Span[IO], Unit],
expectedHistory: List[(Lineage, NatchezCommand)]
) = testTrace[Kleisli[IO, Span[IO], *]](
): IO[Unit] = testTrace[Kleisli[IO, Span[IO], *]](
traceProgram,
root => IO.pure(Trace[Kleisli[IO, Span[IO], *]] -> (k => k.run(root))),
expectedHistory
Expand All @@ -39,13 +42,13 @@ trait InMemorySuite extends CatsEffectSuite {
def testTraceIoLocal(
traceProgram: Trace[IO] => IO[Unit],
expectedHistory: List[(Lineage, NatchezCommand)]
) = testTrace[IO](traceProgram, Trace.ioTrace(_).map(_ -> identity), expectedHistory)
): IO[Unit] = testTrace[IO](traceProgram, Trace.ioTrace(_).map(_ -> identity), expectedHistory)

def testTrace[F[_]](
traceProgram: Trace[F] => F[Unit],
makeTraceAndResolver: Span[IO] => IO[(Trace[F], F[Unit] => IO[Unit])],
expectedHistory: List[(Lineage, NatchezCommand)]
) =
): IO[Unit] =
InMemory.EntryPoint.create[IO].flatMap { ep =>
val traced = ep.root("root").use { r =>
makeTraceAndResolver(r).flatMap { case (traceInstance, resolve) =>
Expand All @@ -56,4 +59,8 @@ trait InMemorySuite extends CatsEffectSuite {
assertEquals(history.toList, expectedHistory)
}
}

val CustomHeaderName = ci"X-Custom-Header"
val CorrelationIdName = ci"X-Correlation-Id"

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,84 +8,22 @@ import cats.Monad
import cats.data.{Chain, Kleisli}
import cats.effect.{IO, MonadCancelThrow, Resource}
import munit.ScalaCheckEffectSuite
import natchez.Span.Options.SpanCreationPolicy
import natchez.*
import natchez.Span.SpanKind
import natchez.TraceValue.StringValue
import natchez.http4s.syntax.entrypoint.*
import natchez.*
import org.http4s.*
import org.http4s.client.Client
import org.http4s.dsl.request.*
import org.http4s.headers.*
import org.http4s.syntax.literals.*
import org.scalacheck.Arbitrary.arbitrary
import org.scalacheck.effect.PropF
import org.scalacheck.{Arbitrary, Gen}
import org.typelevel.ci.*

class NatchezMiddlewareSuite
extends InMemorySuite
with ScalaCheckEffectSuite {

private val CustomHeaderName = ci"X-Custom-Header"
private val CorrelationIdName = ci"X-Correlation-Id"

private implicit val arbTraceValue: Arbitrary[TraceValue] = Arbitrary {
Gen.oneOf(
arbitrary[String].map(TraceValue.StringValue(_)),
arbitrary[Boolean].map(TraceValue.BooleanValue(_)),
arbitrary[Number].map(TraceValue.NumberValue(_)),
)
}

private implicit val arbAttribute: Arbitrary[(String, TraceValue)] = Arbitrary {
for {
key <- arbitrary[String]
value <- arbitrary[TraceValue]
} yield key -> value
}

private implicit val arbCIString: Arbitrary[CIString] = Arbitrary {
Gen.alphaLowerStr.map(CIString(_))
}

private implicit val arbKernel: Arbitrary[Kernel] = Arbitrary {
arbitrary[Map[CIString, String]].map(Kernel(_))
}

private implicit val arbSpanCreationPolicy: Arbitrary[SpanCreationPolicy] = Arbitrary {
Gen.oneOf(SpanCreationPolicy.Default, SpanCreationPolicy.Coalesce, SpanCreationPolicy.Suppress)
}

private implicit val arbSpanKind: Arbitrary[SpanKind] = Arbitrary {
Gen.oneOf(
SpanKind.Internal,
SpanKind.Client,
SpanKind.Server,
SpanKind.Producer,
SpanKind.Consumer,
)
}

private implicit val arbSpanOptions: Arbitrary[Span.Options] = Arbitrary {
for {
parentKernel <- arbitrary[Option[Kernel]]
spanCreationPolicy <- arbitrary[SpanCreationPolicy]
spanKind <- arbitrary[SpanKind]
links <- arbitrary[List[Kernel]].map(Chain.fromSeq)
} yield {
links.foldLeft {
parentKernel.foldLeft {
Span
.Options
.Defaults
.withSpanKind(spanKind)
.withSpanCreationPolicy(spanCreationPolicy)
}(_.withParentKernel(_))
}(_.withLink(_))
}
}

test("do not leak security and payload headers to the client request") {
val headers = Headers(
// security
Expand Down
Loading

0 comments on commit df876e6

Please sign in to comment.