Skip to content

Commit 3324dd5

Browse files
authored
Add validation and codegen for custom validation exception traits (#4317)
## Motivation and Context <!--- Why is this change required? What problem does it solve? --> <!--- If it fixes an open issue, please link to the issue here --> Adds ability to define custom validation exceptions per RFC: - https://github.com/smithy-lang/smithy-rs/blob/custom-validation-rfc/design/src/rfcs/rfc0047_custom_validation.md ## Description <!--- Describe your changes in detail --> Adds ability to use the following traits: - @validationException - @validationMessage - @validationFieldList - @validationFieldName - @validationFieldMessage to define a custom validation exception for a service or operation. Updated Smithy validation ensures: - The custom validation exception shape also has @error trait - The custom validation exception shape has exactly one member with the @validationMessage trait - Default constructibility if it contains constrained shapes - At most one custom validation exception is defined - `smithy.framework#ValidationException` is not used in an operation or service is a custom validation exception is defined - Operations with constrained input have exactly one of the default validation exception or a custom validation exception attached to their errors. ## Testing <!--- Please describe in detail how you tested your changes --> <!--- Include details of your testing environment, and the tests you ran to --> <!--- see how your change affects other areas of the code, etc. --> - For unit testing, the following were added/updated: - `codegen-server-traits/src/test/kotlin/software/amazon/smithy/rust/codegen/server/traits/ValidationExceptionTraitTest.kt` - `codegen-server-traits/src/test/kotlin/software/amazon/smithy/rust/codegen/server/traits/ValidationMessageTraitTest.kt` - `codegen-server-traits/src/test/kotlin/software/amazon/smithy/rust/codegen/server/traits/ValidationFieldListTraitTest.kt` - `codegen-server-traits/src/test/kotlin/software/amazon/smithy/rust/codegen/server/traits/ValidationFieldNameTraitTest.kt` - `codegen-server-traits/src/test/kotlin/software/amazon/smithy/rust/codegen/server/traits/ValidationFieldMessageTraitTest.kt` - `codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraintsAreNotUsedTest.kt` - `codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/CustomValidationExceptionDecoratorTest.kt` - `codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/validators/CustomValidationExceptionValidatorTest.kt` - For integration testing - serverIntegrationTests in `codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/CustomValidationExceptionDecoratorTest.kt` - For e2e integration testing - `codegen-server-test/custom-test-models/custom-validation-exception.smithy` - `codegen-server-test/build.gradle.kts` - For regression testing - The existing e2e integration tests in `codegen-server-tests` use common models in `codegen-core/common-test-models`, which use the default `smithy.framework#ValidationException` Ran with `export RUSTFLAGS="-D warnings -A clippy::redundant_closure -A non_local_definitions"` until fixed in server runtime crates. See #4122. ## Checklist <!--- If a checkbox below is not applicable, then please DELETE it rather than leaving it unchecked --> - [x] For changes to the smithy-rs codegen or runtime crates, I have created a changelog entry Markdown file in the `.changelog` directory, specifying "client," "server," or both in the `applies_to` key. - [x] For changes to the AWS SDK, generated SDK code, or SDK runtime crates, I have created a changelog entry Markdown file in the `.changelog` directory, specifying "aws-sdk-rust" in the `applies_to` key. ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._
1 parent 108d231 commit 3324dd5

File tree

30 files changed

+2209
-62
lines changed

30 files changed

+2209
-62
lines changed

.changelog/1759254918.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
applies_to: ["server"]
3+
authors: ["jasgin"]
4+
references: ["smithy-rs#4317"]
5+
breaking: false
6+
new_feature: true
7+
bug_fix: false
8+
---
9+
Adds validators and codegen support for the custom traits custom traits `@validationException`, `@validationMessage`,
10+
`@validationFieldList`, `@validationFieldName`, and `@validationFieldMessage` for defining a custom validation exception
11+
to use instead of `smithy.framework#ValidationException`.

codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/util/Smithy.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ fun Shape.targetOrSelf(model: Model): Shape =
132132
else -> this
133133
}
134134

135+
fun MemberShape.targetShape(model: Model): Shape = model.expectShape(this.target)
136+
135137
/** Kotlin sugar for hasTrait() check. e.g. shape.hasTrait<EnumTrait>() instead of shape.hasTrait(EnumTrait::class.java) */
136138
inline fun <reified T : Trait> Shape.hasTrait(): Boolean = hasTrait(T::class.java)
137139

codegen-server-test/build.gradle.kts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ smithy {
3333
format.set(false)
3434
}
3535

