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

fix path segment parsing when suffixed with query #689

Merged
merged 8 commits into from
Jan 5, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
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
6 changes: 6 additions & 0 deletions modules/core/src/smithy4s/http/HttpEndpoint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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, there cannot be multiple of the same key !.
yisraelU marked this conversation as resolved.
Show resolved Hide resolved
def staticQueryParams: Map[String, String]
def method: HttpMethod
def code: Int

Expand All @@ -48,13 +51,15 @@ 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(
HttpEndpointError(
s"Unable to parse HTTP path template: ${http.uri.value}"
)
)

encoder <- SchemaVisitorPathEncoder(
endpoint.input.addHints(http)
).toRight(
Expand All @@ -64,6 +69,7 @@ object HttpEndpoint {
} yield {
new HttpEndpoint[I] {
def path(input: I): List[String] = encoder.encode(input)
val staticQueryParams: Map[String, String] = queryParams
val path: List[PathSegment] = httpPath.toList
val method: HttpMethod = httpMethod
val code: Int = http.code
Expand Down
44 changes: 34 additions & 10 deletions modules/core/src/smithy4s/http/internals/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,43 @@ package object internals {
str: String
): Option[Vector[PathSegment]] = {
str
.split('/')
.toVector
.filterNot(_.isEmpty())
.traverse(fromToString(_))
.split('?')
.headOption
yisraelU marked this conversation as resolved.
Show resolved Hide resolved
.flatMap(
_.split('/').toVector
.filterNot(_.isEmpty())
.traverse(fromToString(_))
)
}

private[http] def staticQueryParams(
uri: String
): Map[String, String] = {
uri.split("\\?", 2) match {
case Array(_) => Map.empty
case Array(_, query) =>
query.split("&").toList.foldLeft(Map.empty[String, String]) {
case (acc, param) =>
val (k, v) = param.split("=", 2) match {
case Array(key) => (key, "")
case Array(key, value) => (key, value)
}
acc.updated(k, v)
}
}
}

private def fromToString(str: String): Option[PathSegment] = {
if (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))
Option(str).filter(_.nonEmpty).map { str =>
yisraelU marked this conversation as resolved.
Show resolved Hide resolved
{
// handle query params in path
if (str.startsWith("{") && str.endsWith("+}"))
PathSegment.greedy(str.substring(1, str.length() - 2))
else if (str.startsWith("{") && str.endsWith("}"))
PathSegment.label(str.substring(1, str.length() - 1))
else PathSegment.static(str)
}
}
}

}
40 changes: 40 additions & 0 deletions modules/core/test/src/smithy4s/http/internals/PathSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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" -> "foo", "baz" -> "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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +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(_))))
.withQueryParams(staticQueries)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

.withMultiValueQueryParams(metadata.query)
val headers = toHeaders(metadata.headers)
val baseRequest = Request[F](method, uri, headers = headers)
Expand Down
2 changes: 1 addition & 1 deletion sampleSpecs/metadata.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down