diff --git a/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/TestUtil.kt b/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/TestUtil.kt index 8db0d6a1df..9329153ffe 100644 --- a/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/TestUtil.kt +++ b/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/TestUtil.kt @@ -13,6 +13,7 @@ import software.amazon.smithy.rust.codegen.client.testutil.testCodegenContext import software.amazon.smithy.rust.codegen.core.smithy.CoreRustSettings import software.amazon.smithy.rust.codegen.core.smithy.RuntimeCrateLocation import software.amazon.smithy.rust.codegen.core.smithy.RustCrate +import software.amazon.smithy.rust.codegen.core.testutil.IntegrationTestParams import software.amazon.smithy.rust.codegen.core.testutil.TestRuntimeConfig import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.core.testutil.testRustSettings @@ -39,9 +40,10 @@ fun awsSdkIntegrationTest( test: (ClientCodegenContext, RustCrate) -> Unit = { _, _ -> }, ) = clientIntegrationTest( - model, runtimeConfig = AwsTestRuntimeConfig, - additionalSettings = ObjectNode.builder() - .withMember( + model, + IntegrationTestParams( + runtimeConfig = AwsTestRuntimeConfig, + additionalSettings = ObjectNode.builder().withMember( "customizationConfig", ObjectNode.builder() .withMember( @@ -51,6 +53,7 @@ fun awsSdkIntegrationTest( .build(), ).build(), ) - .withMember("codegen", ObjectNode.builder().withMember("includeFluentClient", false).build()).build(), + .withMember("codegen", ObjectNode.builder().withMember("includeFluentClient", false).build()).build(), + ), test = test, ) diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/RustClientCodegenPlugin.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/RustClientCodegenPlugin.kt index aa5d5bafb7..aa94e25b77 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/RustClientCodegenPlugin.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/RustClientCodegenPlugin.kt @@ -16,7 +16,7 @@ import software.amazon.smithy.rust.codegen.client.smithy.customize.NoOpEventStre import software.amazon.smithy.rust.codegen.client.smithy.customize.RequiredCustomizations import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointsDecorator import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentClientDecorator -import software.amazon.smithy.rust.codegen.client.testutil.DecoratableBuildPlugin +import software.amazon.smithy.rust.codegen.client.testutil.ClientDecoratableBuildPlugin import software.amazon.smithy.rust.codegen.core.rustlang.Attribute.Companion.NonExhaustive import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWordSymbolProvider import software.amazon.smithy.rust.codegen.core.smithy.BaseSymbolMetadataProvider @@ -36,7 +36,7 @@ import java.util.logging.Logger * `resources/META-INF.services/software.amazon.smithy.build.SmithyBuildPlugin` refers to this class by name which * enables the smithy-build plugin to invoke `execute` with all Smithy plugin context + models. */ -class RustClientCodegenPlugin : DecoratableBuildPlugin() { +class RustClientCodegenPlugin : ClientDecoratableBuildPlugin() { override fun getName(): String = "rust-client-codegen" override fun executeWithDecorator( diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/testutil/ClientCodegenIntegrationTest.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/testutil/ClientCodegenIntegrationTest.kt new file mode 100644 index 0000000000..138f43cd26 --- /dev/null +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/testutil/ClientCodegenIntegrationTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.client.testutil + +import software.amazon.smithy.build.PluginContext +import software.amazon.smithy.build.SmithyBuildPlugin +import software.amazon.smithy.model.Model +import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext +import software.amazon.smithy.rust.codegen.client.smithy.RustClientCodegenPlugin +import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator +import software.amazon.smithy.rust.codegen.core.smithy.RustCrate +import software.amazon.smithy.rust.codegen.core.testutil.IntegrationTestParams +import software.amazon.smithy.rust.codegen.core.testutil.codegenIntegrationTest +import java.nio.file.Path + +fun clientIntegrationTest( + model: Model, + params: IntegrationTestParams = IntegrationTestParams(), + additionalDecorators: List = listOf(), + test: (ClientCodegenContext, RustCrate) -> Unit = { _, _ -> }, +): Path { + fun invokeRustCodegenPlugin(ctx: PluginContext) { + val codegenDecorator = object : ClientCodegenDecorator { + override val name: String = "Add tests" + override val order: Byte = 0 + + override fun classpathDiscoverable(): Boolean = false + + override fun extras(codegenContext: ClientCodegenContext, rustCrate: RustCrate) { + test(codegenContext, rustCrate) + } + } + RustClientCodegenPlugin().executeWithDecorator(ctx, codegenDecorator, *additionalDecorators.toTypedArray()) + } + return codegenIntegrationTest(model, params, invokePlugin = ::invokeRustCodegenPlugin) +} + +/** + * A `SmithyBuildPlugin` that accepts an additional decorator. + * + * This exists to allow tests to easily customize the _real_ build without needing to list out customizations + * or attempt to manually discover them from the path. + */ +abstract class ClientDecoratableBuildPlugin : SmithyBuildPlugin { + abstract fun executeWithDecorator( + context: PluginContext, + vararg decorator: ClientCodegenDecorator, + ) + + override fun execute(context: PluginContext) { + executeWithDecorator(context) + } +} diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/testutil/CodegenIntegrationTest.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/testutil/CodegenIntegrationTest.kt deleted file mode 100644 index c764e29059..0000000000 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/testutil/CodegenIntegrationTest.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.rust.codegen.client.testutil - -import software.amazon.smithy.build.PluginContext -import software.amazon.smithy.build.SmithyBuildPlugin -import software.amazon.smithy.model.Model -import software.amazon.smithy.model.node.ObjectNode -import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext -import software.amazon.smithy.rust.codegen.client.smithy.RustClientCodegenPlugin -import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator -import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig -import software.amazon.smithy.rust.codegen.core.smithy.RustCrate -import software.amazon.smithy.rust.codegen.core.testutil.generatePluginContext -import software.amazon.smithy.rust.codegen.core.testutil.printGeneratedFiles -import software.amazon.smithy.rust.codegen.core.util.runCommand -import java.io.File -import java.nio.file.Path - -/** - * Run cargo test on a true, end-to-end, codegen product of a given model. - * - * For test purposes, additional codegen decorators can also be composed. - */ -fun clientIntegrationTest( - model: Model, - additionalDecorators: List = listOf(), - addModuleToEventStreamAllowList: Boolean = false, - service: String? = null, - runtimeConfig: RuntimeConfig? = null, - additionalSettings: ObjectNode = ObjectNode.builder().build(), - command: ((Path) -> Unit)? = null, - test: (ClientCodegenContext, RustCrate) -> Unit = { _, _ -> }, -): Path { - return codegenIntegrationTest( - model, - RustClientCodegenPlugin(), - additionalDecorators, - addModuleToEventStreamAllowList = addModuleToEventStreamAllowList, - service = service, - runtimeConfig = runtimeConfig, - additionalSettings = additionalSettings, - test = test, - command = command, - ) -} - -/** - * A Smithy BuildPlugin that accepts an additional decorator - * - * This exists to allow tests to easily customize the _real_ build without needing to list out customizations - * or attempt to manually discover them from the path - */ -abstract class DecoratableBuildPlugin : SmithyBuildPlugin { - abstract fun executeWithDecorator( - context: PluginContext, - vararg decorator: ClientCodegenDecorator, - ) - - override fun execute(context: PluginContext) { - executeWithDecorator(context) - } -} - -// TODO(https://github.com/awslabs/smithy-rs/issues/1864): move to core once CodegenDecorator is in core -private fun codegenIntegrationTest( - model: Model, - buildPlugin: DecoratableBuildPlugin, - additionalDecorators: List, - additionalSettings: ObjectNode = ObjectNode.builder().build(), - addModuleToEventStreamAllowList: Boolean = false, - service: String? = null, - runtimeConfig: RuntimeConfig? = null, - overrideTestDir: File? = null, test: (ClientCodegenContext, RustCrate) -> Unit, - command: ((Path) -> Unit)? = null, -): Path { - val (ctx, testDir) = generatePluginContext( - model, - additionalSettings, - addModuleToEventStreamAllowList, - service, - runtimeConfig, - overrideTestDir, - ) - - val codegenDecorator = object : ClientCodegenDecorator { - override val name: String = "Add tests" - override val order: Byte = 0 - - override fun classpathDiscoverable(): Boolean = false - - override fun extras(codegenContext: ClientCodegenContext, rustCrate: RustCrate) { - test(codegenContext, rustCrate) - } - } - buildPlugin.executeWithDecorator(ctx, codegenDecorator, *additionalDecorators.toTypedArray()) - ctx.fileManifest.printGeneratedFiles() - command?.invoke(testDir) ?: "cargo test".runCommand(testDir, environment = mapOf("RUSTFLAGS" to "-D warnings")) - return testDir -} diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/customizations/HttpVersionListGeneratorTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/customizations/HttpVersionListGeneratorTest.kt index 0c57ba622a..0b860545aa 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/customizations/HttpVersionListGeneratorTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/customizations/HttpVersionListGeneratorTest.kt @@ -19,6 +19,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.rustlang.writable import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.testutil.IntegrationTestParams import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.core.testutil.integrationTest @@ -170,8 +171,8 @@ internal class HttpVersionListGeneratorTest { clientIntegrationTest( model, - listOf(FakeSigningDecorator()), - addModuleToEventStreamAllowList = true, + IntegrationTestParams(addModuleToEventStreamAllowList = true), + additionalDecorators = listOf(FakeSigningDecorator()), ) { clientCodegenContext, rustCrate -> val moduleName = clientCodegenContext.moduleUseName() rustCrate.integrationTest("validate_eventstream_http") { diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointsDecoratorTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointsDecoratorTest.kt index d05f3b21de..d9b398f0ca 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointsDecoratorTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointsDecoratorTest.kt @@ -11,6 +11,7 @@ import org.junit.jupiter.api.Test import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest import software.amazon.smithy.rust.codegen.core.rustlang.Attribute import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.testutil.IntegrationTestParams import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.core.testutil.integrationTest import software.amazon.smithy.rust.codegen.core.testutil.runWithWarnings @@ -123,8 +124,8 @@ class EndpointsDecoratorTest { fun `set an endpoint in the property bag`() { val testDir = clientIntegrationTest( model, - // just run integration tests - command = { "cargo test --test *".runWithWarnings(it) }, + // Just run integration tests. + IntegrationTestParams(command = { "cargo test --test *".runWithWarnings(it) }), ) { clientCodegenContext, rustCrate -> rustCrate.integrationTest("endpoint_params_test") { val moduleName = clientCodegenContext.moduleUseName() diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/CodegenIntegrationTest.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/CodegenIntegrationTest.kt new file mode 100644 index 0000000000..d1f208c393 --- /dev/null +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/CodegenIntegrationTest.kt @@ -0,0 +1,45 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.core.testutil + +import software.amazon.smithy.build.PluginContext +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.node.ObjectNode +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig +import software.amazon.smithy.rust.codegen.core.util.runCommand +import java.io.File +import java.nio.file.Path + +/** + * A helper class holding common data with defaults that is threaded through several functions, to make their + * signatures shorter. + */ +data class IntegrationTestParams( + val addModuleToEventStreamAllowList: Boolean = false, + val service: String? = null, + val runtimeConfig: RuntimeConfig? = null, + val additionalSettings: ObjectNode = ObjectNode.builder().build(), + val overrideTestDir: File? = null, + val command: ((Path) -> Unit)? = null, +) + +/** + * Run cargo test on a true, end-to-end, codegen product of a given model. + */ +fun codegenIntegrationTest(model: Model, params: IntegrationTestParams, invokePlugin: (PluginContext) -> Unit): Path { + val (ctx, testDir) = generatePluginContext( + model, + params.additionalSettings, + params.addModuleToEventStreamAllowList, + params.service, + params.runtimeConfig, + params.overrideTestDir, + ) + invokePlugin(ctx) + ctx.fileManifest.printGeneratedFiles() + params.command?.invoke(testDir) ?: "cargo test".runCommand(testDir, environment = mapOf("RUSTFLAGS" to "-D warnings")) + return testDir +} diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/EventStreamTestTools.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/EventStreamTestTools.kt index 05151b7ce7..34fac717aa 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/EventStreamTestTools.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/EventStreamTestTools.kt @@ -18,6 +18,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext import software.amazon.smithy.rust.codegen.core.smithy.CodegenTarget +import software.amazon.smithy.rust.codegen.core.smithy.DirectedWalker import software.amazon.smithy.rust.codegen.core.smithy.ErrorsModule import software.amazon.smithy.rust.codegen.core.smithy.ModelsModule import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType @@ -118,11 +119,15 @@ object EventStreamTestTools { val symbolProvider = codegenContext.symbolProvider val operationShape = model.expectShape(ShapeId.from("test#TestStreamOp")) as OperationShape val unionShape = model.expectShape(ShapeId.from("test#TestStream")) as UnionShape + val walker = DirectedWalker(model) val project = TestWorkspace.testProject(symbolProvider) val operationSymbol = symbolProvider.toSymbol(operationShape) project.withModule(ErrorsModule) { - val errors = model.structureShapes.filter { shape -> shape.hasTrait() } + val errors = model.serviceShapes + .flatMap { walker.walkShapes(it) } + .filterIsInstance() + .filter { shape -> shape.hasTrait() } requirements.renderOperationError(this, model, symbolProvider, operationSymbol, errors) requirements.renderOperationError(this, model, symbolProvider, symbolProvider.toSymbol(unionShape), errors) for (shape in errors) { diff --git a/codegen-server/build.gradle.kts b/codegen-server/build.gradle.kts index 8dd6dc5d18..f5b2f5bc29 100644 --- a/codegen-server/build.gradle.kts +++ b/codegen-server/build.gradle.kts @@ -26,6 +26,10 @@ dependencies { implementation(project(":codegen-core")) implementation("software.amazon.smithy:smithy-aws-traits:$smithyVersion") implementation("software.amazon.smithy:smithy-protocol-test-traits:$smithyVersion") + + // `smithy.framework#ValidationException` is defined here, which is used in `constraints.smithy`, which is used + // in `CustomValidationExceptionWithReasonDecoratorTest`. + testImplementation("software.amazon.smithy:smithy-validation-model:$smithyVersion") } tasks.compileKotlin { kotlinOptions.jvmTarget = "1.8" } diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerCodegenVisitor.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerCodegenVisitor.kt index edc5493d94..d689298ca1 100644 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerCodegenVisitor.kt +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerCodegenVisitor.kt @@ -132,7 +132,7 @@ class PythonServerCodegenVisitor( */ override fun stringShape(shape: StringShape) { fun pythonServerEnumGeneratorFactory(codegenContext: ServerCodegenContext, writer: RustWriter, shape: StringShape) = - PythonServerEnumGenerator(codegenContext, writer, shape) + PythonServerEnumGenerator(codegenContext, writer, shape, validationExceptionConversionGenerator) stringShape(shape, ::pythonServerEnumGeneratorFactory) } diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/RustServerCodegenPythonPlugin.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/RustServerCodegenPythonPlugin.kt index eb8ac1debb..19e3e0dba2 100644 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/RustServerCodegenPythonPlugin.kt +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/RustServerCodegenPythonPlugin.kt @@ -19,7 +19,9 @@ import software.amazon.smithy.rust.codegen.server.python.smithy.customizations.D import software.amazon.smithy.rust.codegen.server.smithy.ConstrainedShapeSymbolMetadataProvider import software.amazon.smithy.rust.codegen.server.smithy.ConstrainedShapeSymbolProvider import software.amazon.smithy.rust.codegen.server.smithy.DeriveEqAndHashSymbolMetadataProvider +import software.amazon.smithy.rust.codegen.server.smithy.customizations.CustomValidationExceptionWithReasonDecorator import software.amazon.smithy.rust.codegen.server.smithy.customizations.ServerRequiredCustomizations +import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionDecorator import software.amazon.smithy.rust.codegen.server.smithy.customize.CombinedServerCodegenDecorator import java.util.logging.Level import java.util.logging.Logger @@ -50,7 +52,10 @@ class RustServerCodegenPythonPlugin : SmithyBuildPlugin { val codegenDecorator: CombinedServerCodegenDecorator = CombinedServerCodegenDecorator.fromClasspath( context, - CombinedServerCodegenDecorator(DECORATORS + ServerRequiredCustomizations()), + ServerRequiredCustomizations(), + SmithyValidationExceptionDecorator(), + CustomValidationExceptionWithReasonDecorator(), + *DECORATORS, ) // PythonServerCodegenVisitor is the main driver of code generation that traverses the model and generates code diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/customizations/PythonServerCodegenDecorator.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/customizations/PythonServerCodegenDecorator.kt index ff07ba1207..8de0a3f056 100644 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/customizations/PythonServerCodegenDecorator.kt +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/customizations/PythonServerCodegenDecorator.kt @@ -134,7 +134,7 @@ class PyO3ExtensionModuleDecorator : ServerCodegenDecorator { } } -val DECORATORS = listOf( +val DECORATORS = arrayOf( /** * Add the [InternalServerError] error to all operations. * This is done because the Python interpreter can raise exceptions during execution. diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerEnumGenerator.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerEnumGenerator.kt index 90bdcc6e44..55a4a083a1 100644 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerEnumGenerator.kt +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerEnumGenerator.kt @@ -17,6 +17,7 @@ import software.amazon.smithy.rust.codegen.core.util.dq import software.amazon.smithy.rust.codegen.server.python.smithy.PythonServerCargoDependency import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerEnumGenerator +import software.amazon.smithy.rust.codegen.server.smithy.generators.ValidationExceptionConversionGenerator /** * To share enums defined in Rust with Python, `pyo3` provides the `PyClass` trait. @@ -27,7 +28,8 @@ class PythonServerEnumGenerator( codegenContext: ServerCodegenContext, private val writer: RustWriter, shape: StringShape, -) : ServerEnumGenerator(codegenContext, writer, shape) { + validationExceptionConversionGenerator: ValidationExceptionConversionGenerator, +) : ServerEnumGenerator(codegenContext, writer, shape, validationExceptionConversionGenerator) { private val pyO3 = PythonServerCargoDependency.PyO3.toType() diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/testutil/PythonServerTestHelpers.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/testutil/PythonServerTestHelpers.kt index e38e1ae67c..669279c8d5 100644 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/testutil/PythonServerTestHelpers.kt +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/testutil/PythonServerTestHelpers.kt @@ -13,7 +13,9 @@ import software.amazon.smithy.rust.codegen.core.testutil.generatePluginContext import software.amazon.smithy.rust.codegen.core.util.runCommand import software.amazon.smithy.rust.codegen.server.python.smithy.PythonServerCodegenVisitor import software.amazon.smithy.rust.codegen.server.python.smithy.customizations.DECORATORS +import software.amazon.smithy.rust.codegen.server.smithy.customizations.CustomValidationExceptionWithReasonDecorator import software.amazon.smithy.rust.codegen.server.smithy.customizations.ServerRequiredCustomizations +import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionDecorator import software.amazon.smithy.rust.codegen.server.smithy.customize.CombinedServerCodegenDecorator import java.io.File import java.nio.file.Path @@ -25,10 +27,13 @@ fun generatePythonServerPluginContext(model: Model) = generatePluginContext(model, runtimeConfig = TestRuntimeConfig) fun executePythonServerCodegenVisitor(pluginCtx: PluginContext) { - val codegenDecorator: CombinedServerCodegenDecorator = + val codegenDecorator = CombinedServerCodegenDecorator.fromClasspath( pluginCtx, - CombinedServerCodegenDecorator(DECORATORS + ServerRequiredCustomizations()), + *DECORATORS, + ServerRequiredCustomizations(), + SmithyValidationExceptionDecorator(), + CustomValidationExceptionWithReasonDecorator(), ) PythonServerCodegenVisitor(pluginCtx, codegenDecorator).execute() } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/RustServerCodegenPlugin.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/RustServerCodegenPlugin.kt index 68a538252c..812bc50916 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/RustServerCodegenPlugin.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/RustServerCodegenPlugin.kt @@ -6,7 +6,6 @@ package software.amazon.smithy.rust.codegen.server.smithy import software.amazon.smithy.build.PluginContext -import software.amazon.smithy.build.SmithyBuildPlugin import software.amazon.smithy.codegen.core.ReservedWordSymbolProvider import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.ServiceShape @@ -18,8 +17,12 @@ import software.amazon.smithy.rust.codegen.core.smithy.StreamingShapeMetadataPro import software.amazon.smithy.rust.codegen.core.smithy.StreamingShapeSymbolProvider import software.amazon.smithy.rust.codegen.core.smithy.SymbolVisitor import software.amazon.smithy.rust.codegen.core.smithy.SymbolVisitorConfig +import software.amazon.smithy.rust.codegen.server.smithy.customizations.CustomValidationExceptionWithReasonDecorator import software.amazon.smithy.rust.codegen.server.smithy.customizations.ServerRequiredCustomizations +import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionDecorator import software.amazon.smithy.rust.codegen.server.smithy.customize.CombinedServerCodegenDecorator +import software.amazon.smithy.rust.codegen.server.smithy.customize.ServerCodegenDecorator +import software.amazon.smithy.rust.codegen.server.smithy.testutil.ServerDecoratableBuildPlugin import java.util.logging.Level import java.util.logging.Logger @@ -30,7 +33,7 @@ import java.util.logging.Logger * `resources/META-INF.services/software.amazon.smithy.build.SmithyBuildPlugin` refers to this class by name which * enables the smithy-build plugin to invoke `execute` with all Smithy plugin context + models. */ -class RustServerCodegenPlugin : SmithyBuildPlugin { +class RustServerCodegenPlugin : ServerDecoratableBuildPlugin() { private val logger = Logger.getLogger(javaClass.name) override fun getName(): String = "rust-server-codegen" @@ -38,14 +41,18 @@ class RustServerCodegenPlugin : SmithyBuildPlugin { /** * See [software.amazon.smithy.rust.codegen.client.smithy.RustClientCodegenPlugin]. */ - override fun execute( + override fun executeWithDecorator( context: PluginContext, + vararg decorator: ServerCodegenDecorator, ) { Logger.getLogger(ReservedWordSymbolProvider::class.java.name).level = Level.OFF val codegenDecorator = CombinedServerCodegenDecorator.fromClasspath( context, ServerRequiredCustomizations(), + SmithyValidationExceptionDecorator(), + CustomValidationExceptionWithReasonDecorator(), + *decorator, ) logger.info("Loaded plugin to generate pure Rust bindings for the server SDK") ServerCodegenVisitor(context, codegenDecorator).execute() diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt index e693d1ac5e..def3e4a611 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt @@ -16,6 +16,7 @@ import software.amazon.smithy.model.shapes.IntegerShape import software.amazon.smithy.model.shapes.ListShape import software.amazon.smithy.model.shapes.LongShape import software.amazon.smithy.model.shapes.MapShape +import software.amazon.smithy.model.shapes.NumberShape import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.model.shapes.SetShape @@ -74,6 +75,7 @@ import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerStruct import software.amazon.smithy.rust.codegen.server.smithy.generators.UnconstrainedCollectionGenerator import software.amazon.smithy.rust.codegen.server.smithy.generators.UnconstrainedMapGenerator import software.amazon.smithy.rust.codegen.server.smithy.generators.UnconstrainedUnionGenerator +import software.amazon.smithy.rust.codegen.server.smithy.generators.ValidationExceptionConversionGenerator import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocol import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocolGenerator import software.amazon.smithy.rust.codegen.server.smithy.protocols.ServerProtocolLoader @@ -101,6 +103,7 @@ open class ServerCodegenVisitor( protected var codegenContext: ServerCodegenContext protected var protocolGeneratorFactory: ProtocolGeneratorFactory protected var protocolGenerator: ServerProtocolGenerator + protected var validationExceptionConversionGenerator: ValidationExceptionConversionGenerator init { val symbolVisitorConfig = @@ -144,6 +147,9 @@ open class ServerCodegenVisitor( serverSymbolProviders.pubCrateConstrainedShapeSymbolProvider, ) + // We can use a not-null assertion because [CombinedServerCodegenDecorator] returns a not null value. + validationExceptionConversionGenerator = codegenDecorator.validationExceptionConversion(codegenContext)!! + rustCrate = RustCrate(context.fileManifest, codegenContext.symbolProvider, settings.codegenConfig) protocolGenerator = protocolGeneratorFactory.buildProtocolGenerator(codegenContext) } @@ -195,10 +201,12 @@ open class ServerCodegenVisitor( "[rust-server-codegen] Generating Rust server for service $service, protocol ${codegenContext.protocol}", ) + val validationExceptionShapeId = validationExceptionConversionGenerator.shapeId for (validationResult in listOf( validateOperationsWithConstrainedInputHaveValidationExceptionAttached( model, service, + validationExceptionShapeId, ), validateUnsupportedConstraints(model, service, codegenContext.settings.codegenConfig), )) { @@ -260,7 +268,7 @@ open class ServerCodegenVisitor( writer: RustWriter, ) { if (codegenContext.settings.codegenConfig.publicConstrainedTypes || shape.isReachableFromOperationInput()) { - val serverBuilderGenerator = ServerBuilderGenerator(codegenContext, shape) + val serverBuilderGenerator = ServerBuilderGenerator(codegenContext, shape, validationExceptionConversionGenerator) serverBuilderGenerator.render(writer) if (codegenContext.settings.codegenConfig.publicConstrainedTypes) { @@ -281,7 +289,7 @@ open class ServerCodegenVisitor( if (!codegenContext.settings.codegenConfig.publicConstrainedTypes) { val serverBuilderGeneratorWithoutPublicConstrainedTypes = - ServerBuilderGeneratorWithoutPublicConstrainedTypes(codegenContext, shape) + ServerBuilderGeneratorWithoutPublicConstrainedTypes(codegenContext, shape, validationExceptionConversionGenerator) serverBuilderGeneratorWithoutPublicConstrainedTypes.render(writer) writer.implBlock(shape, codegenContext.symbolProvider) { @@ -334,7 +342,13 @@ open class ServerCodegenVisitor( if (isDirectlyConstrained || renderUnconstrainedList) { rustCrate.withModule(ModelsModule) { - CollectionConstraintViolationGenerator(codegenContext, this, shape, constraintsInfo).render() + CollectionConstraintViolationGenerator( + codegenContext, + this, + shape, + constraintsInfo, + validationExceptionConversionGenerator, + ).render() } } } @@ -374,7 +388,12 @@ open class ServerCodegenVisitor( if (isDirectlyConstrained || renderUnconstrainedMap) { rustCrate.withModule(ModelsModule) { - MapConstraintViolationGenerator(codegenContext, this, shape).render() + MapConstraintViolationGenerator( + codegenContext, + this, + shape, + validationExceptionConversionGenerator, + ).render() } } } @@ -386,42 +405,19 @@ open class ServerCodegenVisitor( */ override fun stringShape(shape: StringShape) { fun serverEnumGeneratorFactory(codegenContext: ServerCodegenContext, writer: RustWriter, shape: StringShape) = - ServerEnumGenerator(codegenContext, writer, shape) + ServerEnumGenerator(codegenContext, writer, shape, validationExceptionConversionGenerator) stringShape(shape, ::serverEnumGeneratorFactory) } - override fun integerShape(shape: IntegerShape) { - if (shape.isDirectlyConstrained(codegenContext.symbolProvider)) { - logger.info("[rust-server-codegen] Generating a constrained integer $shape") - rustCrate.withModule(ModelsModule) { - ConstrainedNumberGenerator(codegenContext, this, shape).render() - } - } - } - - override fun shortShape(shape: ShortShape) { - if (shape.isDirectlyConstrained(codegenContext.symbolProvider)) { - logger.info("[rust-server-codegen] Generating a constrained short $shape") - rustCrate.withModule(ModelsModule) { - ConstrainedNumberGenerator(codegenContext, this, shape).render() - } - } - } - - override fun longShape(shape: LongShape) { - if (shape.isDirectlyConstrained(codegenContext.symbolProvider)) { - logger.info("[rust-server-codegen] Generating a constrained long $shape") - rustCrate.withModule(ModelsModule) { - ConstrainedNumberGenerator(codegenContext, this, shape).render() - } - } - } - - override fun byteShape(shape: ByteShape) { + override fun integerShape(shape: IntegerShape) = integralShape(shape) + override fun shortShape(shape: ShortShape) = integralShape(shape) + override fun longShape(shape: LongShape) = integralShape(shape) + override fun byteShape(shape: ByteShape) = integralShape(shape) + private fun integralShape(shape: NumberShape) { if (shape.isDirectlyConstrained(codegenContext.symbolProvider)) { - logger.info("[rust-server-codegen] Generating a constrained byte $shape") + logger.info("[rust-server-codegen] Generating a constrained integral $shape") rustCrate.withModule(ModelsModule) { - ConstrainedNumberGenerator(codegenContext, this, shape).render() + ConstrainedNumberGenerator(codegenContext, this, shape, validationExceptionConversionGenerator).render() } } } @@ -450,7 +446,7 @@ open class ServerCodegenVisitor( } else if (!shape.hasTrait() && shape.isDirectlyConstrained(codegenContext.symbolProvider)) { logger.info("[rust-server-codegen] Generating a constrained string $shape") rustCrate.withModule(ModelsModule) { - ConstrainedStringGenerator(codegenContext, this, shape).render() + ConstrainedStringGenerator(codegenContext, this, shape, validationExceptionConversionGenerator).render() } } } @@ -544,7 +540,7 @@ open class ServerCodegenVisitor( if (shape.isDirectlyConstrained(codegenContext.symbolProvider)) { rustCrate.withModule(ModelsModule) { - ConstrainedBlobGenerator(codegenContext, this, shape).render() + ConstrainedBlobGenerator(codegenContext, this, shape, validationExceptionConversionGenerator).render() } } } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerRustSettings.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerRustSettings.kt index 19f60aac93..67fbea91cd 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerRustSettings.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerRustSettings.kt @@ -83,12 +83,20 @@ data class ServerCodegenConfig( override val debugMode: Boolean = defaultDebugMode, val publicConstrainedTypes: Boolean = defaultPublicConstrainedTypes, val ignoreUnsupportedConstraints: Boolean = defaultIgnoreUnsupportedConstraints, + /** + * A flag to enable _experimental_ support for custom validation exceptions via the + * [CustomValidationExceptionWithReasonDecorator] decorator. + * TODO(https://github.com/awslabs/smithy-rs/pull/2053): this will go away once we implement the RFC, when users will be + * able to define the converters in their Rust application code. + */ + val experimentalCustomValidationExceptionWithReasonPleaseDoNotUse: String? = defaultExperimentalCustomValidationExceptionWithReasonPleaseDoNotUse, ) : CoreCodegenConfig( formatTimeoutSeconds, debugMode, ) { companion object { private const val defaultPublicConstrainedTypes = true private const val defaultIgnoreUnsupportedConstraints = false + private val defaultExperimentalCustomValidationExceptionWithReasonPleaseDoNotUse = null fun fromCodegenConfigAndNode(coreCodegenConfig: CoreCodegenConfig, node: Optional) = if (node.isPresent) { @@ -97,6 +105,7 @@ data class ServerCodegenConfig( debugMode = coreCodegenConfig.debugMode, publicConstrainedTypes = node.get().getBooleanMemberOrDefault("publicConstrainedTypes", defaultPublicConstrainedTypes), ignoreUnsupportedConstraints = node.get().getBooleanMemberOrDefault("ignoreUnsupportedConstraints", defaultIgnoreUnsupportedConstraints), + experimentalCustomValidationExceptionWithReasonPleaseDoNotUse = node.get().getStringMemberOrDefault("experimentalCustomValidationExceptionWithReasonPleaseDoNotUse", defaultExperimentalCustomValidationExceptionWithReasonPleaseDoNotUse), ) } else { ServerCodegenConfig( diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraints.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraints.kt index 4ebe3299b4..fd3f259bc7 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraints.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraints.kt @@ -140,6 +140,7 @@ private val unsupportedConstraintsOnMemberShapes = allConstraintTraits - Require fun validateOperationsWithConstrainedInputHaveValidationExceptionAttached( model: Model, service: ServiceShape, + validationExceptionShapeId: ShapeId, ): ValidationResult { // Traverse the model and error out if an operation uses constrained input, but it does not have // `ValidationException` attached in `errors`. https://github.com/awslabs/smithy-rs/pull/1199#discussion_r809424783 @@ -154,7 +155,7 @@ fun validateOperationsWithConstrainedInputHaveValidationExceptionAttached( walker.walkShapes(operationShape.inputShape(model)) .any { it is SetShape || it is EnumShape || it.hasConstraintTrait() } } - .filter { !it.errors.contains(ShapeId.from("smithy.framework#ValidationException")) } + .filter { !it.errors.contains(validationExceptionShapeId) } .map { OperationWithConstrainedInputWithoutValidationException(it) } .toSet() @@ -170,11 +171,11 @@ fun validateOperationsWithConstrainedInputHaveValidationExceptionAttached( """ ```smithy - use smithy.framework#ValidationException + use $validationExceptionShapeId operation ${it.shape.id.name} { ... - errors: [..., ValidationException] // <-- Add this. + errors: [..., ${validationExceptionShapeId.name}] // <-- Add this. } ``` """.trimIndent(), diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/CustomValidationExceptionWithReasonDecorator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/CustomValidationExceptionWithReasonDecorator.kt new file mode 100644 index 0000000000..92de1fca48 --- /dev/null +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/CustomValidationExceptionWithReasonDecorator.kt @@ -0,0 +1,314 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.server.smithy.customizations + +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.MapShape +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.model.shapes.StringShape +import software.amazon.smithy.model.traits.EnumTrait +import software.amazon.smithy.model.traits.LengthTrait +import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.join +import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock +import software.amazon.smithy.rust.codegen.core.rustlang.rustBlockTemplate +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider +import software.amazon.smithy.rust.codegen.core.util.getTrait +import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext +import software.amazon.smithy.rust.codegen.server.smithy.ServerRuntimeType +import software.amazon.smithy.rust.codegen.server.smithy.customize.ServerCodegenDecorator +import software.amazon.smithy.rust.codegen.server.smithy.generators.BlobLength +import software.amazon.smithy.rust.codegen.server.smithy.generators.CollectionTraitInfo +import software.amazon.smithy.rust.codegen.server.smithy.generators.ConstraintViolation +import software.amazon.smithy.rust.codegen.server.smithy.generators.Length +import software.amazon.smithy.rust.codegen.server.smithy.generators.Pattern +import software.amazon.smithy.rust.codegen.server.smithy.generators.Range +import software.amazon.smithy.rust.codegen.server.smithy.generators.StringTraitInfo +import software.amazon.smithy.rust.codegen.server.smithy.generators.ValidationExceptionConversionGenerator +import software.amazon.smithy.rust.codegen.server.smithy.generators.isKeyConstrained +import software.amazon.smithy.rust.codegen.server.smithy.generators.isValueConstrained +import software.amazon.smithy.rust.codegen.server.smithy.validationErrorMessage + +/** + * A decorator that adds code to convert from constraint violations to a custom `ValidationException` shape that is very + * similar to `smithy.framework#ValidationException`, with an additional `reason` field. + * + * The shape definition is in [CustomValidationExceptionWithReasonDecoratorTest]. + * + * This is just an example to showcase experimental support for custom validation exceptions. + * TODO(https://github.com/awslabs/smithy-rs/pull/2053): this will go away once we implement the RFC, when users will be + * able to define the converters in their Rust application code. + */ +class CustomValidationExceptionWithReasonDecorator : ServerCodegenDecorator { + override val name: String + get() = "CustomValidationExceptionWithReasonDecorator" + override val order: Byte + get() = -69 + + override fun validationExceptionConversion(codegenContext: ServerCodegenContext): + ValidationExceptionConversionGenerator? = + if (codegenContext.settings.codegenConfig.experimentalCustomValidationExceptionWithReasonPleaseDoNotUse != null) { + ValidationExceptionWithReasonConversionGenerator(codegenContext) + } else { + null + } +} + +class ValidationExceptionWithReasonConversionGenerator(private val codegenContext: ServerCodegenContext) : + ValidationExceptionConversionGenerator { + override val shapeId: ShapeId = + ShapeId.from(codegenContext.settings.codegenConfig.experimentalCustomValidationExceptionWithReasonPleaseDoNotUse) + + override fun renderImplFromConstraintViolationForRequestRejection(): Writable = writable { + val codegenScope = arrayOf( + "RequestRejection" to ServerRuntimeType.requestRejection(codegenContext.runtimeConfig), + "From" to RuntimeType.From, + ) + rustTemplate( + """ + impl #{From} for #{RequestRejection} { + fn from(constraint_violation: ConstraintViolation) -> Self { + let first_validation_exception_field = constraint_violation.as_validation_exception_field("".to_owned()); + let validation_exception = crate::error::ValidationException { + message: format!("1 validation error detected. {}", &first_validation_exception_field.message), + reason: crate::model::ValidationExceptionReason::FieldValidationFailed, + fields: Some(vec![first_validation_exception_field]), + }; + Self::ConstraintViolation( + crate::operation_ser::serialize_structure_crate_error_validation_exception(&validation_exception) + .expect("validation exceptions should never fail to serialize; please file a bug report under https://github.com/awslabs/smithy-rs/issues") + ) + } + } + """, + *codegenScope, + ) + } + + override fun stringShapeConstraintViolationImplBlock(stringConstraintsInfo: Collection): Writable = writable { + val validationExceptionFields = + stringConstraintsInfo.map { + writable { + when (it) { + is Pattern -> { + rustTemplate( + """ + Self::Pattern(string) => crate::model::ValidationExceptionField { + message: #{MessageWritable:W}, + name: path, + reason: crate::model::ValidationExceptionFieldReason::PatternNotValid, + }, + """, + "MessageWritable" to it.errorMessage(), + ) + } + is Length -> { + rust( + """ + Self::Length(length) => crate::model::ValidationExceptionField { + message: format!("${it.lengthTrait.validationErrorMessage()}", length, &path), + name: path, + reason: crate::model::ValidationExceptionFieldReason::LengthNotValid, + }, + """, + ) + } + } + } + }.join("\n") + + rustTemplate( + """ + pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField { + match self { + #{ValidationExceptionFields:W} + } + } + """, + "String" to RuntimeType.String, + "ValidationExceptionFields" to validationExceptionFields, + ) + } + + override fun enumShapeConstraintViolationImplBlock(enumTrait: EnumTrait) = writable { + val enumValueSet = enumTrait.enumDefinitionValues.joinToString(", ") + val message = "Value {} at '{}' failed to satisfy constraint: Member must satisfy enum value set: [$enumValueSet]" + rustTemplate( + """ + pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField { + crate::model::ValidationExceptionField { + message: format!(r##"$message"##, &self.0, &path), + name: path, + reason: crate::model::ValidationExceptionFieldReason::ValueNotValid, + } + } + """, + "String" to RuntimeType.String, + ) + } + + override fun numberShapeConstraintViolationImplBlock(rangeInfo: Range) = writable { + rustTemplate( + """ + pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField { + match self { + Self::Range(value) => crate::model::ValidationExceptionField { + message: format!("${rangeInfo.rangeTrait.validationErrorMessage()}", value, &path), + name: path, + reason: crate::model::ValidationExceptionFieldReason::ValueNotValid, + } + } + } + """, + "String" to RuntimeType.String, + ) + } + + override fun blobShapeConstraintViolationImplBlock(blobConstraintsInfo: Collection) = writable { + val validationExceptionFields = + blobConstraintsInfo.map { + writable { + rust( + """ + Self::Length(length) => crate::model::ValidationExceptionField { + message: format!("${it.lengthTrait.validationErrorMessage()}", length, &path), + name: path, + reason: crate::model::ValidationExceptionFieldReason::LengthNotValid, + }, + """, + ) + } + }.join("\n") + + rustTemplate( + """ + pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField { + match self { + #{ValidationExceptionFields:W} + } + } + """, + "String" to RuntimeType.String, + "ValidationExceptionFields" to validationExceptionFields, + ) + } + + override fun mapShapeConstraintViolationImplBlock( + shape: MapShape, + keyShape: StringShape, + valueShape: Shape, + symbolProvider: RustSymbolProvider, + model: Model, + ) = writable { + rustBlockTemplate( + "pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField", + "String" to RuntimeType.String, + ) { + rustBlock("match self") { + shape.getTrait()?.also { + rust( + """ + Self::Length(length) => crate::model::ValidationExceptionField { + message: format!("${it.validationErrorMessage()}", length, &path), + name: path, + reason: crate::model::ValidationExceptionFieldReason::LengthNotValid, + }, + """, + ) + } + if (isKeyConstrained(keyShape, symbolProvider)) { + rust("""Self::Key(key_constraint_violation) => key_constraint_violation.as_validation_exception_field(path),""") + } + if (isValueConstrained(valueShape, model, symbolProvider)) { + rust("""Self::Value(key, value_constraint_violation) => value_constraint_violation.as_validation_exception_field(path + "/" + key.as_str()),""") + } + } + } + } + + override fun builderConstraintViolationImplBlock(constraintViolations: Collection) = writable { + rustBlock("match self") { + constraintViolations.forEach { + if (it.hasInner()) { + rust("""ConstraintViolation::${it.name()}(inner) => inner.as_validation_exception_field(path + "/${it.forMember.memberName}"),""") + } else { + rust( + """ + ConstraintViolation::${it.name()} => crate::model::ValidationExceptionField { + message: format!("Value null at '{}/${it.forMember.memberName}' failed to satisfy constraint: Member must not be null", path), + name: path + "/${it.forMember.memberName}", + reason: crate::model::ValidationExceptionFieldReason::Other, + }, + """, + ) + } + } + } + } + + override fun collectionShapeConstraintViolationImplBlock( + collectionConstraintsInfo: + Collection, + isMemberConstrained: Boolean, + ) = writable { + val validationExceptionFields = collectionConstraintsInfo.map { + writable { + when (it) { + is CollectionTraitInfo.Length -> { + rust( + """ + Self::Length(length) => crate::model::ValidationExceptionField { + message: format!("${it.lengthTrait.validationErrorMessage()}", length, &path), + name: path, + reason: crate::model::ValidationExceptionFieldReason::LengthNotValid, + }, + """, + ) + } + is CollectionTraitInfo.UniqueItems -> { + rust( + """ + Self::UniqueItems { duplicate_indices, .. } => + crate::model::ValidationExceptionField { + message: format!("${it.uniqueItemsTrait.validationErrorMessage()}", &duplicate_indices, &path), + name: path, + reason: crate::model::ValidationExceptionFieldReason::ValueNotValid, + }, + """, + ) + } + } + } + }.toMutableList() + + if (isMemberConstrained) { + validationExceptionFields += { + rust( + """ + Self::Member(index, member_constraint_violation) => + member_constraint_violation.as_validation_exception_field(path + "/" + &index.to_string()) + """, + ) + } + } + rustTemplate( + """ + pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField { + match self { + #{AsValidationExceptionFields:W} + } + } + """, + "String" to RuntimeType.String, + "AsValidationExceptionFields" to validationExceptionFields.join("\n"), + ) + } +} diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/SmithyValidationExceptionDecorator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/SmithyValidationExceptionDecorator.kt new file mode 100644 index 0000000000..eb91206b5e --- /dev/null +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/SmithyValidationExceptionDecorator.kt @@ -0,0 +1,239 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.server.smithy.customizations + +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.MapShape +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.model.shapes.StringShape +import software.amazon.smithy.model.traits.EnumTrait +import software.amazon.smithy.model.traits.LengthTrait +import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.join +import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock +import software.amazon.smithy.rust.codegen.core.rustlang.rustBlockTemplate +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider +import software.amazon.smithy.rust.codegen.core.util.getTrait +import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext +import software.amazon.smithy.rust.codegen.server.smithy.ServerRuntimeType +import software.amazon.smithy.rust.codegen.server.smithy.customize.ServerCodegenDecorator +import software.amazon.smithy.rust.codegen.server.smithy.generators.BlobLength +import software.amazon.smithy.rust.codegen.server.smithy.generators.CollectionTraitInfo +import software.amazon.smithy.rust.codegen.server.smithy.generators.ConstraintViolation +import software.amazon.smithy.rust.codegen.server.smithy.generators.Range +import software.amazon.smithy.rust.codegen.server.smithy.generators.StringTraitInfo +import software.amazon.smithy.rust.codegen.server.smithy.generators.TraitInfo +import software.amazon.smithy.rust.codegen.server.smithy.generators.ValidationExceptionConversionGenerator +import software.amazon.smithy.rust.codegen.server.smithy.generators.isKeyConstrained +import software.amazon.smithy.rust.codegen.server.smithy.generators.isValueConstrained +import software.amazon.smithy.rust.codegen.server.smithy.validationErrorMessage + +/** + * A decorator that adds code to convert from constraint violations to Smithy's `smithy.framework#ValidationException`, + * defined in [0]. This is Smithy's recommended shape to return when validation fails. + * + * This decorator is always enabled when using the `rust-server-codegen` plugin. + * + * [0]: https://github.com/awslabs/smithy/tree/main/smithy-validation-model + * + * TODO(https://github.com/awslabs/smithy-rs/pull/2053): once the RFC is implemented, consider moving this back into the + * generators. + */ +class SmithyValidationExceptionDecorator : ServerCodegenDecorator { + override val name: String + get() = "SmithyValidationExceptionDecorator" + override val order: Byte + get() = 69 + + override fun validationExceptionConversion(codegenContext: ServerCodegenContext): ValidationExceptionConversionGenerator = + SmithyValidationExceptionConversionGenerator(codegenContext) +} + +class SmithyValidationExceptionConversionGenerator(private val codegenContext: ServerCodegenContext) : + ValidationExceptionConversionGenerator { + + // Define a companion object so that we can refer to this shape id globally. + companion object { + val SHAPE_ID: ShapeId = ShapeId.from("smithy.framework#ValidationException") + } + override val shapeId: ShapeId = SHAPE_ID + + override fun renderImplFromConstraintViolationForRequestRejection(): Writable = writable { + val codegenScope = arrayOf( + "RequestRejection" to ServerRuntimeType.requestRejection(codegenContext.runtimeConfig), + "From" to RuntimeType.From, + ) + rustTemplate( + """ + impl #{From} for #{RequestRejection} { + fn from(constraint_violation: ConstraintViolation) -> Self { + let first_validation_exception_field = constraint_violation.as_validation_exception_field("".to_owned()); + let validation_exception = crate::error::ValidationException { + message: format!("1 validation error detected. {}", &first_validation_exception_field.message), + field_list: Some(vec![first_validation_exception_field]), + }; + Self::ConstraintViolation( + crate::operation_ser::serialize_structure_crate_error_validation_exception(&validation_exception) + .expect("validation exceptions should never fail to serialize; please file a bug report under https://github.com/awslabs/smithy-rs/issues") + ) + } + } + """, + *codegenScope, + ) + } + + override fun stringShapeConstraintViolationImplBlock(stringConstraintsInfo: Collection): Writable = writable { + val constraintsInfo: List = stringConstraintsInfo.map(StringTraitInfo::toTraitInfo) + + rustTemplate( + """ + pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField { + match self { + #{ValidationExceptionFields:W} + } + } + """, + "String" to RuntimeType.String, + "ValidationExceptionFields" to constraintsInfo.map { it.asValidationExceptionField }.join("\n"), + ) + } + + override fun blobShapeConstraintViolationImplBlock(blobConstraintsInfo: Collection): Writable = writable { + val constraintsInfo: List = blobConstraintsInfo.map(BlobLength::toTraitInfo) + + rustTemplate( + """ + pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField { + match self { + #{ValidationExceptionFields:W} + } + } + """, + "String" to RuntimeType.String, + "ValidationExceptionFields" to constraintsInfo.map { it.asValidationExceptionField }.join("\n"), + ) + } + + override fun mapShapeConstraintViolationImplBlock( + shape: MapShape, + keyShape: StringShape, + valueShape: Shape, + symbolProvider: RustSymbolProvider, + model: Model, + ) = writable { + rustBlockTemplate( + "pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField", + "String" to RuntimeType.String, + ) { + rustBlock("match self") { + shape.getTrait()?.also { + rust( + """ + Self::Length(length) => crate::model::ValidationExceptionField { + message: format!("${it.validationErrorMessage()}", length, &path), + path, + }, + """, + ) + } + if (isKeyConstrained(keyShape, symbolProvider)) { + // Note how we _do not_ append the key's member name to the path. This is intentional, as + // per the `RestJsonMalformedLengthMapKey` test. Note keys are always strings. + // https://github.com/awslabs/smithy/blob/ee0b4ff90daaaa5101f32da936c25af8c91cc6e9/smithy-aws-protocol-tests/model/restJson1/validation/malformed-length.smithy#L296-L295 + rust("""Self::Key(key_constraint_violation) => key_constraint_violation.as_validation_exception_field(path),""") + } + if (isValueConstrained(valueShape, model, symbolProvider)) { + // `as_str()` works with regular `String`s and constrained string shapes. + rust("""Self::Value(key, value_constraint_violation) => value_constraint_violation.as_validation_exception_field(path + "/" + key.as_str()),""") + } + } + } + } + + override fun enumShapeConstraintViolationImplBlock(enumTrait: EnumTrait) = writable { + val enumValueSet = enumTrait.enumDefinitionValues.joinToString(", ") + val message = "Value {} at '{}' failed to satisfy constraint: Member must satisfy enum value set: [$enumValueSet]" + rustTemplate( + """ + pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField { + crate::model::ValidationExceptionField { + message: format!(r##"$message"##, &self.0, &path), + path, + } + } + """, + "String" to RuntimeType.String, + ) + } + + override fun numberShapeConstraintViolationImplBlock(rangeInfo: Range) = writable { + rustTemplate( + """ + pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField { + match self { + #{ValidationExceptionFields:W} + } + } + """, + "String" to RuntimeType.String, + "ValidationExceptionFields" to rangeInfo.toTraitInfo().asValidationExceptionField, + ) + } + + override fun builderConstraintViolationImplBlock(constraintViolations: Collection) = writable { + rustBlock("match self") { + constraintViolations.forEach { + if (it.hasInner()) { + rust("""ConstraintViolation::${it.name()}(inner) => inner.as_validation_exception_field(path + "/${it.forMember.memberName}"),""") + } else { + rust( + """ + ConstraintViolation::${it.name()} => crate::model::ValidationExceptionField { + message: format!("Value null at '{}/${it.forMember.memberName}' failed to satisfy constraint: Member must not be null", path), + path: path + "/${it.forMember.memberName}", + }, + """, + ) + } + } + } + } + + override fun collectionShapeConstraintViolationImplBlock( + collectionConstraintsInfo: + Collection, + isMemberConstrained: Boolean, + ) = writable { + val validationExceptionFields = collectionConstraintsInfo.map { it.toTraitInfo().asValidationExceptionField }.toMutableList() + if (isMemberConstrained) { + validationExceptionFields += { + rust( + """ + Self::Member(index, member_constraint_violation) => + member_constraint_violation.as_validation_exception_field(path + "/" + &index.to_string()) + """, + ) + } + } + rustTemplate( + """ + pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField { + match self { + #{AsValidationExceptionFields:W} + } + } + """, + "String" to RuntimeType.String, + "AsValidationExceptionFields" to validationExceptionFields.join("\n"), + ) + } +} diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customize/ServerCodegenDecorator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customize/ServerCodegenDecorator.kt index e6bc78f451..156c00421e 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customize/ServerCodegenDecorator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customize/ServerCodegenDecorator.kt @@ -11,6 +11,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.customize.CombinedCoreCod import software.amazon.smithy.rust.codegen.core.smithy.customize.CoreCodegenDecorator import software.amazon.smithy.rust.codegen.core.smithy.protocols.ProtocolMap import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext +import software.amazon.smithy.rust.codegen.server.smithy.generators.ValidationExceptionConversionGenerator import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocolGenerator import java.util.logging.Logger @@ -21,6 +22,7 @@ typealias ServerProtocolMap = ProtocolMap { fun protocols(serviceId: ShapeId, currentProtocols: ServerProtocolMap): ServerProtocolMap = currentProtocols + fun validationExceptionConversion(codegenContext: ServerCodegenContext): ValidationExceptionConversionGenerator? = null } /** @@ -41,6 +43,11 @@ class CombinedServerCodegenDecorator(private val decorators: List, + private val collectionConstraintsInfo: List, + private val validationExceptionConversionGenerator: ValidationExceptionConversionGenerator, ) { private val model = codegenContext.model private val symbolProvider = codegenContext.symbolProvider @@ -35,6 +35,7 @@ class CollectionConstraintViolationGenerator( PubCrateConstraintViolationSymbolProvider(this) } } + private val constraintsInfo: List = collectionConstraintsInfo.map { it.toTraitInfo() } fun render() { val memberShape = model.expectShape(shape.member.target) @@ -75,30 +76,13 @@ class CollectionConstraintViolationGenerator( ) if (shape.isReachableFromOperationInput()) { - val validationExceptionFields = constraintsInfo.map { it.asValidationExceptionField }.toMutableList() - if (isMemberConstrained) { - validationExceptionFields += { - rust( - """ - Self::Member(index, member_constraint_violation) => - member_constraint_violation.as_validation_exception_field(path + "/" + &index.to_string()) - """, - ) - } - } - rustTemplate( """ impl $constraintViolationName { - pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField { - match self { - #{AsValidationExceptionFields:W} - } - } + #{CollectionShapeConstraintViolationImplBlock} } """, - "String" to RuntimeType.String, - "AsValidationExceptionFields" to validationExceptionFields.join("\n"), + "CollectionShapeConstraintViolationImplBlock" to validationExceptionConversionGenerator.collectionShapeConstraintViolationImplBlock(collectionConstraintsInfo, isMemberConstrained), ) } } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedBlobGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedBlobGenerator.kt index 41fec1cec7..1215691787 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedBlobGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedBlobGenerator.kt @@ -33,6 +33,7 @@ class ConstrainedBlobGenerator( val codegenContext: ServerCodegenContext, val writer: RustWriter, val shape: BlobShape, + private val validationExceptionConversionGenerator: ValidationExceptionConversionGenerator, ) { val model = codegenContext.model val constrainedShapeSymbolProvider = codegenContext.constrainedShapeSymbolProvider @@ -45,9 +46,10 @@ class ConstrainedBlobGenerator( PubCrateConstraintViolationSymbolProvider(this) } } - private val constraintsInfo: List = listOf(LengthTrait::class.java) + private val blobConstraintsInfo: List = listOf(LengthTrait::class.java) .mapNotNull { shape.getTrait(it).orNull() } - .map { BlobLength(it).toTraitInfo() } + .map { BlobLength(it) } + private val constraintsInfo: List = blobConstraintsInfo.map { it.toTraitInfo() } fun render() { val symbol = constrainedShapeSymbolProvider.toSymbol(shape) @@ -128,21 +130,16 @@ class ConstrainedBlobGenerator( writer.rustTemplate( """ impl ${constraintViolation.name} { - pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField { - match self { - #{ValidationExceptionFields:W} - } - } + #{BlobShapeConstraintViolationImplBlock} } """, - "String" to RuntimeType.String, - "ValidationExceptionFields" to constraintsInfo.map { it.asValidationExceptionField }.join("\n"), + "BlobShapeConstraintViolationImplBlock" to validationExceptionConversionGenerator.blobShapeConstraintViolationImplBlock(blobConstraintsInfo), ) } } } -private data class BlobLength(val lengthTrait: LengthTrait) { +data class BlobLength(val lengthTrait: LengthTrait) { fun toTraitInfo(): TraitInfo = TraitInfo( { rust("Self::check_length(&value)?;") }, { diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedCollectionGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedCollectionGenerator.kt index 4463220a96..3ae5a78a08 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedCollectionGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedCollectionGenerator.kt @@ -48,7 +48,7 @@ class ConstrainedCollectionGenerator( val codegenContext: ServerCodegenContext, val writer: RustWriter, val shape: CollectionShape, - private val constraintsInfo: List, + collectionConstraintsInfo: List, private val unconstrainedSymbol: Symbol? = null, ) { private val model = codegenContext.model @@ -63,6 +63,7 @@ class ConstrainedCollectionGenerator( } } private val symbolProvider = codegenContext.symbolProvider + private val constraintsInfo = collectionConstraintsInfo.map { it.toTraitInfo() } fun render() { check(constraintsInfo.isNotEmpty()) { @@ -178,7 +179,7 @@ class ConstrainedCollectionGenerator( } } -internal sealed class CollectionTraitInfo { +sealed class CollectionTraitInfo { data class UniqueItems(val uniqueItemsTrait: UniqueItemsTrait, val memberSymbol: Symbol) : CollectionTraitInfo() { override fun toTraitInfo(): TraitInfo = TraitInfo( @@ -365,11 +366,10 @@ internal sealed class CollectionTraitInfo { } } - fun fromShape(shape: CollectionShape, symbolProvider: SymbolProvider): List = + fun fromShape(shape: CollectionShape, symbolProvider: SymbolProvider): List = supportedCollectionConstraintTraits .mapNotNull { shape.getTrait(it).orNull() } .map { trait -> fromTrait(trait, shape, symbolProvider) } - .map(CollectionTraitInfo::toTraitInfo) } abstract fun toTraitInfo(): TraitInfo diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedNumberGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedNumberGenerator.kt index 281f0005c1..6680d154e9 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedNumberGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedNumberGenerator.kt @@ -21,8 +21,6 @@ import software.amazon.smithy.rust.codegen.core.rustlang.docs import software.amazon.smithy.rust.codegen.core.rustlang.documentShape import software.amazon.smithy.rust.codegen.core.rustlang.render import software.amazon.smithy.rust.codegen.core.rustlang.rust -import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock -import software.amazon.smithy.rust.codegen.core.rustlang.rustBlockTemplate import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.expectRustMetadata @@ -45,6 +43,7 @@ class ConstrainedNumberGenerator( val codegenContext: ServerCodegenContext, val writer: RustWriter, val shape: NumberShape, + private val validationExceptionConversionGenerator: ValidationExceptionConversionGenerator, ) { val model = codegenContext.model val constrainedShapeSymbolProvider = codegenContext.constrainedShapeSymbolProvider @@ -74,7 +73,8 @@ class ConstrainedNumberGenerator( val name = symbol.name val unconstrainedTypeName = unconstrainedType.render() val constraintViolation = constraintViolationSymbolProvider.toSymbol(shape) - val constraintsInfo = listOf(Range(rangeTrait).toTraitInfo(unconstrainedTypeName)) + val rangeInfo = Range(rangeTrait) + val constraintsInfo = listOf(rangeInfo.toTraitInfo()) writer.documentShape(shape, model) writer.docs(rustDocsConstrainedTypeEpilogue(name)) @@ -143,35 +143,23 @@ class ConstrainedNumberGenerator( ) if (shape.isReachableFromOperationInput()) { - rustBlock("impl ${constraintViolation.name}") { - rustBlockTemplate( - "pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField", - "String" to RuntimeType.String, - ) { - rustBlock("match self") { - rust( - """ - Self::Range(value) => crate::model::ValidationExceptionField { - message: format!("${rangeTrait.validationErrorMessage()}", value, &path), - path, - }, - """, - ) - } + rustTemplate( + """ + impl ${constraintViolation.name} { + #{NumberShapeConstraintViolationImplBlock} } - } + """, + "NumberShapeConstraintViolationImplBlock" to validationExceptionConversionGenerator.numberShapeConstraintViolationImplBlock(rangeInfo), + ) } } } } -private data class Range(val rangeTrait: RangeTrait) { - fun toTraitInfo(unconstrainedTypeName: String): TraitInfo = TraitInfo( +data class Range(val rangeTrait: RangeTrait) { + fun toTraitInfo(): TraitInfo = TraitInfo( { rust("Self::check_range(value)?;") }, - { - docs("Error when a number doesn't satisfy its `@range` requirements.") - rust("Range($unconstrainedTypeName)") - }, + { docs("Error when a number doesn't satisfy its `@range` requirements.") }, { rust( """ diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedStringGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedStringGenerator.kt index 7c8a090ff4..73a2ac7a0d 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedStringGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedStringGenerator.kt @@ -50,6 +50,7 @@ class ConstrainedStringGenerator( val codegenContext: ServerCodegenContext, val writer: RustWriter, val shape: StringShape, + private val validationExceptionConversionGenerator: ValidationExceptionConversionGenerator, ) { val model = codegenContext.model val constrainedShapeSymbolProvider = codegenContext.constrainedShapeSymbolProvider @@ -63,10 +64,12 @@ class ConstrainedStringGenerator( } } private val symbol = constrainedShapeSymbolProvider.toSymbol(shape) - private val constraintsInfo: List = + private val stringConstraintsInfo: List = supportedStringConstraintTraits .mapNotNull { shape.getTrait(it).orNull() } .map { StringTraitInfo.fromTrait(symbol, it, isSensitive = shape.hasTrait()) } + private val constraintsInfo: List = + stringConstraintsInfo .map(StringTraitInfo::toTraitInfo) fun render() { @@ -158,15 +161,10 @@ class ConstrainedStringGenerator( writer.rustTemplate( """ impl ${constraintViolation.name} { - pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField { - match self { - #{ValidationExceptionFields:W} - } - } + #{StringShapeConstraintViolationImplBlock:W} } """, - "String" to RuntimeType.String, - "ValidationExceptionFields" to constraintsInfo.map { it.asValidationExceptionField }.join("\n"), + "StringShapeConstraintViolationImplBlock" to validationExceptionConversionGenerator.stringShapeConstraintViolationImplBlock(stringConstraintsInfo), ) } } @@ -187,8 +185,7 @@ class ConstrainedStringGenerator( } } } - -private data class Length(val lengthTrait: LengthTrait) : StringTraitInfo() { +data class Length(val lengthTrait: LengthTrait) : StringTraitInfo() { override fun toTraitInfo(): TraitInfo = TraitInfo( tryFromCheck = { rust("Self::check_length(&value)?;") }, constraintViolationVariant = { @@ -233,8 +230,7 @@ private data class Length(val lengthTrait: LengthTrait) : StringTraitInfo() { } } -private data class Pattern(val symbol: Symbol, val patternTrait: PatternTrait, val isSensitive: Boolean) : - StringTraitInfo() { +data class Pattern(val symbol: Symbol, val patternTrait: PatternTrait, val isSensitive: Boolean) : StringTraitInfo() { override fun toTraitInfo(): TraitInfo { return TraitInfo( tryFromCheck = { rust("let value = Self::check_pattern(value)?;") }, @@ -244,9 +240,10 @@ private data class Pattern(val symbol: Symbol, val patternTrait: PatternTrait, v rust("Pattern(String)") }, asValidationExceptionField = { + Attribute.AllowUnusedVariables.render(this) rustTemplate( """ - Self::Pattern(_string) => crate::model::ValidationExceptionField { + Self::Pattern(string) => crate::model::ValidationExceptionField { message: #{ErrorMessage:W}, path }, @@ -268,7 +265,7 @@ private data class Pattern(val symbol: Symbol, val patternTrait: PatternTrait, v ) } - private fun errorMessage(): Writable { + fun errorMessage(): Writable { val pattern = patternTrait.pattern return if (isSensitive) { @@ -283,7 +280,7 @@ private data class Pattern(val symbol: Symbol, val patternTrait: PatternTrait, v writable { rust( """ - format!("Value {} at '{}' failed to satisfy constraint: Member must satisfy regular expression pattern: {}", &_string, &path, r##"$pattern"##) + format!("Value {} at '{}' failed to satisfy constraint: Member must satisfy regular expression pattern: {}", &string, &path, r##"$pattern"##) """, ) } @@ -327,7 +324,7 @@ private data class Pattern(val symbol: Symbol, val patternTrait: PatternTrait, v } } -private sealed class StringTraitInfo { +sealed class StringTraitInfo { companion object { fun fromTrait(symbol: Symbol, trait: Trait, isSensitive: Boolean) = when (trait) { diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/MapConstraintViolationGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/MapConstraintViolationGenerator.kt index cfcf5a53e1..4833fa3058 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/MapConstraintViolationGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/MapConstraintViolationGenerator.kt @@ -10,23 +10,18 @@ import software.amazon.smithy.model.shapes.StringShape import software.amazon.smithy.model.traits.LengthTrait import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.rustlang.Visibility -import software.amazon.smithy.rust.codegen.core.rustlang.rust -import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock -import software.amazon.smithy.rust.codegen.core.rustlang.rustBlockTemplate import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate -import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.module -import software.amazon.smithy.rust.codegen.core.util.getTrait import software.amazon.smithy.rust.codegen.core.util.hasTrait import software.amazon.smithy.rust.codegen.server.smithy.PubCrateConstraintViolationSymbolProvider import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext import software.amazon.smithy.rust.codegen.server.smithy.traits.isReachableFromOperationInput -import software.amazon.smithy.rust.codegen.server.smithy.validationErrorMessage class MapConstraintViolationGenerator( codegenContext: ServerCodegenContext, private val modelsModuleWriter: RustWriter, val shape: MapShape, + private val validationExceptionConversionGenerator: ValidationExceptionConversionGenerator, ) { private val model = codegenContext.model private val constrainedShapeSymbolProvider = codegenContext.constrainedShapeSymbolProvider @@ -80,35 +75,20 @@ class MapConstraintViolationGenerator( ) if (shape.isReachableFromOperationInput()) { - rustBlock("impl $constraintViolationName") { - rustBlockTemplate( - "pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField", - "String" to RuntimeType.String, - ) { - rustBlock("match self") { - shape.getTrait()?.also { - rust( - """ - Self::Length(length) => crate::model::ValidationExceptionField { - message: format!("${it.validationErrorMessage()}", length, &path), - path, - }, - """, - ) - } - if (isKeyConstrained(keyShape, symbolProvider)) { - // Note how we _do not_ append the key's member name to the path. This is intentional, as - // per the `RestJsonMalformedLengthMapKey` test. Note keys are always strings. - // https://github.com/awslabs/smithy/blob/ee0b4ff90daaaa5101f32da936c25af8c91cc6e9/smithy-aws-protocol-tests/model/restJson1/validation/malformed-length.smithy#L296-L295 - rust("""Self::Key(key_constraint_violation) => key_constraint_violation.as_validation_exception_field(path),""") - } - if (isValueConstrained(valueShape, model, symbolProvider)) { - // `as_str()` works with regular `String`s and constrained string shapes. - rust("""Self::Value(key, value_constraint_violation) => value_constraint_violation.as_validation_exception_field(path + "/" + key.as_str()),""") - } - } + rustTemplate( + """ + impl $constraintViolationName { + #{MapShapeConstraintViolationImplBlock} } - } + """, + "MapShapeConstraintViolationImplBlock" to validationExceptionConversionGenerator.mapShapeConstraintViolationImplBlock( + shape, + keyShape, + valueShape, + symbolProvider, + model, + ), + ) } } } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderConstraintViolations.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderConstraintViolations.kt index 55eae76c51..b2c89f8d73 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderConstraintViolations.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderConstraintViolations.kt @@ -17,7 +17,6 @@ import software.amazon.smithy.rust.codegen.core.rustlang.docs import software.amazon.smithy.rust.codegen.core.rustlang.rust import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate -import software.amazon.smithy.rust.codegen.core.rustlang.writable import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.isOptional import software.amazon.smithy.rust.codegen.core.smithy.makeRustBoxed @@ -38,6 +37,7 @@ class ServerBuilderConstraintViolations( codegenContext: ServerCodegenContext, private val shape: StructureShape, private val builderTakesInUnconstrainedTypes: Boolean, + private val validationExceptionConversionGenerator: ValidationExceptionConversionGenerator, ) { private val model = codegenContext.model private val symbolProvider = codegenContext.symbolProvider @@ -154,25 +154,6 @@ class ServerBuilderConstraintViolations( } private fun renderAsValidationExceptionFieldList(writer: RustWriter) { - val validationExceptionFieldWritable = writable { - rustBlock("match self") { - all.forEach { - if (it.hasInner()) { - rust("""ConstraintViolation::${it.name()}(inner) => inner.as_validation_exception_field(path + "/${it.forMember.memberName}"),""") - } else { - rust( - """ - ConstraintViolation::${it.name()} => crate::model::ValidationExceptionField { - message: format!("Value null at '{}/${it.forMember.memberName}' failed to satisfy constraint: Member must not be null", path), - path: path + "/${it.forMember.memberName}", - }, - """, - ) - } - } - } - } - writer.rustTemplate( """ impl ConstraintViolation { @@ -181,7 +162,7 @@ class ServerBuilderConstraintViolations( } } """, - "ValidationExceptionFieldWritable" to validationExceptionFieldWritable, + "ValidationExceptionFieldWritable" to validationExceptionConversionGenerator.builderConstraintViolationImplBlock((all)), "String" to RuntimeType.String, ) } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGenerator.kt index 27bfa69d32..178aa725b7 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGenerator.kt @@ -88,6 +88,7 @@ import software.amazon.smithy.rust.codegen.server.smithy.wouldHaveConstrainedWra class ServerBuilderGenerator( codegenContext: ServerCodegenContext, private val shape: StructureShape, + private val customValidationExceptionWithReasonConversionGenerator: ValidationExceptionConversionGenerator, ) { companion object { /** @@ -141,7 +142,7 @@ class ServerBuilderGenerator( private val builderSymbol = shape.serverBuilderSymbol(codegenContext) private val isBuilderFallible = hasFallibleBuilder(shape, model, symbolProvider, takeInUnconstrainedTypes) private val serverBuilderConstraintViolations = - ServerBuilderConstraintViolations(codegenContext, shape, takeInUnconstrainedTypes) + ServerBuilderConstraintViolations(codegenContext, shape, takeInUnconstrainedTypes, customValidationExceptionWithReasonConversionGenerator) private val codegenScope = arrayOf( "RequestRejection" to ServerRuntimeType.requestRejection(runtimeConfig), @@ -214,21 +215,9 @@ class ServerBuilderGenerator( private fun renderImplFromConstraintViolationForRequestRejection(writer: RustWriter) { writer.rustTemplate( """ - impl #{From} for #{RequestRejection} { - fn from(constraint_violation: ConstraintViolation) -> Self { - let first_validation_exception_field = constraint_violation.as_validation_exception_field("".to_owned()); - let validation_exception = crate::error::ValidationException { - message: format!("1 validation error detected. {}", &first_validation_exception_field.message), - field_list: Some(vec![first_validation_exception_field]), - }; - Self::ConstraintViolation( - crate::operation_ser::serialize_structure_crate_error_validation_exception(&validation_exception) - .expect("impossible") - ) - } - } + #{Converter:W} """, - *codegenScope, + "Converter" to customValidationExceptionWithReasonConversionGenerator.renderImplFromConstraintViolationForRequestRejection(), ) } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorWithoutPublicConstrainedTypes.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorWithoutPublicConstrainedTypes.kt index b30252a8a7..32e58fd44e 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorWithoutPublicConstrainedTypes.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorWithoutPublicConstrainedTypes.kt @@ -46,6 +46,7 @@ import software.amazon.smithy.rust.codegen.server.smithy.ServerRuntimeType class ServerBuilderGeneratorWithoutPublicConstrainedTypes( private val codegenContext: ServerCodegenContext, shape: StructureShape, + validationExceptionConversionGenerator: ValidationExceptionConversionGenerator, ) { companion object { /** @@ -79,7 +80,7 @@ class ServerBuilderGeneratorWithoutPublicConstrainedTypes( private val builderSymbol = shape.serverBuilderSymbol(symbolProvider, false) private val isBuilderFallible = hasFallibleBuilder(shape, symbolProvider) private val serverBuilderConstraintViolations = - ServerBuilderConstraintViolations(codegenContext, shape, builderTakesInUnconstrainedTypes = false) + ServerBuilderConstraintViolations(codegenContext, shape, builderTakesInUnconstrainedTypes = false, validationExceptionConversionGenerator) private val codegenScope = arrayOf( "RequestRejection" to ServerRuntimeType.requestRejection(codegenContext.runtimeConfig), diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerEnumGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerEnumGenerator.kt index 1a55cb8879..82c040f66b 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerEnumGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerEnumGenerator.kt @@ -23,6 +23,7 @@ open class ServerEnumGenerator( val codegenContext: ServerCodegenContext, private val writer: RustWriter, shape: StringShape, + private val validationExceptionConversionGenerator: ValidationExceptionConversionGenerator, ) : EnumGenerator(codegenContext.model, codegenContext.symbolProvider, writer, shape, shape.expectTrait()) { override var target: CodegenTarget = CodegenTarget.SERVER @@ -52,21 +53,13 @@ open class ServerEnumGenerator( ) if (shape.isReachableFromOperationInput()) { - val enumValueSet = enumTrait.enumDefinitionValues.joinToString(", ") - val message = "Value {} at '{}' failed to satisfy constraint: Member must satisfy enum value set: [$enumValueSet]" - rustTemplate( """ impl $constraintViolationName { - pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField { - crate::model::ValidationExceptionField { - message: format!(r##"$message"##, &self.0, &path), - path, - } - } + #{EnumShapeConstraintViolationImplBlock:W} } """, - *codegenScope, + "EnumShapeConstraintViolationImplBlock" to validationExceptionConversionGenerator.enumShapeConstraintViolationImplBlock(enumTrait), ) } } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ValidationExceptionConversionGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ValidationExceptionConversionGenerator.kt new file mode 100644 index 0000000000..7db4426586 --- /dev/null +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ValidationExceptionConversionGenerator.kt @@ -0,0 +1,50 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.server.smithy.generators + +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.MapShape +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.model.shapes.StringShape +import software.amazon.smithy.model.traits.EnumTrait +import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider + +/** + * Collection of methods that will be invoked by the respective generators to generate code to convert constraint + * violations to validation exceptions. + * This is only rendered for shapes that lie in a constrained operation's closure. + */ +interface ValidationExceptionConversionGenerator { + val shapeId: ShapeId + + /** + * Convert from a top-level operation input's constraint violation into + * `aws_smithy_http_server::rejection::RequestRejection`. + */ + fun renderImplFromConstraintViolationForRequestRejection(): Writable + + // Simple shapes. + fun stringShapeConstraintViolationImplBlock(stringConstraintsInfo: Collection): Writable + fun enumShapeConstraintViolationImplBlock(enumTrait: EnumTrait): Writable + fun numberShapeConstraintViolationImplBlock(rangeInfo: Range): Writable + fun blobShapeConstraintViolationImplBlock(blobConstraintsInfo: Collection): Writable + + // Aggregate shapes. + fun mapShapeConstraintViolationImplBlock( + shape: MapShape, + keyShape: StringShape, + valueShape: Shape, + symbolProvider: RustSymbolProvider, + model: Model, + ): Writable + fun builderConstraintViolationImplBlock(constraintViolations: Collection): Writable + fun collectionShapeConstraintViolationImplBlock( + collectionConstraintsInfo: Collection, + isMemberConstrained: Boolean, + ): Writable +} diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerCodegenIntegrationTest.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerCodegenIntegrationTest.kt new file mode 100644 index 0000000000..fc83f1392b --- /dev/null +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerCodegenIntegrationTest.kt @@ -0,0 +1,54 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.server.smithy.testutil + +import software.amazon.smithy.build.PluginContext +import software.amazon.smithy.build.SmithyBuildPlugin +import software.amazon.smithy.model.Model +import software.amazon.smithy.rust.codegen.core.smithy.RustCrate +import software.amazon.smithy.rust.codegen.core.testutil.IntegrationTestParams +import software.amazon.smithy.rust.codegen.core.testutil.codegenIntegrationTest +import software.amazon.smithy.rust.codegen.server.smithy.RustServerCodegenPlugin +import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext +import software.amazon.smithy.rust.codegen.server.smithy.customize.ServerCodegenDecorator +import java.nio.file.Path + +/** + * This file is entirely analogous to [software.amazon.smithy.rust.codegen.client.testutil.ClientCodegenIntegrationTest.kt]. + */ + +fun serverIntegrationTest( + model: Model, + params: IntegrationTestParams = IntegrationTestParams(), + additionalDecorators: List = listOf(), + test: (ServerCodegenContext, RustCrate) -> Unit = { _, _ -> }, +): Path { + fun invokeRustCodegenPlugin(ctx: PluginContext) { + val codegenDecorator = object : ServerCodegenDecorator { + override val name: String = "Add tests" + override val order: Byte = 0 + + override fun classpathDiscoverable(): Boolean = false + + override fun extras(codegenContext: ServerCodegenContext, rustCrate: RustCrate) { + test(codegenContext, rustCrate) + } + } + RustServerCodegenPlugin().executeWithDecorator(ctx, codegenDecorator, *additionalDecorators.toTypedArray()) + } + return codegenIntegrationTest(model, params, invokePlugin = ::invokeRustCodegenPlugin) +} + +abstract class ServerDecoratableBuildPlugin : SmithyBuildPlugin { + abstract fun executeWithDecorator( + context: PluginContext, + vararg decorator: ServerCodegenDecorator, + ) + + override fun execute(context: PluginContext) { + executeWithDecorator(context) + } +} diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerTestHelpers.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerTestHelpers.kt index dd80905a2e..e1e4f3bd52 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerTestHelpers.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerTestHelpers.kt @@ -24,6 +24,7 @@ import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenConfig import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext import software.amazon.smithy.rust.codegen.server.smithy.ServerRustSettings import software.amazon.smithy.rust.codegen.server.smithy.ServerSymbolProviders +import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionConversionGenerator import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerBuilderGenerator // These are the settings we default to if the user does not override them in their `smithy-build.json`. @@ -122,7 +123,7 @@ fun StructureShape.serverRenderWithModelBuilder(model: Model, symbolProvider: Ru val serverCodegenContext = serverTestCodegenContext(model) // Note that this always uses `ServerBuilderGenerator` and _not_ `ServerBuilderGeneratorWithoutPublicConstrainedTypes`, // regardless of the `publicConstrainedTypes` setting. - val modelBuilder = ServerBuilderGenerator(serverCodegenContext, this) + val modelBuilder = ServerBuilderGenerator(serverCodegenContext, this, SmithyValidationExceptionConversionGenerator(serverCodegenContext)) modelBuilder.render(writer) writer.implBlock(this, symbolProvider) { modelBuilder.renderConvenienceMethod(this) diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/transformers/AttachValidationExceptionToConstrainedOperationInputsInAllowList.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/transformers/AttachValidationExceptionToConstrainedOperationInputsInAllowList.kt index 02e1d4be64..68840bde20 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/transformers/AttachValidationExceptionToConstrainedOperationInputsInAllowList.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/transformers/AttachValidationExceptionToConstrainedOperationInputsInAllowList.kt @@ -13,6 +13,7 @@ import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.model.transform.ModelTransformer import software.amazon.smithy.rust.codegen.core.smithy.DirectedWalker import software.amazon.smithy.rust.codegen.core.util.inputShape +import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionConversionGenerator import software.amazon.smithy.rust.codegen.server.smithy.hasConstraintTrait /** @@ -60,11 +61,11 @@ object AttachValidationExceptionToConstrainedOperationInputsInAllowList { walker.walkShapes(operationShape.inputShape(model)) .any { it is SetShape || it is EnumShape || it.hasConstraintTrait() } } - .filter { !it.errors.contains(ShapeId.from("smithy.framework#ValidationException")) } + .filter { !it.errors.contains(SmithyValidationExceptionConversionGenerator.SHAPE_ID) } return ModelTransformer.create().mapShapes(model) { shape -> if (shape is OperationShape && operationsWithConstrainedInputWithoutValidationException.contains(shape)) { - shape.toBuilder().addError("smithy.framework#ValidationException").build() + shape.toBuilder().addError(SmithyValidationExceptionConversionGenerator.SHAPE_ID).build() } else { shape } diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitorTest.kt index 2f23c143f4..942e5f7617 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitorTest.kt @@ -11,6 +11,7 @@ import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.core.testutil.generatePluginContext import software.amazon.smithy.rust.codegen.server.smithy.customizations.ServerRequiredCustomizations +import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionDecorator import software.amazon.smithy.rust.codegen.server.smithy.customize.CombinedServerCodegenDecorator import kotlin.io.path.writeText @@ -45,8 +46,12 @@ class ServerCodegenVisitorTest { """.asSmithyModel(smithyVersion = "2.0") val (ctx, testDir) = generatePluginContext(model) testDir.resolve("src/main.rs").writeText("fn main() {}") - val codegenDecorator: CombinedServerCodegenDecorator = - CombinedServerCodegenDecorator.fromClasspath(ctx, ServerRequiredCustomizations()) + val codegenDecorator = + CombinedServerCodegenDecorator.fromClasspath( + ctx, + ServerRequiredCustomizations(), + SmithyValidationExceptionDecorator(), + ) val visitor = ServerCodegenVisitor(ctx, codegenDecorator) val baselineModel = visitor.baselineTransformInternalTest(model) baselineModel.getShapesWithTrait(ShapeId.from("smithy.api#mixin")).isEmpty() shouldBe true diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraintsAreNotUsedTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraintsAreNotUsedTest.kt index 83ea701a97..7e111f758c 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraintsAreNotUsedTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraintsAreNotUsedTest.kt @@ -18,6 +18,7 @@ import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.rust.codegen.core.smithy.transformers.EventStreamNormalizer import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.core.util.lookup +import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionConversionGenerator import java.util.logging.Level internal class ValidateUnsupportedConstraintsAreNotUsedTest { @@ -53,7 +54,11 @@ internal class ValidateUnsupportedConstraintsAreNotUsedTest { } """.asSmithyModel() val service = model.lookup("test#TestService") - val validationResult = validateOperationsWithConstrainedInputHaveValidationExceptionAttached(model, service) + val validationResult = validateOperationsWithConstrainedInputHaveValidationExceptionAttached( + model, + service, + SmithyValidationExceptionConversionGenerator.SHAPE_ID, + ) validationResult.messages shouldHaveSize 1 diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/CustomValidationExceptionWithReasonDecoratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/CustomValidationExceptionWithReasonDecoratorTest.kt new file mode 100644 index 0000000000..31b4602381 --- /dev/null +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/CustomValidationExceptionWithReasonDecoratorTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.server.smithy.customizations + +import org.junit.jupiter.api.Test +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.node.Node +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.model.transform.ModelTransformer +import software.amazon.smithy.rust.codegen.core.testutil.IntegrationTestParams +import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel +import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverIntegrationTest +import java.io.File +import kotlin.streams.toList + +fun swapOutSmithyValidationExceptionForCustomOne(model: Model): Model { + val customValidationExceptionModel = + """ + namespace com.amazonaws.constraints + + enum ValidationExceptionFieldReason { + LENGTH_NOT_VALID = "LengthNotValid" + PATTERN_NOT_VALID = "PatternNotValid" + SYNTAX_NOT_VALID = "SyntaxNotValid" + VALUE_NOT_VALID = "ValueNotValid" + OTHER = "Other" + } + + /// Stores information about a field passed inside a request that resulted in an exception. + structure ValidationExceptionField { + /// The field name. + @required + Name: String + + @required + Reason: ValidationExceptionFieldReason + + /// Message describing why the field failed validation. + @required + Message: String + } + + /// A list of fields. + list ValidationExceptionFieldList { + member: ValidationExceptionField + } + + enum ValidationExceptionReason { + FIELD_VALIDATION_FAILED = "FieldValidationFailed" + UNKNOWN_OPERATION = "UnknownOperation" + CANNOT_PARSE = "CannotParse" + OTHER = "Other" + } + + /// The input fails to satisfy the constraints specified by an AWS service. + @error("client") + @httpError(400) + structure ValidationException { + /// Description of the error. + @required + Message: String + + /// Reason the request failed validation. + @required + Reason: ValidationExceptionReason + + /// The field that caused the error, if applicable. If more than one field + /// caused the error, pick one and elaborate in the message. + Fields: ValidationExceptionFieldList + } + """.asSmithyModel(smithyVersion = "2.0") + + // Remove Smithy's `ValidationException`. + var model = ModelTransformer.create().removeShapes( + model, + listOf(model.expectShape(SmithyValidationExceptionConversionGenerator.SHAPE_ID)), + ) + // Add our custom one. + model = ModelTransformer.create().replaceShapes(model, customValidationExceptionModel.shapes().toList()) + // Make all operations use our custom one. + val newOperationShapes = model.operationShapes.map { operationShape -> + operationShape.toBuilder().addError(ShapeId.from("com.amazonaws.constraints#ValidationException")).build() + } + return ModelTransformer.create().replaceShapes(model, newOperationShapes) +} + +internal class CustomValidationExceptionWithReasonDecoratorTest { + @Test + fun `constraints model with the CustomValidationExceptionWithReasonDecorator applied compiles`() { + var model = File("../codegen-core/common-test-models/constraints.smithy").readText().asSmithyModel() + model = swapOutSmithyValidationExceptionForCustomOne(model) + + serverIntegrationTest( + model, + IntegrationTestParams( + additionalSettings = Node.objectNodeBuilder().withMember( + "codegen", + Node.objectNodeBuilder() + .withMember("experimentalCustomValidationExceptionWithReasonPleaseDoNotUse", "com.amazonaws.constraints#ValidationException") + .build(), + ).build(), + ), + ) + } +} diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedBlobGeneratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedBlobGeneratorTest.kt index cfb8bbd38b..52d9008ebf 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedBlobGeneratorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedBlobGeneratorTest.kt @@ -23,6 +23,7 @@ import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest import software.amazon.smithy.rust.codegen.core.testutil.unitTest import software.amazon.smithy.rust.codegen.core.util.dq import software.amazon.smithy.rust.codegen.core.util.lookup +import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionConversionGenerator import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverTestCodegenContext import java.util.stream.Stream @@ -68,7 +69,12 @@ class ConstrainedBlobGeneratorTest { project.withModule(ModelsModule) { addDependency(RuntimeType.blob(codegenContext.runtimeConfig).toSymbol()) - ConstrainedBlobGenerator(codegenContext, this, constrainedBlobShape).render() + ConstrainedBlobGenerator( + codegenContext, + this, + constrainedBlobShape, + SmithyValidationExceptionConversionGenerator(codegenContext), + ).render() unitTest( name = "try_from_success", @@ -121,7 +127,12 @@ class ConstrainedBlobGeneratorTest { val writer = RustWriter.forModule(ModelsModule.name) - ConstrainedBlobGenerator(codegenContext, writer, constrainedBlobShape).render() + ConstrainedBlobGenerator( + codegenContext, + writer, + constrainedBlobShape, + SmithyValidationExceptionConversionGenerator(codegenContext), + ).render() // Check that the wrapped type is `pub(crate)`. writer.toString() shouldContain "pub struct ConstrainedBlob(pub(crate) aws_smithy_types::Blob);" diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedCollectionGeneratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedCollectionGeneratorTest.kt index 8e043ec47c..0916fceae2 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedCollectionGeneratorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedCollectionGeneratorTest.kt @@ -33,6 +33,7 @@ import software.amazon.smithy.rust.codegen.core.testutil.unitTest import software.amazon.smithy.rust.codegen.core.util.UNREACHABLE import software.amazon.smithy.rust.codegen.core.util.lookup import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext +import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionConversionGenerator import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverTestCodegenContext import software.amazon.smithy.rust.codegen.server.smithy.transformers.ShapesReachableFromOperationInputTagger import java.util.stream.Stream @@ -284,6 +285,12 @@ class ConstrainedCollectionGeneratorTest { ) { val constraintsInfo = CollectionTraitInfo.fromShape(constrainedCollectionShape, codegenContext.symbolProvider) ConstrainedCollectionGenerator(codegenContext, writer, constrainedCollectionShape, constraintsInfo).render() - CollectionConstraintViolationGenerator(codegenContext, writer, constrainedCollectionShape, constraintsInfo).render() + CollectionConstraintViolationGenerator( + codegenContext, + writer, + constrainedCollectionShape, + constraintsInfo, + SmithyValidationExceptionConversionGenerator(codegenContext), + ).render() } } diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedMapGeneratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedMapGeneratorTest.kt index 2da058cde5..93f6676d47 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedMapGeneratorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedMapGeneratorTest.kt @@ -24,6 +24,7 @@ import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest import software.amazon.smithy.rust.codegen.core.testutil.unitTest import software.amazon.smithy.rust.codegen.core.util.lookup import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext +import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionConversionGenerator import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverTestCodegenContext import software.amazon.smithy.rust.codegen.server.smithy.transformers.ShapesReachableFromOperationInputTagger import java.util.stream.Stream @@ -153,6 +154,11 @@ class ConstrainedMapGeneratorTest { constrainedMapShape: MapShape, ) { ConstrainedMapGenerator(codegenContext, writer, constrainedMapShape).render() - MapConstraintViolationGenerator(codegenContext, writer, constrainedMapShape).render() + MapConstraintViolationGenerator( + codegenContext, + writer, + constrainedMapShape, + SmithyValidationExceptionConversionGenerator(codegenContext), + ).render() } } diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedNumberGeneratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedNumberGeneratorTest.kt index 681d0fffe3..44e08a8fc8 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedNumberGeneratorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedNumberGeneratorTest.kt @@ -20,6 +20,7 @@ import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest import software.amazon.smithy.rust.codegen.core.testutil.unitTest import software.amazon.smithy.rust.codegen.core.util.lookup +import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionConversionGenerator import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverTestCodegenContext import java.util.stream.Stream @@ -71,7 +72,12 @@ class ConstrainedNumberGeneratorTest { val project = TestWorkspace.testProject(symbolProvider) project.withModule(ModelsModule) { - ConstrainedNumberGenerator(codegenContext, this, shape).render() + ConstrainedNumberGenerator( + codegenContext, + this, + shape, + SmithyValidationExceptionConversionGenerator(codegenContext), + ).render() unitTest( name = "try_from_success", @@ -132,7 +138,12 @@ class ConstrainedNumberGeneratorTest { val codegenContext = serverTestCodegenContext(model) val writer = RustWriter.forModule(ModelsModule.name) - ConstrainedNumberGenerator(codegenContext, writer, constrainedShape).render() + ConstrainedNumberGenerator( + codegenContext, + writer, + constrainedShape, + SmithyValidationExceptionConversionGenerator(codegenContext), + ).render() // Check that the wrapped type is `pub(crate)`. writer.toString() shouldContain "pub struct $shapeName(pub(crate) $rustType);" diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedStringGeneratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedStringGeneratorTest.kt index a8fa6e441d..63fafa4a13 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedStringGeneratorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedStringGeneratorTest.kt @@ -23,6 +23,7 @@ import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest import software.amazon.smithy.rust.codegen.core.testutil.unitTest import software.amazon.smithy.rust.codegen.core.util.CommandFailed import software.amazon.smithy.rust.codegen.core.util.lookup +import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionConversionGenerator import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverTestCodegenContext import java.util.stream.Stream @@ -82,7 +83,12 @@ class ConstrainedStringGeneratorTest { val project = TestWorkspace.testProject(symbolProvider) project.withModule(ModelsModule) { - ConstrainedStringGenerator(codegenContext, this, constrainedStringShape).render() + ConstrainedStringGenerator( + codegenContext, + this, + constrainedStringShape, + SmithyValidationExceptionConversionGenerator(codegenContext), + ).render() unitTest( name = "try_from_success", @@ -136,7 +142,12 @@ class ConstrainedStringGeneratorTest { val writer = RustWriter.forModule(ModelsModule.name) - ConstrainedStringGenerator(codegenContext, writer, constrainedStringShape).render() + ConstrainedStringGenerator( + codegenContext, + writer, + constrainedStringShape, + SmithyValidationExceptionConversionGenerator(codegenContext), + ).render() // Check that the wrapped type is `pub(crate)`. writer.toString() shouldContain "pub struct ConstrainedString(pub(crate) std::string::String);" @@ -162,8 +173,19 @@ class ConstrainedStringGeneratorTest { val project = TestWorkspace.testProject(codegenContext.symbolProvider) project.withModule(ModelsModule) { - ConstrainedStringGenerator(codegenContext, this, constrainedStringShape).render() - ConstrainedStringGenerator(codegenContext, this, sensitiveConstrainedStringShape).render() + val validationExceptionConversionGenerator = SmithyValidationExceptionConversionGenerator(codegenContext) + ConstrainedStringGenerator( + codegenContext, + this, + constrainedStringShape, + validationExceptionConversionGenerator, + ).render() + ConstrainedStringGenerator( + codegenContext, + this, + sensitiveConstrainedStringShape, + validationExceptionConversionGenerator, + ).render() unitTest( name = "non_sensitive_string_display_implementation", @@ -201,7 +223,12 @@ class ConstrainedStringGeneratorTest { val project = TestWorkspace.testProject(codegenContext.symbolProvider) project.withModule(ModelsModule) { - ConstrainedStringGenerator(codegenContext, this, constrainedStringShape).render() + ConstrainedStringGenerator( + codegenContext, + this, + constrainedStringShape, + SmithyValidationExceptionConversionGenerator(codegenContext), + ).render() } assertThrows { diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderDefaultValuesTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderDefaultValuesTest.kt index 2219c2e653..dc18daf855 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderDefaultValuesTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderDefaultValuesTest.kt @@ -31,6 +31,7 @@ import software.amazon.smithy.rust.codegen.core.util.lookup import software.amazon.smithy.rust.codegen.core.util.toPascalCase import software.amazon.smithy.rust.codegen.core.util.toSnakeCase import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenConfig +import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionConversionGenerator import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverTestCodegenContext import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverTestRustSettings import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverTestSymbolProvider @@ -175,28 +176,38 @@ class ServerBuilderDefaultValuesTest { codegenConfig = ServerCodegenConfig(publicConstrainedTypes = false), ), ) - val builderGenerator = ServerBuilderGeneratorWithoutPublicConstrainedTypes(codegenContext, struct) + val builderGenerator = ServerBuilderGeneratorWithoutPublicConstrainedTypes(codegenContext, struct, SmithyValidationExceptionConversionGenerator(codegenContext)) writer.implBlock(struct, symbolProvider) { builderGenerator.renderConvenienceMethod(writer) } builderGenerator.render(writer) - ServerEnumGenerator(codegenContext, writer, model.lookup("com.test#Language")).render() + ServerEnumGenerator( + codegenContext, + writer, + model.lookup("com.test#Language"), + SmithyValidationExceptionConversionGenerator(codegenContext), + ).render() StructureGenerator(model, symbolProvider, writer, struct).render() } private fun writeServerBuilderGenerator(writer: RustWriter, model: Model, symbolProvider: RustSymbolProvider) { val struct = model.lookup("com.test#MyStruct") val codegenContext = serverTestCodegenContext(model) - val builderGenerator = ServerBuilderGenerator(codegenContext, struct) + val builderGenerator = ServerBuilderGenerator(codegenContext, struct, SmithyValidationExceptionConversionGenerator(codegenContext)) writer.implBlock(struct, symbolProvider) { builderGenerator.renderConvenienceMethod(writer) } builderGenerator.render(writer) - ServerEnumGenerator(codegenContext, writer, model.lookup("com.test#Language")).render() + ServerEnumGenerator( + codegenContext, + writer, + model.lookup("com.test#Language"), + SmithyValidationExceptionConversionGenerator(codegenContext), + ).render() StructureGenerator(model, symbolProvider, writer, struct).render() } diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorTest.kt index 24688f3654..515bbb58f8 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorTest.kt @@ -14,6 +14,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.generators.implBlock import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest import software.amazon.smithy.rust.codegen.core.util.lookup +import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionConversionGenerator import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverTestCodegenContext class ServerBuilderGeneratorTest { @@ -38,7 +39,7 @@ class ServerBuilderGeneratorTest { val writer = RustWriter.forModule("model") val shape = model.lookup("test#Credentials") StructureGenerator(model, codegenContext.symbolProvider, writer, shape).render(CodegenTarget.SERVER) - val builderGenerator = ServerBuilderGenerator(codegenContext, shape) + val builderGenerator = ServerBuilderGenerator(codegenContext, shape, SmithyValidationExceptionConversionGenerator(codegenContext)) builderGenerator.render(writer) writer.implBlock(shape, codegenContext.symbolProvider) { builderGenerator.renderConvenienceMethod(this) diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerEnumGeneratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerEnumGeneratorTest.kt index 0e813cebbd..abef4485aa 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerEnumGeneratorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerEnumGeneratorTest.kt @@ -12,6 +12,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest import software.amazon.smithy.rust.codegen.core.util.lookup +import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionConversionGenerator import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverTestCodegenContext class ServerEnumGeneratorTest { @@ -40,7 +41,12 @@ class ServerEnumGeneratorTest { @Test fun `it generates TryFrom, FromStr and errors for enums`() { - ServerEnumGenerator(codegenContext, writer, shape).render() + ServerEnumGenerator( + codegenContext, + writer, + shape, + SmithyValidationExceptionConversionGenerator(codegenContext), + ).render() writer.compileAndTest( """ use std::str::FromStr; @@ -53,10 +59,15 @@ class ServerEnumGeneratorTest { @Test fun `it generates enums without the unknown variant`() { - ServerEnumGenerator(codegenContext, writer, shape).render() + ServerEnumGenerator( + codegenContext, + writer, + shape, + SmithyValidationExceptionConversionGenerator(codegenContext), + ).render() writer.compileAndTest( """ - // check no unknown + // Check no `Unknown` variant. let instance = InstanceType::T2Micro; match instance { InstanceType::T2Micro => (), @@ -68,7 +79,12 @@ class ServerEnumGeneratorTest { @Test fun `it generates enums without non_exhaustive`() { - ServerEnumGenerator(codegenContext, writer, shape).render() + ServerEnumGenerator( + codegenContext, + writer, + shape, + SmithyValidationExceptionConversionGenerator(codegenContext), + ).render() writer.toString() shouldNotContain "#[non_exhaustive]" } } diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedCollectionGeneratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedCollectionGeneratorTest.kt index 6a02765c9a..a0d608d7c7 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedCollectionGeneratorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedCollectionGeneratorTest.kt @@ -16,6 +16,7 @@ import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest import software.amazon.smithy.rust.codegen.core.testutil.unitTest import software.amazon.smithy.rust.codegen.core.util.lookup +import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionConversionGenerator import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverRenderWithModelBuilder import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverTestCodegenContext @@ -68,7 +69,13 @@ class UnconstrainedCollectionGeneratorTest { it, ).render() - CollectionConstraintViolationGenerator(codegenContext, this@modelsModuleWriter, it, listOf()).render() + CollectionConstraintViolationGenerator( + codegenContext, + this@modelsModuleWriter, + it, + CollectionTraitInfo.fromShape(it, codegenContext.constrainedShapeSymbolProvider), + SmithyValidationExceptionConversionGenerator(codegenContext), + ).render() } this@unconstrainedModuleWriter.unitTest( diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedMapGeneratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedMapGeneratorTest.kt index 75e176be39..ea2a6907a3 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedMapGeneratorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedMapGeneratorTest.kt @@ -16,6 +16,7 @@ import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest import software.amazon.smithy.rust.codegen.core.testutil.unitTest import software.amazon.smithy.rust.codegen.core.util.lookup +import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionConversionGenerator import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverRenderWithModelBuilder import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverTestCodegenContext @@ -66,7 +67,12 @@ class UnconstrainedMapGeneratorTest { listOf(mapA, mapB).forEach { UnconstrainedMapGenerator(codegenContext, this@unconstrainedModuleWriter, it).render() - MapConstraintViolationGenerator(codegenContext, this@modelsModuleWriter, it).render() + MapConstraintViolationGenerator( + codegenContext, + this@modelsModuleWriter, + it, + SmithyValidationExceptionConversionGenerator(codegenContext), + ).render() } this@unconstrainedModuleWriter.unitTest( diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/eventstream/ServerEventStreamBaseRequirements.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/eventstream/ServerEventStreamBaseRequirements.kt index e3ff924db8..2eae338e6b 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/eventstream/ServerEventStreamBaseRequirements.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/eventstream/ServerEventStreamBaseRequirements.kt @@ -21,6 +21,7 @@ import software.amazon.smithy.rust.codegen.core.testutil.EventStreamTestModels import software.amazon.smithy.rust.codegen.core.testutil.EventStreamTestRequirements import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenConfig import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext +import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionConversionGenerator import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerBuilderGenerator import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerBuilderGeneratorWithoutPublicConstrainedTypes import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerOperationErrorGenerator @@ -55,7 +56,8 @@ abstract class ServerEventStreamBaseRequirements : EventStreamTestRequirements