diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCrateDocsDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCrateDocsDecorator.kt index b98e8d4bb45..56ac86a2ab0 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCrateDocsDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCrateDocsDecorator.kt @@ -11,6 +11,7 @@ import org.jsoup.nodes.TextNode import software.amazon.smithy.model.traits.DocumentationTrait import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator +import software.amazon.smithy.rust.codegen.client.smithy.customize.SerdeDecorator import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.rustlang.Writable diff --git a/buildSrc/src/main/kotlin/CodegenTestCommon.kt b/buildSrc/src/main/kotlin/CodegenTestCommon.kt index ac9d8db6c9b..d6dea95060f 100644 --- a/buildSrc/src/main/kotlin/CodegenTestCommon.kt +++ b/buildSrc/src/main/kotlin/CodegenTestCommon.kt @@ -268,14 +268,12 @@ fun Project.registerCargoCommandsTasks( this.tasks.register(Cargo.CHECK.toString) { dependsOn(dependentTasks) workingDir(outputDir) - environment("RUSTFLAGS", "--cfg aws_sdk_unstable") commandLine("cargo", "check", "--lib", "--tests", "--benches", "--all-features") } this.tasks.register(Cargo.TEST.toString) { dependsOn(dependentTasks) workingDir(outputDir) - environment("RUSTFLAGS", "--cfg aws_sdk_unstable") commandLine("cargo", "test", "--all-features", "--no-fail-fast") } @@ -283,14 +281,12 @@ fun Project.registerCargoCommandsTasks( dependsOn(dependentTasks) workingDir(outputDir) environment("RUSTDOCFLAGS", defaultRustDocFlags) - environment("RUSTFLAGS", "--cfg aws_sdk_unstable") commandLine("cargo", "doc", "--no-deps", "--document-private-items") } this.tasks.register(Cargo.CLIPPY.toString) { dependsOn(dependentTasks) workingDir(outputDir) - environment("RUSTFLAGS", "--cfg aws_sdk_unstable") commandLine("cargo", "clippy") } } 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 7f9c07c2888..1087a63feb5 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 @@ -18,6 +18,7 @@ import software.amazon.smithy.rust.codegen.client.smithy.customizations.Sensitiv import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator import software.amazon.smithy.rust.codegen.client.smithy.customize.CombinedClientCodegenDecorator import software.amazon.smithy.rust.codegen.client.smithy.customize.RequiredCustomizations +import software.amazon.smithy.rust.codegen.client.smithy.customize.SerdeDecorator import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointParamsDecorator import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointsDecorator import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentClientDecorator @@ -59,6 +60,7 @@ class RustClientCodegenPlugin : ClientDecoratableBuildPlugin() { val codegenDecorator = CombinedClientCodegenDecorator.fromClasspath( context, + SerdeDecorator(), ClientCustomizations(), RequiredCustomizations(), FluentClientDecorator(), diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/SerdeDecorator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/SerdeDecorator.kt new file mode 100644 index 00000000000..108b4687820 --- /dev/null +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/SerdeDecorator.kt @@ -0,0 +1,83 @@ +/* +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* SPDX-License-Identifier: Apache-2.0 +*/ + +package software.amazon.smithy.rust.codegen.client.smithy.customize + +import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext +import software.amazon.smithy.rust.codegen.core.rustlang.containerDocs +import software.amazon.smithy.rust.codegen.core.rustlang.Feature +import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter +import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.RustCrate +import software.amazon.smithy.rust.codegen.core.smithy.generators.LibRsCustomization +import software.amazon.smithy.rust.codegen.core.smithy.generators.LibRsSection +import software.amazon.smithy.rust.codegen.core.smithy.generators.ModuleDocSection + +/** + * Decorator that adds the `serde-serialize` and `serde-deserialize` features. + */ +class SerdeDecorator : ClientCodegenDecorator { + override val name: String = "SerdeDecorator" + override val order: Byte = 5 + + override fun extras(codegenContext: ClientCodegenContext, rustCrate: RustCrate) { + fun feature(featureName: String): Feature { + return Feature(featureName, false, listOf("aws-smithy-types/$featureName")) + } + rustCrate.mergeFeature(feature("serde-serialize")) + rustCrate.mergeFeature(feature("serde-deserialize")) + + } + + override fun libRsCustomizations( + codegenContext: ClientCodegenContext, + baseCustomizations: List + ): List { + return baseCustomizations + SerdeDocGenerator(codegenContext) + } +} + +class SerdeDocGenerator(private val codegenContext: ClientCodegenContext) : LibRsCustomization() { + override fun section(section: LibRsSection): Writable { + return when (section) { + is LibRsSection.ModuleDoc-> + if (section.subsection is ModuleDocSection.AWSSdkUnstable) { + serdeInfoText() + } else { + emptySection + } + + else -> emptySection + } + } + + private fun serdeInfoText(): Writable { + return writable { + containerDocs( + """ + ## How to enable `Serialize` and `Deserialize` + + This data type implements `Serialize` and `Deserialize` traits from the popular serde crate, but those traits are behind feature gate. + + As they increase it's compile time dramatically, you should not turn them on unless it's necessary. + Implementation of `serde` traits in AWS SDK for Rust is still unstable, and implementation may change anytime in future. + + To enable traits, you must pass `aws_sdk_unstable` to RUSTFLAGS and enable `serde-serialize` or `serde-deserialize` feature. + + e.g. + ```bash,no_run + export RUSTFLAGS="--cfg aws_sdk_unstable" + cargo build --features serde-serialize serde-deserialize + ``` + + If you enable `serde-serialize` and/or `serde-deserialize` without `RUSTFLAGS="--cfg aws_sdk_unstable"`, + compilation will fail with warning. + + """.trimIndent() + ) + } + } +} diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ClientEnumGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ClientEnumGenerator.kt index 608606740f9..eecf6c9c8c5 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ClientEnumGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ClientEnumGenerator.kt @@ -23,6 +23,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.generators.EnumGenerator import software.amazon.smithy.rust.codegen.core.smithy.generators.EnumGeneratorContext import software.amazon.smithy.rust.codegen.core.smithy.generators.EnumMemberModel import software.amazon.smithy.rust.codegen.core.smithy.generators.EnumType +import software.amazon.smithy.rust.codegen.core.smithy.generators.RenderSerdeAttribute import software.amazon.smithy.rust.codegen.core.util.dq /** Infallible enums have an `Unknown` variant and can't fail to parse */ @@ -158,6 +159,8 @@ data class InfallibleEnumType( This is not intended to be used directly. """.trimIndent(), ) + + RenderSerdeAttribute.addSerdeWithoutShapeModel(this) context.enumMeta.render(this) rustTemplate("struct $UnknownVariantValue(pub(crate) #{String});", *preludeScope) rustBlock("impl $UnknownVariantValue") { diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustType.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustType.kt index 10c8def3995..fefc2353aaf 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustType.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustType.kt @@ -667,6 +667,11 @@ class Attribute(val inner: Writable, val isDeriveHelper: Boolean = false) { } } + fun attributeWithStringAsArgument(runtimeType: RuntimeType, comment: String): Writable = { + // Sorted derives look nicer than unsorted, and it makes test output easier to predict + rustInline("#W(\"$comment\")", runtimeType.writable) + } + fun derive(runtimeTypes: Collection): Writable = derive(*runtimeTypes.toTypedArray()) fun pair(pair: Pair): Writable = diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/BuilderGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/BuilderGenerator.kt index 09c0f49e229..90554a1e5d5 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/BuilderGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/BuilderGenerator.kt @@ -308,11 +308,16 @@ class BuilderGenerator( writer.docs("A builder for #D.", structureSymbol) metadata.additionalAttributes.render(writer) Attribute(derive(builderDerives)).render(writer) + RenderSerdeAttribute.addSerde(writer, shape, model) + RenderSerdeAttribute.addSensitiveWarningDoc(writer, shape, model) writer.rustBlock("pub struct $builderName") { + // add serde for (member in members) { val memberName = symbolProvider.toMemberName(member) // All fields in the builder are optional. val memberSymbol = symbolProvider.toSymbol(member).makeOptional() + RenderSerdeAttribute.skipIfStream(writer, member, model, shape) + RenderSerdeAttribute.addSensitiveWarningDoc(writer, shape, model) renderBuilderMember(this, memberName, memberSymbol) } writeCustomizations(customizations, BuilderSection.AdditionalFields(shape)) diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/CargoTomlGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/CargoTomlGenerator.kt index e5f49163489..66bcc2ca86f 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/CargoTomlGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/CargoTomlGenerator.kt @@ -110,9 +110,18 @@ class CargoTomlGenerator( "dev-dependencies" to dependencies.filter { it.scope == DependencyScope.Dev } .associate { it.name to it.toMap() }, + "target.'cfg(aws_sdk_unstable)'.dependencies" to dependencies.filter { + it.scope == DependencyScope.CfgUnstable + } + .associate { it.name to it.toMap() }, "features" to cargoFeatures.toMap(), ).deepMergeWith(manifestCustomizations) - writer.writeWithNoFormatting(TomlWriter().write(cargoToml)) + // NOTE: without this it will produce ["target.'cfg(aws_sdk_unstable)'.dependencies"] + // In JSON, this is an equivalent of: {"target.'cfg(aws_sdk_unstable)'.dependencies" : ...} + // To make it work, it has to be: {"target": {'cfg(aws_sdk_unstable)': {"dependencies": ...}}} + // This piece of code fixes it. + var tomlString = TomlWriter().write(cargoToml).replace("\"target.'cfg(aws_sdk_unstable)'.dependencies\"", "target.'cfg(aws_sdk_unstable)'.dependencies") + writer.writeWithNoFormatting(tomlString) } } diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/EnumGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/EnumGenerator.kt index d28896f18f6..1c765b745fd 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/EnumGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/EnumGenerator.kt @@ -229,6 +229,8 @@ open class EnumGenerator( private fun RustWriter.renderUnnamedEnum() { documentShape(shape, model) deprecatedShape(shape) + RenderSerdeAttribute.addSerde(this, shape, model) + RenderSerdeAttribute.addSensitiveWarningDoc(this, shape, model) context.enumMeta.render(this) rust("struct ${context.enumName}(String);") implBlock( @@ -279,6 +281,8 @@ open class EnumGenerator( ) deprecatedShape(shape) + RenderSerdeAttribute.addSerde(this, shape, model) + RenderSerdeAttribute.addSensitiveWarningDoc(this, shape, model) context.enumMeta.render(this) rustBlock("enum ${context.enumName}") { context.sortedMembers.forEach { member -> member.render(this) } diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/LibRsGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/LibRsGenerator.kt index 24beaa51d80..10afe312405 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/LibRsGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/LibRsGenerator.kt @@ -25,6 +25,8 @@ sealed class ModuleDocSection { object CrateOrganization : ModuleDocSection() object Examples : ModuleDocSection() + + object AWSSdkUnstable : ModuleDocSection() } sealed class LibRsSection(name: String) : Section(name) { @@ -78,6 +80,16 @@ class LibRsGenerator( } } + docSection(ModuleDocSection.AWSSdkUnstable).also { docs -> + if (docs.isNotEmpty()) { + containerDocs("\n## Enabling Unstable Features") + docs.forEach { writeTo -> + writeTo(this) + } + } + } + + // Examples docSection(ModuleDocSection.Examples).also { docs -> if (docs.isNotEmpty() || settings.examplesUri != null) { diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/RenderSerdeAttribute.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/RenderSerdeAttribute.kt new file mode 100644 index 00000000000..4f5c7535e09 --- /dev/null +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/RenderSerdeAttribute.kt @@ -0,0 +1,62 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.core.smithy.generators + +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.MemberShape +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.traits.ErrorTrait +import software.amazon.smithy.model.traits.SensitiveTrait +import software.amazon.smithy.rust.codegen.core.rustlang.Attribute +import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter +import software.amazon.smithy.rust.codegen.core.rustlang.raw +import software.amazon.smithy.rust.codegen.core.util.hasTrait +import software.amazon.smithy.rust.codegen.core.util.isEventStream +import software.amazon.smithy.rust.codegen.core.util.isStreaming + +// Part of RFC30 +public object RenderSerdeAttribute { + private const val warningMessage = "/// This data may contain sensitive information; It will not be obscured when serialized.\n" + private const val skipFieldMessage = "/// This field will note be serialized or deserialized because it's a stream type.\n" + + // guards to check if you want to add serde attributes + fun isApplicable(shape: Shape, model: Model): Boolean { + return !shape.hasTrait() && shape.members().none { it.isEventStream(model) } + } + + public fun addSensitiveWarningDoc(writer: RustWriter, shape: Shape, model: Model) { + if (isApplicable(shape, model) && shape.hasTrait()) { + writer.writeInline(warningMessage) + } + } + + public fun addSerdeWithoutShapeModel(writer: RustWriter) { + Attribute("").SerdeSerialize().render(writer) + Attribute("").SerdeDeserialize().render(writer) + } + + public fun addSerde(writer: RustWriter, shape: Shape, model: Model) { + if (isApplicable(shape, model)) { + addSerdeWithoutShapeModel(writer) + } + } + + public fun skipIfStream(writer: RustWriter, member: MemberShape, model: Model, shape: Shape) { + if (isApplicable(shape, model) && member.isStreaming(model)) { + writer.writeInline(skipFieldMessage) + Attribute("").serdeSkip().render(writer) + } + } + + public fun importSerde(writer: RustWriter, shape: Shape, model: Model) { + if (isApplicable(shape, model)) { + // we need this for skip serde to work + Attribute.AllowUnusedImports.render(writer) + Attribute("").SerdeSerializeOrDeserialize().render(writer) + writer.raw("use serde;") + } + } +} diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/StructureGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/StructureGenerator.kt index 61ea75bb35e..2c77cd13bbf 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/StructureGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/StructureGenerator.kt @@ -173,7 +173,9 @@ open class StructureGenerator( memberName: String, memberSymbol: Symbol, ) { + RenderSerdeAttribute.skipIfStream(writer, member, model, shape) writer.renderMemberDoc(member, memberSymbol) + RenderSerdeAttribute.addSensitiveWarningDoc(writer, shape, model) writer.deprecatedShape(member) memberSymbol.expectRustMetadata().render(writer) writer.write("$memberName: #T,", memberSymbol) @@ -184,6 +186,8 @@ open class StructureGenerator( val containerMeta = symbol.expectRustMetadata() writer.documentShape(shape, model) writer.deprecatedShape(shape) + RenderSerdeAttribute.addSerde(writer, shape, model) + RenderSerdeAttribute.addSensitiveWarningDoc(writer, shape, model) containerMeta.render(writer) writer.rustBlock("struct $name ${shape.lifetimeDeclaration(symbolProvider)}") { @@ -193,6 +197,7 @@ open class StructureGenerator( writeCustomizations(customizations, StructureSection.AdditionalFields(shape)) } + RenderSerdeAttribute.importSerde(writer, shape, model) renderStructureImpl() if (!containerMeta.hasDebugDerive()) { renderDebugImpl() diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/UnionGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/UnionGenerator.kt index 0b35ed99488..50c908e8df7 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/UnionGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/UnionGenerator.kt @@ -66,7 +66,7 @@ open class UnionGenerator( open fun render() { writer.documentShape(shape, model) writer.deprecatedShape(shape) - + RenderSerdeAttribute.addSerde(writer, shape, model) val containerMeta = unionSymbol.expectRustMetadata() containerMeta.render(writer) diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/Rust.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/Rust.kt index 2f13c96455f..f8a41c3209e 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/Rust.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/Rust.kt @@ -46,6 +46,89 @@ import java.nio.file.Path import kotlin.io.path.absolutePathString import kotlin.io.path.writeText +// cargo commands and env values +private object Commands { + const val CargoFmt = "cargo fmt" + const val CargoClippy = "cargo clippy" + + private const val cfgUnstable = "--cfg aws_sdk_unstable" + private const val allFeature = "--all-features" + + // test variations + enum class TestVariation { + ALL_FEATURES_WITHOUT_UNSTABLE, + NO_FEATURES_WITHOUT_UNSTABLE, + ALL_FEATURES_WITH_UNSTABLE, + NO_FEATURES_WITH_UNSTABLE, + + val TestModuleDocProvider = + object : ModuleDocProvider { + override fun docsWriter(module: RustModule.LeafModule): Writable = + writable { + docs("Some test documentation\n\nSome more details...") + } + } + + val ALL_VARIATIONS = listOf( + TestVariation.ALL_FEATURES_WITHOUT_UNSTABLE, + TestVariation.NO_FEATURES_WITHOUT_UNSTABLE, + TestVariation.ALL_FEATURES_WITH_UNSTABLE, + TestVariation.NO_FEATURES_WITH_UNSTABLE, + ) + fun decorateCmd(cmd: String, variation: TestVariation): String { + var data = when (variation) { + TestVariation.ALL_FEATURES_WITHOUT_UNSTABLE -> "$cmd $allFeature" + TestVariation.NO_FEATURES_WITHOUT_UNSTABLE -> cmd + TestVariation.ALL_FEATURES_WITH_UNSTABLE -> "$cmd $allFeature $cfgUnstable" + TestVariation.NO_FEATURES_WITH_UNSTABLE -> "$cmd $cfgUnstable" + } + + return data + } + + // helper + private fun func(s: String, add: String, flag: Boolean): String = + if (flag) { + "$s $add" + } else { + s + } + + // unstable flag + fun cargoEnvDenyWarnings(enableUnstable: Boolean): Map { + return mapOf( + "RUSTFLAGS" to func("-D warnings", cfgUnstable, enableUnstable), + ) + } + + fun cargoEnvAllowDeadCode(enableUnstable: Boolean): Map { + return mapOf( + "RUSTFLAGS" to func("-A dead_code", cfgUnstable, enableUnstable), + ) + } + + // enable all features + // e.g. + // ```kotlin + // cargoTest(true) + // // cargo test --all-features + // cargoTest(false) + // // cargo test + // ``` + fun cargoTest(variation: TestVariation): String { + return decorateCmd("cargo test", variation) + } + + // enable all features + // e.g. + // ```kotlin + // cargoCheck(true) + // // cargo test --all-features + // cargoCheck(false) + // // cargo test + // ``` + fun cargoCheck(variation: TestVariation): String { + return decorateCmd("cargo check", variation) val TestModuleDocProvider = object : ModuleDocProvider { override fun docsWriter(module: RustModule.LeafModule): Writable = @@ -54,9 +137,14 @@ val TestModuleDocProvider = } } -/** - * Waiting for Kotlin to stabilize their temp directory functionality - */ +val TestModuleDocProvider = + object : ModuleDocProvider { + override fun docsWriter(module: RustModule.LeafModule): Writable = writable { + docs("Some test documentation\n\nSome more details...") + } + } + +/** Waiting for Kotlin to stabilize their temp directory functionality */ private fun tempDir(directory: File? = null): File { return if (directory != null) { createTempDirectory(directory.toPath(), "smithy-test").toFile() @@ -68,7 +156,8 @@ private fun tempDir(directory: File? = null): File { /** * Creates a Cargo workspace shared among all tests * - * This workspace significantly improves test performance by sharing dependencies between different tests. + * This workspace significantly improves test performance by sharing dependencies between different + * tests. */ object TestWorkspace { private val baseDir by lazy { @@ -143,20 +232,25 @@ object TestWorkspace { FileManifest.create(subprojectDir.toPath()), symbolProvider, codegenConfig, - ).apply { - lib { - // If the test fails before the crate is finalized, we'll end up with a broken crate. - // Since all tests are generated into the same workspace (to avoid re-compilation) a broken crate - // breaks the workspace and all subsequent unit tests. By putting this comment in, we prevent - // that state from occurring. - rust("// touch lib.rs") + ) + .apply { + lib { + // If the test fails before the crate is finalized, we'll end up with a + // broken crate. + // Since all tests are generated into the same workspace (to avoid + // re-compilation) a broken crate + // breaks the workspace and all subsequent unit tests. By putting this + // comment in, we prevent + // that state from occurring. + rust("// touch lib.rs") + } } - } } } /** - * Generates a test plugin context for [model] and returns the plugin context and the path it is rooted it. + * Generates a test plugin context for [model] and returns the plugin context and the path it is + * rooted it. * * Example: * ```kotlin @@ -205,7 +299,8 @@ fun generatePluginContext( } val settings = settingsBuilder.merge(additionalSettings).build() - val pluginContext = PluginContext.builder().model(model).fileManifest(manifest).settings(settings).build() + val pluginContext = + PluginContext.builder().model(model).fileManifest(manifest).settings(settings).build() return pluginContext to testPath } @@ -215,9 +310,7 @@ fun RustWriter.unitTest( ) { val testName = name ?: safeName("test") raw("#[test]") - rustBlock("fn $testName()") { - writeWithNoFormatting(test) - } + rustBlock("fn $testName()") { writeWithNoFormatting(test) } } /* @@ -252,7 +345,9 @@ fun RustWriter.assertNoNewDependencies( val endingDependencies = cargoDependencies().toSet() val newDeps = (endingDependencies - startingDependencies) val invalidDeps = - newDeps.mapNotNull { dep -> dependencyFilter(dep)?.let { message -> message to dep } }.orNullIfEmpty() + newDeps + .mapNotNull { dep -> dependencyFilter(dep)?.let { message -> message to dep } } + .orNullIfEmpty() if (invalidDeps != null) { val badDeps = invalidDeps.map { it.second.rustName } val writtenOut = this.toString() @@ -316,7 +411,8 @@ class TestWriterDelegator( /** * Generate a new test module * - * This should only be used in test code—the generated module name will be something like `tests_123` + * This should only be used in test code—the generated module name will be something like + * `tests_123` */ fun RustCrate.testModule(block: Writable) = lib { @@ -328,15 +424,20 @@ fun RustCrate.testModule(block: Writable) = } fun FileManifest.printGeneratedFiles() { - this.files.forEach { path -> - println("file:///$path") - } + this.files.forEach { path -> println("file:///$path") } } /** - * Setting `runClippy` to true can be helpful when debugging clippy failures, but - * should generally be set to `false` to avoid invalidating the Cargo cache between - * every unit test run. + * Setting `runClippy` to true can be helpful when debugging clippy failures, but should generally + * be set to `false` to avoid invalidating the Cargo cache between every unit test run. If you want + * to enable each features individually, specify the name of the feature on featuresToEnable. e.g. + * ```kotlin + * compileAndTest(featuresToEnable = ["this", "that"]) + * ``` + * All features are enabled by default. If you wish to disable them, set enableAllFeatures to False. + * ```kotlin + * compileAndTest(featuresToEnable = false) + * ``` */ fun TestWriterDelegator.compileAndTest( runClippy: Boolean = false, @@ -358,7 +459,7 @@ fun TestWriterDelegator.compileAndTest( println("Generated files:") printGeneratedFiles() try { - "cargo fmt".runCommand(baseDir) + Commands.CargoFmt.runCommand(baseDir) } catch (e: Exception) { // cargo fmt errors are useless, ignore } @@ -369,18 +470,24 @@ fun TestWriterDelegator.compileAndTest( val env = mapOf("RUSTFLAGS" to "") baseDir.writeDotCargoConfigToml(listOf("--allow", "dead_code")) - val testOutput = "cargo test".runCommand(baseDir, env) + val sep = "\n======================= OUTPUT ===========================\n" + val allOutputs = Commands.ALL_VARIATIONS.map { + Commands.cargoTest(it).runCommand(baseDir, env) + }.map { "$sep $it" }.joinToString() { it } + if (runClippy) { - "cargo clippy --all-features".runCommand(baseDir, env) + Commands.CargoClippy.runCommand(baseDir, env) } - return testOutput + + return allOutputs } fun Path.writeDotCargoConfigToml(rustFlags: List) { val dotCargoDir = this.resolve(".cargo") Files.createDirectory(dotCargoDir) - dotCargoDir.resolve("config.toml") + dotCargoDir + .resolve("config.toml") .writeText( """ [build] @@ -403,12 +510,9 @@ fun String.shouldParseAsRust() { "rustfmt ${tempFile.absolutePath}".runCommand() } -/** - * Compiles the contents of the given writer (including dependencies) and runs the tests - */ +/** Compiles the contents of the given writer (including dependencies) and runs the tests */ fun RustWriter.compileAndTest( - @Language("Rust", prefix = "fn test() {", suffix = "}") - main: String = "", + @Language("Rust", prefix = "fn test() {", suffix = "}") main: String = "", clippy: Boolean = false, expectFailure: Boolean = false, ): String { @@ -440,13 +544,31 @@ fun RustWriter.compileAndTest( if (expectFailure) { println("Test sources for debugging: file://${testModule.absolutePath}") } - return testOutput - } catch (e: CommandError) { - if (!expectFailure) { - println("Test sources for debugging: file://${testModule.absolutePath}") + val tempDir = this.toString().intoCrate(deps, module = module, main = main, strict = clippy) + val mainRs = tempDir.resolve("src/main.rs") + val testModule = tempDir.resolve("src/$module.rs") + + val outputKeep = ArrayList() + for (variation in Commands.ALL_VARIATIONS) { + try { + val testOutput = + if ((mainRs.readText() + testModule.readText()).contains("#[test]")) { + Commands.cargoTest(variation).runCommand(tempDir.toPath()) + } else { + Commands.cargoCheck(variation).runCommand(tempDir.toPath()) + } + outputKeep.add(testOutput) + if (expectFailure) { + println("Test sources for debugging: file://${testModule.absolutePath}") + } + } catch (e: CommandError) { + if (!expectFailure) { + println("Test sources for debugging: file://${testModule.absolutePath}") + } + throw e } - throw e } + return outputKeep.joinToString() { it } } private fun String.intoCrate( @@ -518,8 +640,8 @@ fun String.shouldCompile(): File { } /** - * Inserts the provided strings as a main function and executes the result. This is intended to be used to validate - * that generated code compiles and has some basic properties. + * Inserts the provided strings as a main function and executes the result. This is intended to be + * used to validate that generated code compiles and has some basic properties. * * Example usage: * ``` @@ -541,12 +663,11 @@ fun TestWriterDelegator.unitTest(test: Writable): TestWriterDelegator { lib { val name = safeName("test") withInlineModule(RustModule.inlineTests(name), TestModuleDocProvider) { - unitTest(name) { - test(this) - } + unitTest(name) { test(this) } } } return this } -fun String.runWithWarnings(crate: Path) = this.runCommand(crate, mapOf("RUSTFLAGS" to "-D warnings")) +fun String.runWithWarnings(crate: Path, enableUnstableFlag: Boolean = true) = + this.runCommand(crate, Commands.cargoEnvDenyWarnings(enableUnstableFlag)) diff --git a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/BuilderGeneratorTest.kt b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/BuilderGeneratorTest.kt index 2ea5c56f59e..c65c9077b75 100644 --- a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/BuilderGeneratorTest.kt +++ b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/BuilderGeneratorTest.kt @@ -5,6 +5,7 @@ package software.amazon.smithy.rust.codegen.core.smithy.generators +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.model.Model @@ -25,6 +26,8 @@ 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.testSymbolProvider import software.amazon.smithy.rust.codegen.core.testutil.unitTest +import kotlin.io.path.extension +import kotlin.io.path.readText import software.amazon.smithy.rust.codegen.core.util.lookup internal class BuilderGeneratorTest { @@ -33,6 +36,7 @@ internal class BuilderGeneratorTest { private val struct = StructureGeneratorTest.struct private val credentials = StructureGeneratorTest.credentials private val secretStructure = StructureGeneratorTest.secretStructure + private val errorStruct = StructureGeneratorTest.error @Test fun `generate builders`() { @@ -157,6 +161,30 @@ internal class BuilderGeneratorTest { project.compileAndTest() } + @Test + fun `don't add serde to error types`() { + val provider = testSymbolProvider(model) + val project = TestWorkspace.testProject(provider) + project.moduleFor(errorStruct) { + rust("##![allow(deprecated)]") + StructureGenerator(model, provider, this, errorStruct, emptyList()).render() + implBlock(provider.toSymbol(errorStruct)) { + BuilderGenerator.renderConvenienceMethod(this, provider, errorStruct) + } + } + project.withModule(provider.moduleForBuilder(errorStruct)) { + BuilderGenerator(model, provider, errorStruct, emptyList()).render(this) + } + project.compileAndTest() + + // checks if there is a serde derive in the code + project.generatedFiles().forEach { + if (it.extension == "rs") { + val file = project.baseDir.resolve(it).toFile().readText() + Assertions.assertFalse(file.contains("serde::")) + } + } + @Test fun `it supports nonzero defaults`() { val model = @@ -239,5 +267,5 @@ internal class BuilderGeneratorTest { } } project.compileAndTest() - } + } }