Skip to content

Commit

Permalink
Move converters from constraint violations into ValidationException
Browse files Browse the repository at this point in the history
… to decorators

This allows "easily" converting to other custom validation exception
shapes by implementing a decorator.

As a simple example, this PR adds a
`CustomValidationExceptionWithReasonDecorator` decorator that is able to
convert into a shape that is very similar to
`smithy.framework#ValidationException`, but that has an additional
`reason` field. The decorator can be enabled via the newly added
`experimentalCustomValidationExceptionWithReasonPleaseDoNotUse` codegen
config flag.

This effectively provides a way for users to use custom validation
exceptions without having to wait for the full implementation of #2053,
provided they're interested enough to write a decorator in a JVM
language. This mechanism is _experimental_ and will be removed once full
support for custom validation exceptions as described in #2053 lands,
hence why the configuration key is strongly worded in this respect.

This commit also ports the mechanism to run codegen integration tests
within Kotlin unit tests for client SDKs to the server. See #1956 for
details. The custom validation exception decorator is tested this way.
  • Loading branch information
david-perez committed Feb 3, 2023
1 parent 7bf9251 commit f0aa96b
Show file tree
Hide file tree
Showing 50 changed files with 1,199 additions and 362 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -66,10 +66,10 @@ class RustClientCodegenPlugin : DecoratableBuildPlugin() {
}

companion object {
/** SymbolProvider
/**
* When generating code, smithy types need to be converted into Rust types—that is the core role of the symbol provider
*
* The Symbol provider is composed of a base `SymbolVisitor` which handles the core functionality, then is layered
* The Symbol provider is composed of a base [SymbolVisitor] which handles the core functionality, then is layered
* with other symbol providers, documented inline, to handle the full scope of Smithy types.
*/
fun baseSymbolProvider(model: Model, serviceShape: ServiceShape, symbolVisitorConfig: SymbolVisitorConfig) =
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ClientCodegenDecorator> = 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)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ class RustWriter private constructor(
* Callers must take care to use [this] when writing to ensure code is written to the right place:
* ```kotlin
* val writer = RustWriter.forModule("model")
* writer.withModule(RustModule.public("nested")) {
* writer.withInlineModule(RustModule.public("nested")) {
* Generator(...).render(this) // GOOD
* Generator(...).render(writer) // WRONG!
* }
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,7 @@ fun generatePluginContext(
)
}

val settings = settingsBuilder.merge(additionalSettings)
.build()
val settings = settingsBuilder.merge(additionalSettings).build()
val pluginContext = PluginContext.builder().model(model).fileManifest(manifest).settings(settings).build()
return pluginContext to testPath
}
Expand Down
6 changes: 6 additions & 0 deletions codegen-server-test/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ val allCodegenTests = "../codegen-core/common-test-models".let { commonModels ->
imports = listOf("$commonModels/constraints.smithy"),
extraConfig = """, "codegen": { "publicConstrainedTypes": false } """,
),
CodegenTest(
"com.amazonaws.constraints#CustomValidationExceptionsExperimental",
"custom_validation_exceptions_experimental",
imports = listOf("$commonModels/custom-validation-exceptions-experimental.smithy"),
extraConfig = """, "codegen": { "experimentalCustomValidationExceptionWithReasonPleaseDoNotUse": "com.amazonaws.constraints#ValidationException" } """.trimMargin(),
),
CodegenTest(
"com.amazonaws.constraints#UniqueItemsService",
"unique_items",
Expand Down
4 changes: 4 additions & 0 deletions codegen-server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,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
Expand All @@ -47,7 +49,10 @@ class PythonCodegenServerPlugin : 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit f0aa96b

Please sign in to comment.