36-
val allCodegenTests = "../codegen-core/common-test-models".let { commonModels ->
36+
val commonCodegenTests = "../codegen-core/common-test-models".let { commonModels ->
3737
listOf(
3838
CodegenTest(
3939
"crate#Config",
@@ -118,6 +118,18 @@ val allCodegenTests = "../codegen-core/common-test-models".let { commonModels ->
118118
// When iterating on protocol tests use this to speed up codegen:
119119
// .filter { it.module == "rpcv2Cbor_extras" || it.module == "rpcv2Cbor_extras_no_initial_response" }
120120

121+
val customCodegenTests = "custom-test-models".let { customModels ->
122+
listOf(
123+
CodegenTest(
124+
"com.aws.example#CustomValidationExample",
125+
"custom-validation-exception-example",
126+
imports = listOf("$customModels/custom-validation-exception.smithy"),
127+
),
128+
)
129+
}
130+
131+
val allCodegenTests = commonCodegenTests + customCodegenTests
132+
121133
project.registerGenerateSmithyBuildTask(rootProject, pluginName, allCodegenTests)
122134
project.registerGenerateCargoWorkspaceTask(rootProject, pluginName, allCodegenTests, workingDirUnderBuildDir)
123135
project.registerGenerateCargoConfigTomlTask(layout.buildDirectory.dir(workingDirUnderBuildDir).get().asFile)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
$version: "2.0"
2+
3+
namespace com.aws.example
4+
5+
use aws.protocols#restJson1
6+
use smithy.framework.rust#validationException
7+
use smithy.framework.rust#validationFieldList
8+
use smithy.framework.rust#validationFieldMessage
9+
use smithy.framework.rust#validationFieldName
10+
use smithy.framework.rust#validationMessage
11+
12+
@restJson1
13+
service CustomValidationExample {
14+
version: "1.0.0"
15+
operations: [
16+
TestOperation
17+
]
18+
errors: [
19+
MyCustomValidationException
20+
]
21+
}
22+
23+
@http(method: "POST", uri: "/test")
24+
operation TestOperation {
25+
input: TestInput
26+
}
27+
28+
structure TestInput {
29+
@required
30+
@length(min: 1, max: 10)
31+
name: String
32+
33+
@range(min: 1, max: 100)
34+
age: Integer
35+
}
36+
37+
@error("client")
38+
@httpError(400)
39+
@validationException
40+
structure MyCustomValidationException {
41+
@required
42+
@validationMessage
43+
customMessage: String
44+
45+
@required
46+
@default("testReason1")
47+
reason: ValidationExceptionReason
48+
49+
@validationFieldList
50+
customFieldList: CustomValidationFieldList
51+
}
52+
53+
enum ValidationExceptionReason {
54+
TEST_REASON_0 = "testReason0"
55+
TEST_REASON_1 = "testReason1"
56+
}
57+
58+
structure CustomValidationField {
59+
@required
60+
@validationFieldName
61+
customFieldName: String
62+
63+
@required
64+
@validationFieldMessage
65+
customFieldMessage: String
66+
}
67+
68+
list CustomValidationFieldList {
69+
member: CustomValidationField
70+
}

codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/Constraints.kt

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import software.amazon.smithy.model.Model
1010
import software.amazon.smithy.model.shapes.BlobShape
1111
import software.amazon.smithy.model.shapes.ByteShape
1212
import software.amazon.smithy.model.shapes.CollectionShape
13+
import software.amazon.smithy.model.shapes.EnumShape
14+
import software.amazon.smithy.model.shapes.IntEnumShape
1315
import software.amazon.smithy.model.shapes.IntegerShape
1416
import software.amazon.smithy.model.shapes.LongShape
1517
import software.amazon.smithy.model.shapes.MapShape
@@ -97,6 +99,39 @@ fun Shape.isDirectlyConstrained(symbolProvider: SymbolProvider): Boolean =
9799
this.members().any { !symbolProvider.toSymbol(it).isOptional() && !it.hasNonNullDefault() }
98100
}
99101

102+
else -> this.isDirectlyConstrainedHelper()
103+
}
104+
105+
/**
106+
* Finds shapes that are directly constrained in validation phase, which means the shape is a:
107+
* - [StructureShape] with a required member that does not have a non-null default
108+
* - [EnumShape]
109+
* - [IntEnumShape]
110+
* - [MemberShape] that is required and does not have a non-null default
111+
*
112+
* We use this rather than [Shape.isDirectlyConstrained] to check for constrained shapes in validation phase because
113+
* the [SymbolProvider] has not yet been created
114+
*/
115+
fun Shape.isDirectlyConstrainedForValidation(): Boolean =
116+
when (this) {
117+
is StructureShape -> {
118+
// we use `member.isOptional` here because the issue outlined in (https://github.com/smithy-lang/smithy-rs/issues/1302)
119+
// should not be relevant in validation phase
120+
this.members().any { !it.isOptional && !it.hasNonNullDefault() }
121+
}
122+
123+
// For alignment with
124+
// (https://github.com/smithy-lang/smithy-rs/blob/custom-validation-rfc/design/src/rfcs/rfc0047_custom_validation.md#terminology)
125+
// TODO(move to [isDirectlyConstrainerHelper] if they can be safely applied to [isDirectlyConstrained] without breaking implications)
126+
is EnumShape -> true
127+
is IntEnumShape -> true
128+
is MemberShape -> !this.isOptional && !this.hasNonNullDefault()
129+
130+
else -> this.isDirectlyConstrainedHelper()
131+
}
132+
133+
private fun Shape.isDirectlyConstrainedHelper(): Boolean =
134+
when (this) {
100135
is MapShape -> this.hasTrait<LengthTrait>()
101136
is StringShape -> this.hasTrait<EnumTrait>() || supportedStringConstraintTraits.any { this.hasTrait(it) }
102137
is CollectionShape -> supportedCollectionConstraintTraits.any { this.hasTrait(it) }
@@ -129,11 +164,27 @@ fun Shape.canReachConstrainedShape(
129164
DirectedWalker(model).walkShapes(this).toSet().any { it.isDirectlyConstrained(symbolProvider) }
130165
}
131166

167+
/**
168+
* Whether this shape (or the shape's target for [MemberShape]s) can reach constrained shapes for validations.
169+
*
170+
* We use this rather than [Shape.canReachConstrainedShape] to check for constrained shapes in validation phase because
171+
* the [SymbolProvider] has not yet been created
172+
*/
173+
fun Shape.canReachConstrainedShapeForValidation(model: Model): Boolean =
174+
if (this is MemberShape) {
175+
this.targetCanReachConstrainedShapeForValidation(model)
176+
} else {
177+
DirectedWalker(model).walkShapes(this).toSet().any { it.isDirectlyConstrainedForValidation() }
178+
}
179+
132180
fun MemberShape.targetCanReachConstrainedShape(
133181
model: Model,
134182
symbolProvider: SymbolProvider,
135183
): Boolean = model.expectShape(this.target).canReachConstrainedShape(model, symbolProvider)
136184

185+
fun MemberShape.targetCanReachConstrainedShapeForValidation(model: Model): Boolean =
186+
model.expectShape(this.target).canReachConstrainedShapeForValidation(model)
187+
137188
fun Shape.hasPublicConstrainedWrapperTupleType(
138189
model: Model,
139190
publicConstrainedTypes: Boolean,

codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/RustServerCodegenPlugin.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.SymbolVisitor
2020
import software.amazon.smithy.rust.codegen.server.smithy.customizations.CustomValidationExceptionWithReasonDecorator
2121
import software.amazon.smithy.rust.codegen.server.smithy.customizations.ServerRequiredCustomizations
2222
import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionDecorator
23+
import software.amazon.smithy.rust.codegen.server.smithy.customizations.UserProvidedValidationExceptionDecorator
2324
import software.amazon.smithy.rust.codegen.server.smithy.customize.CombinedServerCodegenDecorator
2425
import software.amazon.smithy.rust.codegen.server.smithy.customize.ServerCodegenDecorator
2526
import software.amazon.smithy.rust.codegen.server.smithy.testutil.ServerDecoratableBuildPlugin
@@ -50,6 +51,7 @@ class RustServerCodegenPlugin : ServerDecoratableBuildPlugin() {
5051
CombinedServerCodegenDecorator.fromClasspath(
5152
context,
5253
ServerRequiredCustomizations(),
54+
UserProvidedValidationExceptionDecorator(),
5355
SmithyValidationExceptionDecorator(),
5456
CustomValidationExceptionWithReasonDecorator(),
5557
*decorator,

codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ open class ServerCodegenVisitor(
238238

239239
val validationExceptionShapeId = validationExceptionConversionGenerator.shapeId
240240
for (validationResult in listOf(
241+
validateModelHasAtMostOneValidationException(model, service),
241242
codegenDecorator.postprocessValidationExceptionNotAttachedErrorMessage(
242243
validateOperationsWithConstrainedInputHaveValidationExceptionAttached(
243244
model,
@@ -246,6 +247,13 @@ open class ServerCodegenVisitor(
246247
),
247248
),
248249
validateUnsupportedConstraints(model, service, codegenContext.settings.codegenConfig),
250+
codegenDecorator.postprocessMultipleValidationExceptionsErrorMessage(
251+
validateOperationsWithConstrainedInputHaveOneValidationExceptionAttached(
252+
model,
253+
service,
254+
validationExceptionShapeId,
255+
),
256+
),
249257
)) {
250258
for (logMessage in validationResult.messages) {
251259
// TODO(https://github.com/smithy-lang/smithy-rs/issues/1756): These are getting duplicated.

0 commit comments

Comments
 (0)