Skip to content

Commit aa4eebc

Browse files
committed
Move converters from constraint violations into ValidationException to decorators
This allows "easily" converting to other custom validation exception shapes by implementing a decorator. As a simple example, this PR adds a `CustomValidationExceptionWithReasonDecorator` decorator that is able to convert into a shape that is very similar to `smithy.framework#ValidationException`, but that has an additional `reason` field. The decorator can be enabled via the newly added `experimentalCustomValidationExceptionWithReasonPleaseDoNotUse` codegen config flag. This effectively provides a way for users to use custom validation exceptions without having to wait for the full implementation of #2053, provided they're interested enough to write a decorator in a JVM language. This mechanism is _experimental_ and will be removed once full support for custom validation exceptions as described in #2053 lands, hence why the configuration key is strongly worded in this respect. This commit also ports the mechanism to run codegen integration tests within Kotlin unit tests for client SDKs to the server. See #1956 for details. The custom validation exception decorator is tested this way.
1 parent 7bf9251 commit aa4eebc

File tree

51 files changed

+1279
-362
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1279
-362
lines changed

aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/TestUtil.kt

+7-4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import software.amazon.smithy.rust.codegen.client.testutil.testCodegenContext
1313
import software.amazon.smithy.rust.codegen.core.smithy.CoreRustSettings
1414
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeCrateLocation
1515
import software.amazon.smithy.rust.codegen.core.smithy.RustCrate
16+
import software.amazon.smithy.rust.codegen.core.testutil.IntegrationTestParams
1617
import software.amazon.smithy.rust.codegen.core.testutil.TestRuntimeConfig
1718
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
1819
import software.amazon.smithy.rust.codegen.core.testutil.testRustSettings
@@ -39,9 +40,10 @@ fun awsSdkIntegrationTest(
3940
test: (ClientCodegenContext, RustCrate) -> Unit = { _, _ -> },
4041
) =
4142
clientIntegrationTest(
42-
model, runtimeConfig = AwsTestRuntimeConfig,
43-
additionalSettings = ObjectNode.builder()
44-
.withMember(
43+
model,
44+
IntegrationTestParams(
45+
runtimeConfig = AwsTestRuntimeConfig,
46+
additionalSettings = ObjectNode.builder().withMember(
4547
"customizationConfig",
4648
ObjectNode.builder()
4749
.withMember(
@@ -51,6 +53,7 @@ fun awsSdkIntegrationTest(
5153
.build(),
5254
).build(),
5355
)
54-
.withMember("codegen", ObjectNode.builder().withMember("includeFluentClient", false).build()).build(),
56+
.withMember("codegen", ObjectNode.builder().withMember("includeFluentClient", false).build()).build(),
57+
),
5558
test = test,
5659
)

codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/RustClientCodegenPlugin.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import software.amazon.smithy.rust.codegen.client.smithy.customize.NoOpEventStre
1616
import software.amazon.smithy.rust.codegen.client.smithy.customize.RequiredCustomizations
1717
import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointsDecorator
1818
import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentClientDecorator
19-
import software.amazon.smithy.rust.codegen.client.testutil.DecoratableBuildPlugin
19+
import software.amazon.smithy.rust.codegen.client.testutil.ClientDecoratableBuildPlugin
2020
import software.amazon.smithy.rust.codegen.core.rustlang.Attribute.Companion.NonExhaustive
2121
import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWordSymbolProvider
2222
import software.amazon.smithy.rust.codegen.core.smithy.BaseSymbolMetadataProvider
@@ -36,7 +36,7 @@ import java.util.logging.Logger
3636
* `resources/META-INF.services/software.amazon.smithy.build.SmithyBuildPlugin` refers to this class by name which
3737
* enables the smithy-build plugin to invoke `execute` with all Smithy plugin context + models.
3838
*/
39-
class RustClientCodegenPlugin : DecoratableBuildPlugin() {
39+
class RustClientCodegenPlugin : ClientDecoratableBuildPlugin() {
4040
override fun getName(): String = "rust-client-codegen"
4141

4242
override fun executeWithDecorator(
@@ -66,10 +66,10 @@ class RustClientCodegenPlugin : DecoratableBuildPlugin() {
6666
}
6767

6868
companion object {
69-
/** SymbolProvider
69+
/**
7070
* When generating code, smithy types need to be converted into Rust types—that is the core role of the symbol provider
7171
*
72-
* The Symbol provider is composed of a base `SymbolVisitor` which handles the core functionality, then is layered
72+
* The Symbol provider is composed of a base [SymbolVisitor] which handles the core functionality, then is layered
7373
* with other symbol providers, documented inline, to handle the full scope of Smithy types.
7474
*/
7575
fun baseSymbolProvider(model: Model, serviceShape: ServiceShape, symbolVisitorConfig: SymbolVisitorConfig) =
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.rust.codegen.client.testutil
7+
8+
import software.amazon.smithy.build.PluginContext
9+
import software.amazon.smithy.build.SmithyBuildPlugin
10+
import software.amazon.smithy.model.Model
11+
import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext
12+
import software.amazon.smithy.rust.codegen.client.smithy.RustClientCodegenPlugin
13+
import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator
14+
import software.amazon.smithy.rust.codegen.core.smithy.RustCrate
15+
import software.amazon.smithy.rust.codegen.core.testutil.IntegrationTestParams
16+
import software.amazon.smithy.rust.codegen.core.testutil.codegenIntegrationTest
17+
import java.nio.file.Path
18+
19+
fun clientIntegrationTest(
20+
model: Model,
21+
params: IntegrationTestParams = IntegrationTestParams(),
22+
additionalDecorators: List<ClientCodegenDecorator> = listOf(),
23+
test: (ClientCodegenContext, RustCrate) -> Unit = { _, _ -> },
24+
): Path {
25+
fun invokeRustCodegenPlugin(ctx: PluginContext) {
26+
val codegenDecorator = object : ClientCodegenDecorator {
27+
override val name: String = "Add tests"
28+
override val order: Byte = 0
29+
30+
override fun classpathDiscoverable(): Boolean = false
31+
32+
override fun extras(codegenContext: ClientCodegenContext, rustCrate: RustCrate) {
33+
test(codegenContext, rustCrate)
34+
}
35+
}
36+
RustClientCodegenPlugin().executeWithDecorator(ctx, codegenDecorator, *additionalDecorators.toTypedArray())
37+
}
38+
return codegenIntegrationTest(model, params, invokePlugin = ::invokeRustCodegenPlugin)
39+
}
40+
41+
/**
42+
* A `SmithyBuildPlugin` that accepts an additional decorator.
43+
*
44+
* This exists to allow tests to easily customize the _real_ build without needing to list out customizations
45+
* or attempt to manually discover them from the path.
46+
*/
47+
abstract class ClientDecoratableBuildPlugin : SmithyBuildPlugin {
48+
abstract fun executeWithDecorator(
49+
context: PluginContext,
50+
vararg decorator: ClientCodegenDecorator,
51+
)
52+
53+
override fun execute(context: PluginContext) {
54+
executeWithDecorator(context)
55+
}
56+
}

codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/testutil/CodegenIntegrationTest.kt

-103
This file was deleted.

codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/customizations/HttpVersionListGeneratorTest.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
1919
import software.amazon.smithy.rust.codegen.core.rustlang.writable
2020
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig
2121
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
22+
import software.amazon.smithy.rust.codegen.core.testutil.IntegrationTestParams
2223
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
2324
import software.amazon.smithy.rust.codegen.core.testutil.integrationTest
2425

@@ -170,8 +171,8 @@ internal class HttpVersionListGeneratorTest {
170171

171172
clientIntegrationTest(
172173
model,
173-
listOf(FakeSigningDecorator()),
174-
addModuleToEventStreamAllowList = true,
174+
IntegrationTestParams(addModuleToEventStreamAllowList = true),
175+
additionalDecorators = listOf(FakeSigningDecorator()),
175176
) { clientCodegenContext, rustCrate ->
176177
val moduleName = clientCodegenContext.moduleUseName()
177178
rustCrate.integrationTest("validate_eventstream_http") {

codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointsDecoratorTest.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import org.junit.jupiter.api.Test
1111
import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest
1212
import software.amazon.smithy.rust.codegen.core.rustlang.Attribute
1313
import software.amazon.smithy.rust.codegen.core.rustlang.rust
14+
import software.amazon.smithy.rust.codegen.core.testutil.IntegrationTestParams
1415
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
1516
import software.amazon.smithy.rust.codegen.core.testutil.integrationTest
1617
import software.amazon.smithy.rust.codegen.core.testutil.runWithWarnings
@@ -123,8 +124,8 @@ class EndpointsDecoratorTest {
123124
fun `set an endpoint in the property bag`() {
124125
val testDir = clientIntegrationTest(
125126
model,
126-
// just run integration tests
127-
command = { "cargo test --test *".runWithWarnings(it) },
127+
// Just run integration tests.
128+
IntegrationTestParams(command = { "cargo test --test *".runWithWarnings(it) }),
128129
) { clientCodegenContext, rustCrate ->
129130
rustCrate.integrationTest("endpoint_params_test") {
130131
val moduleName = clientCodegenContext.moduleUseName()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
$version: "2.0"
2+
3+
namespace com.amazonaws.constraints
4+
5+
use aws.protocols#restJson1
6+
7+
enum ValidationExceptionFieldReason {
8+
LENGTH_NOT_VALID = "LengthNotValid"
9+
PATTERN_NOT_VALID = "PatternNotValid"
10+
SYNTAX_NOT_VALID = "SyntaxNotValid"
11+
VALUE_NOT_VALID = "ValueNotValid"
12+
OTHER = "Other"
13+
}
14+
15+
/// Stores information about a field passed inside a request that resulted in an exception.
16+
structure ValidationExceptionField {
17+
/// The field name.
18+
@required
19+
Name: String
20+
21+
@required
22+
Reason: ValidationExceptionFieldReason
23+
24+
/// Message describing why the field failed validation.
25+
@required
26+
Message: String
27+
}
28+
29+
/// A list of fields.
30+
list ValidationExceptionFieldList {
31+
member: ValidationExceptionField
32+
}
33+
34+
enum ValidationExceptionReason {
35+
FIELD_VALIDATION_FAILED = "FieldValidationFailed"
36+
UNKNOWN_OPERATION = "UnknownOperation"
37+
CANNOT_PARSE = "CannotParse"
38+
OTHER = "Other"
39+
}
40+
41+
/// The input fails to satisfy the constraints specified by an AWS service.
42+
@error("client")
43+
@httpError(400)
44+
structure ValidationException {
45+
/// Description of the error.
46+
@required
47+
Message: String
48+
49+
/// Reason the request failed validation.
50+
@required
51+
Reason: ValidationExceptionReason
52+
53+
/// The field that caused the error, if applicable. If more than one field
54+
/// caused the error, pick one and elaborate in the message.
55+
Fields: ValidationExceptionFieldList
56+
}
57+
58+
/// A service to test (experimental support for) custom validation exceptions.
59+
@restJson1
60+
@title("CustomValidationExceptionsExperimental")
61+
service CustomValidationExceptionsExperimental {
62+
operations: [
63+
ConstrainedShapesOperation,
64+
],
65+
}
66+
67+
@http(uri: "/constrained-shapes-operation", method: "POST")
68+
operation ConstrainedShapesOperation {
69+
input: ConstrainedShapesOperationInputOutput,
70+
output: ConstrainedShapesOperationInputOutput,
71+
errors: [ValidationException]
72+
}
73+
74+
structure ConstrainedShapesOperationInputOutput {
75+
@required
76+
lengthString: LengthString,
77+
}
78+
79+
@length(min: 2, max: 69)
80+
string LengthString

codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ class RustWriter private constructor(
481481
* Callers must take care to use [this] when writing to ensure code is written to the right place:
482482
* ```kotlin
483483
* val writer = RustWriter.forModule("model")
484-
* writer.withModule(RustModule.public("nested")) {
484+
* writer.withInlineModule(RustModule.public("nested")) {
485485
* Generator(...).render(this) // GOOD
486486
* Generator(...).render(writer) // WRONG!
487487
* }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.rust.codegen.core.testutil
7+
8+
import software.amazon.smithy.build.PluginContext
9+
import software.amazon.smithy.model.Model
10+
import software.amazon.smithy.model.node.ObjectNode
11+
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig
12+
import software.amazon.smithy.rust.codegen.core.util.runCommand
13+
import java.io.File
14+
import java.nio.file.Path
15+
16+
/**
17+
* A helper class holding common data with defaults that is threaded through several functions, to make their
18+
* signatures shorter.
19+
*/
20+
data class IntegrationTestParams(
21+
val addModuleToEventStreamAllowList: Boolean = false,
22+
val service: String? = null,
23+
val runtimeConfig: RuntimeConfig? = null,
24+
val additionalSettings: ObjectNode = ObjectNode.builder().build(),
25+
val overrideTestDir: File? = null,
26+
val command: ((Path) -> Unit)? = null,
27+
)
28+
29+
/**
30+
* Run cargo test on a true, end-to-end, codegen product of a given model.
31+
*/
32+
fun codegenIntegrationTest(model: Model, params: IntegrationTestParams, invokePlugin: (PluginContext) -> Unit): Path {
33+
val (ctx, testDir) = generatePluginContext(
34+
model,
35+
params.additionalSettings,
36+
params.addModuleToEventStreamAllowList,
37+
params.service,
38+
params.runtimeConfig,
39+
params.overrideTestDir,
40+
)
41+
invokePlugin(ctx)
42+
ctx.fileManifest.printGeneratedFiles()
43+
params.command?.invoke(testDir) ?: "cargo test".runCommand(testDir, environment = mapOf("RUSTFLAGS" to "-D warnings"))
44+
return testDir
45+
}

0 commit comments

Comments
 (0)