diff --git a/modules/core/src/smithy4s/http/HttpEndpoint.scala b/modules/core/src/smithy4s/http/HttpEndpoint.scala index 2d8ef0e82..28b3d609c 100644 --- a/modules/core/src/smithy4s/http/HttpEndpoint.scala +++ b/modules/core/src/smithy4s/http/HttpEndpoint.scala @@ -26,6 +26,9 @@ trait HttpEndpoint[I] { // Returns a path template as a list of segments, which can be constant strings or placeholders. def path: List[PathSegment] + + // Returns a map of static query parameters that are found in the uri of Http hint. + def staticQueryParams: Map[String, Seq[String]] def method: HttpMethod def code: Int @@ -48,6 +51,7 @@ object HttpEndpoint { .get(Http) .toRight(HttpEndpointError("Operation doesn't have a @http trait")) httpMethod = HttpMethod.fromStringOrDefault(http.method.value) + queryParams = internals.staticQueryParams(http.uri.value) httpPath <- internals .pathSegments(http.uri.value) .toRight( @@ -55,6 +59,7 @@ object HttpEndpoint { s"Unable to parse HTTP path template: ${http.uri.value}" ) ) + encoder <- SchemaVisitorPathEncoder( endpoint.input.addHints(http) ).toRight( @@ -64,6 +69,7 @@ object HttpEndpoint { } yield { new HttpEndpoint[I] { def path(input: I): List[String] = encoder.encode(input) + val staticQueryParams: Map[String, Seq[String]] = queryParams val path: List[PathSegment] = httpPath.toList val method: HttpMethod = httpMethod val code: Int = http.code diff --git a/modules/core/src/smithy4s/http/internals/package.scala b/modules/core/src/smithy4s/http/internals/package.scala index ddaaafa5f..5683f568b 100644 --- a/modules/core/src/smithy4s/http/internals/package.scala +++ b/modules/core/src/smithy4s/http/internals/package.scala @@ -56,19 +56,38 @@ package object internals { str: String ): Option[Vector[PathSegment]] = { str + .split('?') + .head .split('/') .toVector .filterNot(_.isEmpty()) .traverse(fromToString(_)) + + } + + private[http] def staticQueryParams( + uri: String + ): Map[String, Seq[String]] = { + uri.split("\\?", 2) match { + case Array(_) => Map.empty + case Array(_, query) => + query.split("&").toList.foldLeft(Map.empty[String, Seq[String]]) { + case (acc, param) => + val (k, v) = param.split("=", 2) match { + case Array(key) => (key, "") + case Array(key, value) => (key, value) + } + acc.updated(k, acc.getOrElse(k, Seq.empty) :+ v) + } + } } private def fromToString(str: String): Option[PathSegment] = { - if (str.isEmpty()) None + if (str == null || str.isEmpty) None else if (str.startsWith("{") && str.endsWith("+}")) Some(PathSegment.greedy(str.substring(1, str.length() - 2))) else if (str.startsWith("{") && str.endsWith("}")) Some(PathSegment.label(str.substring(1, str.length() - 1))) else Some(PathSegment.static(str)) } - } diff --git a/modules/core/test/src/smithy4s/http/internals/PathSpec.scala b/modules/core/test/src/smithy4s/http/internals/PathSpec.scala index d56d8b7af..bc1af52c2 100644 --- a/modules/core/test/src/smithy4s/http/internals/PathSpec.scala +++ b/modules/core/test/src/smithy4s/http/internals/PathSpec.scala @@ -57,6 +57,46 @@ class PathSpec() extends munit.FunSuite { ) ) } + test("Parse path pattern from path that has query param into path segments") { + val result = pathSegments("/{head}/foo/{tail+}?hello=world&hi") + expect( + result == Option( + Vector( + PathSegment.label("head"), + PathSegment.static("foo"), + PathSegment.greedy("tail") + ) + ) + ) + } + test("parse static query params from DummyPath") { + val httpEndpoint = HttpEndpoint + .cast( + DummyPath + ) + .toOption + .get + + val sqp = httpEndpoint.staticQueryParams + val path = httpEndpoint.path + + val expectedQueryMap = Map("value" -> Seq("foo"), "baz" -> Seq("bar")) + expect(sqp == expectedQueryMap) + expect( + path == + List( + PathSegment.static("dummy-path"), + PathSegment.label("str"), + PathSegment.label("int"), + PathSegment.label("ts1"), + PathSegment.label("ts2"), + PathSegment.label("ts3"), + PathSegment.label("ts4"), + PathSegment.label("b"), + PathSegment.label("ie") + ) + ) + } test("Write PathParams for DummyPath") { val result = HttpEndpoint diff --git a/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sClientEndpoint.scala b/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sClientEndpoint.scala index ee4ecb066..5bc234ac4 100644 --- a/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sClientEndpoint.scala +++ b/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sClientEndpoint.scala @@ -114,9 +114,10 @@ private[http4s] class SmithyHttp4sClientEndpointImpl[F[_], Op[_, _, _, _, _], I, def inputToRequest(input: I): Request[F] = { val metadata = inputMetadataEncoder.encode(input) val path = httpEndpoint.path(input) + val staticQueries = httpEndpoint.staticQueryParams val uri = baseUri .copy(path = baseUri.path.addSegments(path.map(Uri.Path.Segment(_)))) - .withMultiValueQueryParams(metadata.query) + .withMultiValueQueryParams(staticQueries ++ metadata.query) val headers = toHeaders(metadata.headers) val baseRequest = Request[F](method, uri, headers = headers) if (inputHasBody) { diff --git a/sampleSpecs/metadata.smithy b/sampleSpecs/metadata.smithy index ff30786ea..f7735be6e 100644 --- a/sampleSpecs/metadata.smithy +++ b/sampleSpecs/metadata.smithy @@ -15,7 +15,7 @@ operation Dummy { input: Queries } -@http(method: "GET", uri: "/dummy-path/{str}/{int}/{ts1}/{ts2}/{ts3}/{ts4}/{b}/{ie}") +@http(method: "GET", uri: "/dummy-path/{str}/{int}/{ts1}/{ts2}/{ts3}/{ts4}/{b}/{ie}?value=foo&baz=bar") @readonly operation DummyPath { input: PathParams