admin管理员组

文章数量:1122846

Is there any way to extract the endpoint path template as input for Tapir endpoint (to be used inside of the serverLogic)? Something like in(extractFromRequest(_.uri)) but not from ServerRequest but from Endpoint

So instead of /create-user/john I would like to get /create-user/{username}

Is there any way to extract the endpoint path template as input for Tapir endpoint (to be used inside of the serverLogic)? Something like in(extractFromRequest(_.uri)) but not from ServerRequest but from Endpoint

So instead of /create-user/john I would like to get /create-user/{username}

Share Improve this question edited Nov 21, 2024 at 21:30 Matzz asked Nov 21, 2024 at 17:12 MatzzMatzz 6921 gold badge9 silver badges17 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 3

The method you are looking for is showPathTemplate from Endpoint object.

Just doing something like the following would be enough to get that value

endpoint
  .in("some-path")
  .in(path[String]("some-user"))
  .showPathTemplate() // this returns /some-path/{some-user}

showPathTemplate the method has some params if you need to customize the value returned


A full example to use the template path inside your business logic could be like the following

// just a case class to use it as an output response
case class PathAndUserResponse(path: String, user: String)

// the json serialization, in my case I use jsoniter for this example
implicit val jsonCodec: JsonValueCodec[PathAndUserResponse] =
      JsonCodecMaker.make

// the endpoint definition without the businessLogic
val endpointSpec = endpoint
  .in("some-path")
  .in(path[String]("some-user"))
  .out(jsonBody[PathAndUserResponse])

// the template path is just an string
val templatePath = endpointSpec.showPathTemplate()

// the endpoint with the logic implemented using the template path
val fullEndpoint = endpointSpec
  .serverLogic[Future](user =>
    Future(
      Right(
        PathAndUserResponse(
          templatePath, // the template path extracted from the endpoint definition used inside the business logic
          user // the user received in the path param
        )
      )
    )
  )

A unit test that validates you are getting the expected output following the docs from tapir - testing. In this case I'm using scala test

lazy val stubBackend =
  TapirStubInterpreter(SttpBackendStub.asynchronousFuture)
    .whenServerEndpoint(fullEndpoint)
    .thenRunLogic()
    .backend()

val userValue = "user-value"

basicRequest
  .get(uri"https://test.com/some-path/$userValue")
  .response(asJson[PathAndUserResponse])
  .send(stubBackend)
  .map(_.body.value should be(PathAndUserResponse(templatePath, userValue)))

From the scaladoc of showPathTemplate

  /** Shows endpoint path, by default all parametrised path and query components are replaced by {param_name} or {paramN}, e.g. for
    * {{{
    * endpoint.in("p1" / path[String] / query[String]("par2"))
    * }}}
    * returns `/p1/{param1}?par2={par2}`
    *
    * @param includeAuth
    *   Should authentication inputs be included in the result.
    * @param showNoPathAs
    *   How to show the path if the endpoint does not define any path inputs.
    * @param showPathsAs
    *   How to show [[Tapir.paths]] inputs (if at all), which capture multiple paths segments
    * @param showQueryParamsAs
    *   How to show [[Tapir.queryParams]] inputs (if at all), which capture multiple query parameters
    */
  def showPathTemplate(
      showPathParam: (Int, PathCapture[_]) => String = (index, pc) => pc.name.map(name => s"{$name}").getOrElse(s"{param$index}"),
      showQueryParam: Option[(Int, Query[_]) => String] = Some((_, q) => s"${q.name}={${q.name}}"),
      includeAuth: Boolean = true,
      showNoPathAs: String = "*",
      showPathsAs: Option[String] = Some("*"),
      showQueryParamsAs: Option[String] = Some("*")
  ): String

you can also see the value showPathTemplateTestData from EnpointTest with different endpoint definitions and the expected output

  val showPathTemplateTestData = List(
    (endpoint, "*"),
    (endpoint.in(""), "/"),
    (endpoint.in("p1"), "/p1"),
    (endpoint.in("p1" / "p2"), "/p1/p2"),
    (endpoint.securityIn("p1").in("p2"), "/p1/p2"),
    (endpoint.in("p1" / path[String]), "/p1/{param1}"),
    (endpoint.in("p1" / path[String].name("par")), "/p1/{par}"),
    (endpoint.in("p1" / query[String]("par")), "/p1?par={par}"),
    (endpoint.in("p1" / query[String]("par1") / query[String]("par2")), "/p1?par1={par1}&par2={par2}"),
    (endpoint.in("p1" / path[String].name("par1") / query[String]("par2")), "/p1/{par1}?par2={par2}"),
    (endpoint.in("p1" / auth.apiKey(query[String]("par2"))), "/p1?par2={par2}"),
    (endpoint.in("p2" / path[String]).mapIn(identity(_))(identity(_)), "/p2/{param1}"),
    (endpoint.in("p1/p2"), "/p1%2Fp2"),
    (endpoint.in(pathAllowedCharacters), "/" + pathAllowedCharacters),
    (endpoint.in("p1" / paths), "/p1/*"),
    (endpoint.in("p1").in(queryParams), "/p1?*"),
    (
      endpoint.in("p1" / "p2".schema(_.hidden(true)) / query[String]("par1") / query[String]("par2").schema(_.hidden(true))),
      "/p1?par1={par1}"
    ),
    (endpoint.in("not" / "allowed" / "chars" / "hi?hello"), "/not/allowed/chars/hi%3Fhello")
  )

also it is the method used in the observability module to add the labels

import sttp.tapir.server.metrics.MetricLabels

val labels = MetricLabels(
  forRequest = List(
    "path" -> { case (ep, _) => ep.showPathTemplate() }, // here
    "protocol" -> { case (_, req) => req.protocol }
  ),
  forResponse = Nil
)

本文标签: scalaHow extract endpoint path template in TapirStack Overflow