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 aa4eebc
Show file tree
Hide file tree
Showing 51 changed files with 1,279 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
@@ -0,0 +1,80 @@
$version: "2.0"

namespace com.amazonaws.constraints

use aws.protocols#restJson1

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
}

/// A service to test (experimental support for) custom validation exceptions.
@restJson1
@title("CustomValidationExceptionsExperimental")
service CustomValidationExceptionsExperimental {
operations: [
ConstrainedShapesOperation,
],
}

@http(uri: "/constrained-shapes-operation", method: "POST")
operation ConstrainedShapesOperation {
input: ConstrainedShapesOperationInputOutput,
output: ConstrainedShapesOperationInputOutput,
errors: [ValidationException]
}

structure ConstrainedShapesOperationInputOutput {
@required
lengthString: LengthString,
}

@length(min: 2, max: 69)
string LengthString
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
}
Loading

0 comments on commit aa4eebc

Please sign in to comment.