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

feat: Rust codegen for Constraints #582

Merged
merged 12 commits into from
Sep 26, 2024

Conversation

alex-chew
Copy link
Contributor

@alex-chew alex-chew commented Sep 13, 2024

Issue #, if available: #533

Description of changes:
This PR implements Rust library codegen for input validation of supported constraint traits (@required, @length, @range).

Notes:

  • Unlike in Java and C#, library users may directly construct the generated operation input structures in Rust (in order to aid in idiomatic usage, especially w.r.t. pattern matching, like the AWS SDK for Rust does) and are not restricted to using the builder pattern. Therefore, the builder can't take sole responsibility for validation; instead, validation happens whenever an operation is called.
  • Unlike other target languages, Rust is limited in its ability to propagate arbitrary errors up the call stack unless the API is designed for doing so. Our models don't define a specific error shape for validation, so in order to represent validation errors in the generated Error enum, we generate a ValidationError variant that only exists on the Rust side. When converting to Dafny, we upcast the error into the Opaque variant; when converting from Dafny, we downcast it back after checking that doing so is safe using dafny_runtime::is_object!.
  • By Polymorph's design, values crossing the boundary from Dafny-compiled code are assumed to be "valid", but the WrappedSimpleConstraintsTest intentionally breaks this assumption in order to test the constraint validation logic, forcing invalid values across the boundary by (ab)using {:axiom}. This would be okay except that the Dafny tests in the Constraints test model also pass across malformed UTF-8 for @DafnyUtf8Bytes strings (which is not a constraint trait). The Rust type conversion process does not allow for failure when converting values from Dafny (doing so would be a major refactor and probably some performance impact), so I've isolated the malformed UTF-8 test assertions and used a bit of sed to simply disable them for Rust specifically, while allowing the other languages to continue testing them.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@alex-chew alex-chew marked this pull request as ready for review September 13, 2024 14:47
@alex-chew alex-chew requested a review from a team as a code owner September 13, 2024 14:47
pub(crate) async fn send(
client: &crate::client::Client,
input: crate::operation::get_resource::GetResourceInput,
) -> ::std::result::Result<
crate::operation::get_resource::GetResourceOutput,
crate::types::error::Error,
> {
if input.name.is_none() {
return ::std::result::Result::Err(::aws_smithy_types::error::operation::BuildError::missing_field(
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like going through a aws_smithy_types::error is adding complexity without adding value.
Couldn't we just make a wrap_validation_err out of a String and leave it as that?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's valuable: it makes it possible to programmatically handle validation errors, and I can see that being useful for middleware that handles translation of Smithy-modeled types, for example.

As long as displaying these errors gives a good user experience I say we keep them.

@robin-aws robin-aws linked an issue Sep 22, 2024 that may be closed by this pull request
Copy link
Contributor

@robin-aws robin-aws left a comment

Choose a reason for hiding this comment

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

Nice job solving some tricky constraints on this (:D)

// scalar values
? "x.chars().count()"
// bytes
: "x.len()";
Copy link
Contributor

Choose a reason for hiding this comment

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

This is technically wrong, but it looks like the other languages are also wrong according to the Smithy spec: #610.

Would it be a massive pain to call something that calculates the number of UTF-16 code points instead, to at least be consistent with the other languages?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It turns out that it's not too difficult to count UTF-16 code points instead, so I've implemented it in the latest commit.

list: ::std::vec::Vec<Self>,
message: ::std::string::String,
},
ValidationError(ValidationError),
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this be ValidationError(BuildError) instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For implementing constraint trait validation only, yes, I believe this could just directly wrap a BuildError.

There's two levels of indirection here:

  • This PR uses an Rc<dyn std::error::Error> instead of BuildError directly, because I was not (and am not) sure that all validation errors will be BuildErrors.
  • In order to safely up-/down-cast the contents of the Error::ValidationError variant from the dyn Any in an arbitrary dafny_runtime::Object, it's best to define a unique type that we own - that is, the ValidationError struct (not the enum variant). If we defined Enum::ValidationError(Rc<...>) or Enum::ValidationError<BuildError>, then it seems possible for us to mishandle a dafny_runtime::Object containing an Rc<?> or BuildError from somewhere else.

In my mind, this doesn't make the library harder to use: the typical library user would mainly care whether or not the error variant is a ValidationError or something else. If they want the message or the underlying dyn std::error::Error, there's no need to manually traverse the indirections: they can use to_string() and source() as you would for any other std::error::Error.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fair points. What would it look like for an end-user to react differently to a validation error, and actually dig out the offending field from the wrapped BuildError?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There isn't a clean way to dig out the name of the offending field right now. Neither BuildError nor its inner BuildErrorKind give public visibility to their members.

Did you have a particular use-case in mind for needing to programmatically retrieve the name of the invalid field, rather than showing the full error message?

Copy link
Contributor

Choose a reason for hiding this comment

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

I see what you mean - I had thought BuildErrorKind was structured rather than just a string to enable programmatic handling, but it looks more like the main purpose is just to display a nice error message string.

Looks like we're meeting the bar of the existing smithy-rs behavior so I think this is the right call for now. We can potentially revisit and improve as part of #598

Copy link
Contributor

@robin-aws robin-aws left a comment

Choose a reason for hiding this comment

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

🚀 (within carefully specified limits of course :)

@robin-aws robin-aws merged commit 336d788 into main-1.x Sep 26, 2024
80 checks passed
@robin-aws robin-aws deleted the alexchew/rust-codegen-constraints branch September 26, 2024 00:10
ShubhamChaturvedi7 pushed a commit that referenced this pull request Oct 18, 2024
*Issue #, if available:* #533

*Description of changes:*
This PR implements Rust library codegen for input validation of supported constraint traits (`@required`, `@length`, `@range`).

Notes:
* Unlike in Java and C#, library users may directly construct the generated operation input structures in Rust (in order to aid in idiomatic usage, especially w.r.t. pattern matching, like the AWS SDK for Rust does) and are not restricted to using the builder pattern. Therefore, the builder can't take sole responsibility for validation; instead, validation happens whenever an operation is called.
* Unlike other target languages, Rust is limited in its ability to propagate arbitrary errors up the call stack unless the API is designed for doing so. Our models don't define a specific error shape for validation, so in order to represent validation errors in the generated `Error` enum, we generate a `ValidationError` variant that only exists on the Rust side. When converting to Dafny, we upcast the error into the `Opaque` variant; when converting from Dafny, we downcast it back after checking that doing so is safe using `dafny_runtime::is_object!`.
* By Polymorph's design, values crossing the boundary from Dafny-compiled code are assumed to be "valid", but the `WrappedSimpleConstraintsTest` intentionally breaks this assumption in order to test the constraint validation logic, forcing invalid values across the boundary by (ab)using `{:axiom}`. This would be okay except that the Dafny tests in the Constraints test model *also* pass across malformed UTF-8 for `@DafnyUtf8Bytes` strings (which is not a constraint trait). The Rust type conversion process does not allow for failure when converting values from Dafny (doing so would be a major refactor and probably some performance impact), so I've isolated the malformed UTF-8 test assertions and used a bit of `sed` to simply disable them for Rust specifically, while allowing the other languages to continue testing them.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support Constraints on Rust
3 participants