-
Notifications
You must be signed in to change notification settings - Fork 190
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
Support @default #1879
Support @default #1879
Conversation
...c/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/BuilderGenerator.kt
Outdated
Show resolved
Hide resolved
@@ -132,3 +132,22 @@ structure StoreServiceBlobInput { | |||
structure StoreServiceBlobOutput { | |||
|
|||
} | |||
|
|||
@http(method: "POST", uri: "/default-value") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should go into a Kotlin unit test for the BuilderGenerator so that it can be more thoroughly tested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. @82marbag let's move this to a Kotlin unit test.
It needs to test @default
on all supported shapes. And not only that compilation succeeds, but that when using the builders, the default value is used when none is provided.
} | ||
is IntegerShape, is FloatShape -> rust(".unwrap_or(${node.expectNumberNode().value})") | ||
is BooleanShape -> rust(".unwrap_or(${node.expectBooleanNode().value})") | ||
is StringShape -> rust(".unwrap_or(String::from(${node.expectStringNode().value.dq()}))") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Recommend using unwrap_or_else
to avoid unnecessary allocations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
A new generated diff is ready to view.
A new doc preview is ready to view. |
@@ -258,6 +260,7 @@ class BuilderGenerator( | |||
withBlock("$memberName: self.$memberName", ",") { | |||
// Write the modifier | |||
when { | |||
!memberSymbol.isOptional() && member.hasTrait<DefaultTrait>() -> renderDefaultBuilder(writer, member) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking through this on the client side... Everything in an input is always optional (at present), so this won't set a value for a customer on input. So this would only set a default when a server didn't respond with a default value, which is the incorrect behavior for the server.
I think this should probably be an error case. These builders are fallible, right? If so, then this should definitely just emit an error. If they're not fallible, then perhaps we need more discussion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My apologies, I was wrong on this. I talked to the Smithy folks, and the client should actually set a default value if a value is not present. If a value is given by the default trait, then it should use that. Otherwise, it should use the zero value.
codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/TestHelpers.kt
Outdated
Show resolved
Hide resolved
@@ -268,4 +271,8 @@ class BuilderGenerator( | |||
} | |||
} | |||
} | |||
|
|||
open fun renderDefaultBuilder(writer: RustWriter, member: MemberShape) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we do this with composition instead of inheritance by passing in a function for handling defaults as a constructor argument?
It seems like the implementation of what it does for clients should also live in codegen-client
rather than being a default for codegen-core
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll have a ServerBuilderGenerator when builders of builders is merged. Do we still want to refactor this or can we merge this with the new generator (and add one for the client)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@david-perez and @hlbarber - Can you weigh in on this? I don't think I have enough context to make this call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this is not relevant anymore since it's the same code for both client and server
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is no longer relevant. Now that the server has its own builders, we can place the implementation of default there until the client implements presence tracking.
A new generated diff is ready to view.
A new doc preview is ready to view. |
@@ -19,7 +19,7 @@ fn stub_config() -> Config { | |||
/// EC2 replies with `<nextToken></nextToken>` which our XML parser parses as empty string and not "none" | |||
#[tokio::test] | |||
async fn paginators_handle_empty_tokens() { | |||
let request= "Action=DescribeSpotPriceHistory&Version=2016-11-15&AvailabilityZone=eu-north-1a&InstanceType.1=g5.48xlarge&ProductDescription.1=Linux%2FUNIX"; | |||
let request= "Action=DescribeSpotPriceHistory&Version=2016-11-15&AvailabilityZone=eu-north-1a&DryRun=false&InstanceType.1=g5.48xlarge&MaxResults=0&ProductDescription.1=Linux%2FUNIX"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies again, I should have been clearer. For clients, the default should be set for responses, but not for requests. From the spec:
To allow servers to change default values if necessary, clients SHOULD NOT serialize default values unless the member is explicitly set to the default value or marked with the default trait. This implies that clients SHOULD implement a kind of "presence tracking" of defaulted members so that the member is only serialized if it is explicitly set to the default value.
There shouldn't be any changes to these tests.
We need to implement presence tracking anyway for nullability support: #1767
I think you have a couple of options for moving forward:
- Feature flag handling the default trait for clients and have it disabled for now until presence tracking is implemented. File an issue to track finishing it.
- Implement presence tracking in this PR.
...c/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/BuilderGenerator.kt
Outdated
Show resolved
Hide resolved
A new generated diff is ready to view.
A new doc preview is ready to view. |
A new generated diff is ready to view.
A new doc preview is ready to view. |
A new generated diff is ready to view.
A new doc preview is ready to view. |
A new generated diff is ready to view.
A new doc preview is ready to view. |
A new generated diff is ready to view.
A new doc preview is ready to view. |
A new generated diff is ready to view.
A new doc preview is ready to view. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation is a good start, but needs more work and we especially need a lot more testing to be confident that it is correct.
@@ -132,3 +132,22 @@ structure StoreServiceBlobInput { | |||
structure StoreServiceBlobOutput { | |||
|
|||
} | |||
|
|||
@http(method: "POST", uri: "/default-value") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. @82marbag let's move this to a Kotlin unit test.
It needs to test @default
on all supported shapes. And not only that compilation succeeds, but that when using the builders, the default value is used when none is provided.
...c/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/BuilderGenerator.kt
Outdated
Show resolved
Hide resolved
@@ -275,7 +294,7 @@ open class ServerCodegenVisitor( | |||
|
|||
if (!codegenContext.settings.codegenConfig.publicConstrainedTypes) { | |||
val serverBuilderGeneratorWithoutPublicConstrainedTypes = | |||
ServerBuilderGeneratorWithoutPublicConstrainedTypes(codegenContext, shape) | |||
ServerBuilderGeneratorWithoutPublicConstrainedTypes(codegenContext, shape, ::renderDefaultBuilder) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to pass in this function. The server builder generators should host it.
...er/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt
Outdated
Show resolved
Hide resolved
...er/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt
Outdated
Show resolved
Hide resolved
...otlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGenerator.kt
Show resolved
Hide resolved
@@ -160,6 +164,10 @@ class ServerBuilderGeneratorWithoutPublicConstrainedTypes( | |||
withBlock("$memberName: self.$memberName", ",") { | |||
serverBuilderConstraintViolations.forMember(member)?.also { | |||
rust(".ok_or(ConstraintViolation::${it.name()})?") | |||
} ?: run { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, we need to inject the writable before constraint checking.
We also need unit tests that exercise this builder type.
.../software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedUnionGenerator.kt
Outdated
Show resolved
Hide resolved
...n/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorTest.kt
Outdated
Show resolved
Hide resolved
...n/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorTest.kt
Outdated
Show resolved
Hide resolved
A new generated diff is ready to view.
A new doc preview is ready to view. |
A new generated diff is ready to view.
A new doc preview is ready to view. |
A new generated diff is ready to view.
A new doc preview is ready to view. |
A new generated diff is ready to view.
A new doc preview is ready to view. |
A new generated diff is ready to view.
A new doc preview is ready to view. |
A new generated diff is ready to view.
A new doc preview is ready to view. |
A new generated diff is ready to view.
A new doc preview is ready to view. |
...are/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderConstraintViolations.kt
Outdated
Show resolved
Hide resolved
@@ -257,6 +290,9 @@ class ServerBuilderGenerator( | |||
"fn build_enforcing_all_constraints(self) -> #{ReturnType:W}", | |||
"ReturnType" to buildFnReturnType(isBuilderFallible, structureSymbol), | |||
) { | |||
if (members.mapNotNull { serverBuilderConstraintViolations.forDefaultMember(it) }.isNotEmpty()) { | |||
Attribute.Custom("allow(clippy::useless_conversion)").render(this) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What model is Clippy complaining about if we don't opt out of this lint?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made codegen a bit more complex to remove this and left a comment where needed
.isRustBoxed() | ||
if (hasBox) { | ||
rustTemplate( | ||
val defaultWrap: (inner: String) -> String = { inner -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With this model:
structure ConstrainedShapesOperationInputOutput {
@default("ab")
defaultLengthString: DefaultLengthString,
@required
lengthString: LengthString,
}
This is generating:
fn build_enforcing_all_constraints(
self,
) -> Result<crate::input::ConstrainedShapesOperationInput, ConstraintViolation> {
Ok(crate::input::ConstrainedShapesOperationInput {
default_length_string: self
.default_length_string
.map(Ok)
.unwrap_or_else(|| {
String::from("ab")
.try_into()
.map_err(|_| ConstraintViolation::InvalidDefaultForDefaultLengthString)
})
.map(|v| match v {
crate::constrained::MaybeConstrained::Constrained(x) => Ok(x),
crate::constrained::MaybeConstrained::Unconstrained(x) => x.try_into(),
})
.map(|res| res.map_err(ConstraintViolation::DefaultLengthString))??,
length_string: self
.length_string
.map(|v| match v {
crate::constrained::MaybeConstrained::Constrained(x) => Ok(x),
crate::constrained::MaybeConstrained::Unconstrained(x) => x.try_into(),
})
.map(|res| res.map_err(ConstraintViolation::LengthString))
.transpose()?
.ok_or(ConstraintViolation::MissingLengthString)?,
})
}
This technically works, but note how the default_length_string
code is less efficient. It's artificially wrapping the Option
in a Result
via map(Ok)
only to then unwrap it in the next line, to then wrap the Result
in a Result
that gets doubly unwrapped via ??
at the end!
I tried for a long time to rewrite this more efficiently but still using a functional approach (i.e. chaining method calls all the way through so that the codegen code is easily composable), but I couldn't get it to work.
The chain was getting long and complex to understand anyway, so here's what I came up with:
default_length_string: {
let v = self.default_length_string.unwrap_or_else(|| {
crate::constrained::MaybeConstrained::Unconstrained(String::from("ab"))
});
match v {
crate::constrained::MaybeConstrained::Constrained(x) => Ok(x),
crate::constrained::MaybeConstrained::Unconstrained(x) => x.try_into(),
}
.map_err(ConstraintViolation::DefaultLengthString)?
},
Not only is this more efficient and easier to understand, I think it generalizes better to the other cases. For instance, here's how length_string
would look like:
length_string: {
let v = self
.length_string
.ok_or(ConstraintViolation::MissingLengthString)?;
match v {
crate::constrained::MaybeConstrained::Constrained(x) => Ok(x),
crate::constrained::MaybeConstrained::Unconstrained(x) => x.try_into(),
}
.map_err(ConstraintViolation::LengthString)?
},
You also don't end up needing to special-case @default
-handling like we're doing right now with the ??
.
So, the general approach would be:
- open a scope
{}
where we can have intermediate variable bindings. - check for
@required
or@default
first. - enforce the rest of the constraint traits last.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Talked over this offline: we decided to revert back to the strategy of relying on java/kotlin checks (generation time) for constrained default values. The current path would lead to complexity and maintainability issues
...otlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGenerator.kt
Outdated
Show resolved
Hide resolved
} | ||
|
||
private fun localParameters(): Stream<Arguments> { | ||
val builderWriters = listOf(::writeServerBuilderGenerator, ::writeServerBuilderGeneratorWithoutPublicConstrainedTypes) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
val builderWriters = listOf(::writeServerBuilderGenerator, ::writeServerBuilderGeneratorWithoutPublicConstrainedTypes) | |
val builderWriters = Stream.of(::writeServerBuilderGenerator, ::writeServerBuilderGeneratorWithoutPublicConstrainedTypes) |
You then don't need the stream()
call below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't because the stream is consumed after the first use, that's why it's constructed again
...n/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorTest.kt
Outdated
Show resolved
Hide resolved
setupFiles(this, model, symbolProvider) | ||
|
||
val rustValues = setupValuesForTest(testConfig.values) | ||
val setters = if (testConfig.requiredTrait) structSetters(rustValues) else writable { } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems odd. It's only setting the values on the builder when the members are @required
. What we want is to not set them when the members are @default
, and test that the built structure uses the modeled default values.
@required
should have no bearing on our assertions / generated code. Note the spec says both @default
and @required
should generate non-Option
al structure fields, so we're only testing that case out of an abundance of caution.
...n/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorTest.kt
Outdated
Show resolved
Hide resolved
...n/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorTest.kt
Outdated
Show resolved
Hide resolved
...n/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorTest.kt
Outdated
Show resolved
Hide resolved
Signed-off-by: Daniele Ahmed <[email protected]>
Signed-off-by: Daniele Ahmed <[email protected]>
Signed-off-by: Daniele Ahmed <[email protected]> Co-authored-by: david-perez <[email protected]>
Signed-off-by: Daniele Ahmed <[email protected]>
A new generated diff is ready to view.
A new doc preview is ready to view. |
We need a changelog entry. |
A new generated diff is ready to view.
A new doc preview is ready to view. |
A new generated diff is ready to view.
A new doc preview is ready to view. |
Motivation and Context
The
@default
trait is not supported.Description
Add support for the
@default
trait.Issue: #1860
Reference for the trait:
https://awslabs.github.io/smithy/2.0/spec/type-refinement-traits.html
I have updated the simple model to use version 2. If it is better to split in two and keep the current at version 1, I will change this.
Checklist
CHANGELOG.next.toml
if I made changes to the smithy-rs codegen or runtime cratesCHANGELOG.next.toml
if I made changes to the AWS SDK, generated SDK code, or SDK runtime cratesBy submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.