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

Implement httpLabel with nom #996

Merged
merged 2 commits into from
Jan 5, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ object ServerCargoDependency {
val AsyncTrait: CargoDependency = CargoDependency("async-trait", CratesIo("0.1"))
val AxumCore: CargoDependency = CargoDependency("axum-core", CratesIo("0.1"))
val FuturesUtil: CargoDependency = CargoDependency("futures-util", CratesIo("0.3"))
val Nom: CargoDependency = CargoDependency("nom", CratesIo("7"))
val PinProjectLite: CargoDependency = CargoDependency("pin-project-lite", CratesIo("0.2"))
val SerdeUrlEncoded: CargoDependency = CargoDependency("serde_urlencoded", CratesIo("0.7"))
val Tower: CargoDependency = CargoDependency("tower", CratesIo("0.4"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ private class ServerHttpProtocolImplGenerator(
"HttpBody" to CargoDependency.HttpBody.asType(),
"Hyper" to CargoDependency.Hyper.asType(),
"LazyStatic" to CargoDependency.LazyStatic.asType(),
"Nom" to ServerCargoDependency.Nom.asType(),
"PercentEncoding" to CargoDependency.PercentEncoding.asType(),
"Regex" to CargoDependency.Regex.asType(),
"SerdeUrlEncoded" to ServerCargoDependency.SerdeUrlEncoded.asType(),
Expand Down Expand Up @@ -572,46 +573,80 @@ private class ServerHttpProtocolImplGenerator(
if (pathBindings.isEmpty()) {
return
}
val pattern = StringBuilder()
val httpTrait = httpBindingResolver.httpTrait(operationShape)
httpTrait.uri.segments.forEach {
pattern.append("/")
if (it.isLabel) {
pattern.append("(?P<${it.content}>")
if (it.isGreedyLabel) {
pattern.append(".+")
val greedyLabelIndex = httpTrait.uri.segments.indexOfFirst { it.isGreedyLabel }
val segments =
if (greedyLabelIndex >= 0)
httpTrait.uri.segments.slice(0 until (greedyLabelIndex + 1))
else
httpTrait.uri.segments
val restAfterGreedyLabel =
if (greedyLabelIndex >= 0)
httpTrait.uri.segments.slice((greedyLabelIndex + 1) until httpTrait.uri.segments.size).joinToString(prefix = "/", separator = "/")
else
""
val labeledNames = segments
.mapIndexed { index, segment ->
if (segment.isLabel) { "m$index" } else { "_" }
}
.joinToString(prefix = (if (segments.size > 1) "(" else ""), separator = ",", postfix = (if (segments.size > 1) ")" else ""))
val nomParser = segments
.map { segment ->
if (segment.isGreedyLabel) {
"#{Nom}::combinator::rest::<_, #{Nom}::error::Error<&str>>"
} else if (segment.isLabel) {
"""#{Nom}::branch::alt::<_, _, #{Nom}::error::Error<&str>, _>((#{Nom}::bytes::complete::take_until("/"), #{Nom}::combinator::rest))"""
} else {
pattern.append("[^/]+")
Copy link
Contributor

Choose a reason for hiding this comment

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

Why were we accepting empty path segments? I see the TypeScript sSDK is also stripping away empty path segments.

Copy link
Contributor

Choose a reason for hiding this comment

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

Writing my thoughts down here. I guess done this way because anecdotally servers accept empty path segments e.g. https://github.com//awslabs///////smithy-typescript///commits works fine.

But when extracting labels, I'm not sure about whether this behavior should be the correct one, and I don't see it specified in the Smithy spec. I'm thinking of possible ambiguous scenarios e.g. say the URI pattern is /foo/{label}/bar and the server receives a request with URI path /foo//bar. Should the label field be bound to an empty string? Or should we strip away the empty segment and reject the request?

Copy link
Contributor

Choose a reason for hiding this comment

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

My opinion on this and interpretation of the Smithy spec is that we were indeed doing it wrong and empty path segments should not be stripped away, but I've opened an issue with the Smithy team to clarify the spec: smithy-lang/smithy#1024

"""#{Nom}::bytes::complete::tag::<_, _, #{Nom}::error::Error<&str>>("${segment.content}")"""
}
pattern.append(")")
} else {
pattern.append(it.content)
}
}
.joinToString(
// TODO: tuple() is currently limited to 21 items
prefix = if (segments.size > 1) "#{Nom}::sequence::tuple::<_, _, #{Nom}::error::Error<&str>, _>((" else "",
postfix = if (segments.size > 1) "))" else "",
transform = { parser ->
"""
#{Nom}::sequence::preceded(#{Nom}::bytes::complete::tag("/"), $parser)
""".trimIndent()
}
)
with(writer) {
rustTemplate("let input_string = request.uri().path();")
82marbag marked this conversation as resolved.
Show resolved Hide resolved
if (greedyLabelIndex >= 0 && greedyLabelIndex + 1 < httpTrait.uri.segments.size) {
rustTemplate(
"""
if !input_string.ends_with(${restAfterGreedyLabel.dq()}) {
return std::result::Result::Err(#{SmithyRejection}::Deserialize(
aws_smithy_http_server::rejection::Deserialize::from_err(format!("Postfix not found: {}", ${restAfterGreedyLabel.dq()}))));
}
let input_string = &input_string[..(input_string.len() - ${restAfterGreedyLabel.dq()}.len())];
""".trimIndent(),
*codegenScope
)
}
rustTemplate(
"""
#{LazyStatic}::lazy_static! {
static ref RE: #{Regex}::Regex = #{Regex}::Regex::new("$pattern").unwrap();
}
let (input_string, $labeledNames) = $nomParser(input_string)?;
debug_assert_eq!("", input_string);
""".trimIndent(),
*codegenScope,
*codegenScope
)
rustBlock("if let Some(captures) = RE.captures(request.uri().path())") {
pathBindings.forEach {
val deserializer = generateParsePercentEncodedStrFn(it)
rustTemplate(
"""
if let Some(m) = captures.name("${it.locationName}") {
input = input.${it.member.setterName()}(
#{deserializer}(m.as_str())?
segments
.forEachIndexed { index, segment ->
val binding = pathBindings.find { it.memberName == segment.content }
if (binding != null && segment.isLabel) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If segment.isLabel is true, then binding must be non-null, right? Because pathBindings contains all the @httpLabel bindings.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In some cases, such as /foo/{foo}, foo is found as binding too because I look for the name

val deserializer = generateParsePercentEncodedStrFn(binding)
rustTemplate(
"""
input = input.${binding.member.setterName()}(
#{deserializer}(m$index)?
);
}
""".trimIndent(),
"deserializer" to deserializer,
)
""".trimIndent(),
*codegenScope,
"deserializer" to deserializer,
)
}
}
}
}
}

Expand Down
1 change: 1 addition & 0 deletions rust-runtime/aws-smithy-http-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ http = "0.2"
http-body = "0.4"
hyper = { version = "0.14", features = ["server", "http1", "http2", "tcp"] }
mime = "0.3"
nom = "7"
pin-project-lite = "0.2"
regex = "1.0"
serde_urlencoded = "0.7"
Expand Down
6 changes: 6 additions & 0 deletions rust-runtime/aws-smithy-http-server/src/rejection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,9 @@ impl From<serde_urlencoded::de::Error> for SmithyRejection {
SmithyRejection::Deserialize(Deserialize::from_err(err))
}
}

impl From<nom::Err<nom::error::Error<&str>>> for SmithyRejection {
fn from(err: nom::Err<nom::error::Error<&str>>) -> Self {
SmithyRejection::Deserialize(Deserialize::from_err(err.to_owned()))
}
}