diff --git a/codegen-serde/src/main/kotlin/software/amazon/smithy/rust/codegen/serde/SerdeDecorator.kt b/codegen-serde/src/main/kotlin/software/amazon/smithy/rust/codegen/serde/SerdeDecorator.kt new file mode 100644 index 00000000000..18ccc64c21f --- /dev/null +++ b/codegen-serde/src/main/kotlin/software/amazon/smithy/rust/codegen/serde/SerdeDecorator.kt @@ -0,0 +1,80 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.serde + +import software.amazon.smithy.model.neighbor.Walker +import software.amazon.smithy.model.shapes.Shape +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.core.rustlang.Attribute +import software.amazon.smithy.rust.codegen.core.rustlang.Feature +import software.amazon.smithy.rust.codegen.core.rustlang.RustModule +import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext +import software.amazon.smithy.rust.codegen.core.smithy.RustCrate +import software.amazon.smithy.rust.codegen.core.util.hasTrait +import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext +import software.amazon.smithy.rust.codegen.server.smithy.customize.ServerCodegenDecorator + +val SerdeFeature = Feature("serde", false, listOf("dep:serde")) +val Module = + RustModule.public( + "serde_impl", + additionalAttributes = listOf(Attribute.featureGate(SerdeFeature.name)), + documentationOverride = "Implementations of `serde` for model types", + ) + +class ClientSerdeDecorator : ClientCodegenDecorator { + override val name: String = "ClientSerdeDecorator" + override val order: Byte = 0 + + override fun extras( + codegenContext: ClientCodegenContext, + rustCrate: RustCrate, + ) { + rustCrate.mergeFeature(SerdeFeature) + val generator = SerializeImplGenerator(codegenContext) + rustCrate.withModule(Module) { + serializationRoots(codegenContext).forEach { + generator.generateRootSerializerForShape( + it, + )(this) + } + addDependency(SupportStructures.serializeRedacted().toSymbol()) + addDependency(SupportStructures.serializeUnredacted().toSymbol()) + } + } +} + +class ServerSerdeDecorator : ServerCodegenDecorator { + override val name: String = "ServerSerdeDecorator" + override val order: Byte = 0 + + override fun extras( + codegenContext: ServerCodegenContext, + rustCrate: RustCrate, + ) { + rustCrate.mergeFeature(SerdeFeature) + val generator = SerializeImplGenerator(codegenContext) + rustCrate.withModule(Module) { + serializationRoots(codegenContext).forEach { + generator.generateRootSerializerForShape( + it, + )(this) + } + addDependency(SupportStructures.serializeRedacted().toSymbol()) + addDependency(SupportStructures.serializeUnredacted().toSymbol()) + } + } +} + +/** + * All entry points for serialization in the service closure. + */ +fun serializationRoots(ctx: CodegenContext): List { + val serviceShape = ctx.serviceShape + val walker = Walker(ctx.model) + return walker.walkShapes(serviceShape).filter { it.hasTrait() } +} diff --git a/codegen-serde/src/main/kotlin/software/amazon/smithy/rust/codegen/serde/KotlinClientSerdeDecorator.kt b/codegen-serde/src/main/kotlin/software/amazon/smithy/rust/codegen/serde/SerializeImplGenerator.kt similarity index 57% rename from codegen-serde/src/main/kotlin/software/amazon/smithy/rust/codegen/serde/KotlinClientSerdeDecorator.kt rename to codegen-serde/src/main/kotlin/software/amazon/smithy/rust/codegen/serde/SerializeImplGenerator.kt index d8495e648e5..de34a43070b 100644 --- a/codegen-serde/src/main/kotlin/software/amazon/smithy/rust/codegen/serde/KotlinClientSerdeDecorator.kt +++ b/codegen-serde/src/main/kotlin/software/amazon/smithy/rust/codegen/serde/SerializeImplGenerator.kt @@ -6,7 +6,7 @@ package software.amazon.smithy.rust.codegen.serde import software.amazon.smithy.codegen.core.Symbol -import software.amazon.smithy.model.neighbor.Walker +import software.amazon.smithy.model.shapes.BlobShape import software.amazon.smithy.model.shapes.CollectionShape import software.amazon.smithy.model.shapes.DocumentShape import software.amazon.smithy.model.shapes.MapShape @@ -18,13 +18,6 @@ import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.shapes.TimestampShape import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.model.traits.SensitiveTrait -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.core.rustlang.Attribute -import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency -import software.amazon.smithy.rust.codegen.core.rustlang.DependencyScope -import software.amazon.smithy.rust.codegen.core.rustlang.Feature -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.rustlang.Writable import software.amazon.smithy.rust.codegen.core.rustlang.map @@ -36,59 +29,19 @@ import software.amazon.smithy.rust.codegen.core.rustlang.withBlock import software.amazon.smithy.rust.codegen.core.rustlang.writable import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType -import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope -import software.amazon.smithy.rust.codegen.core.smithy.RustCrate import software.amazon.smithy.rust.codegen.core.smithy.contextName import software.amazon.smithy.rust.codegen.core.smithy.generators.UnionGenerator import software.amazon.smithy.rust.codegen.core.smithy.generators.renderUnknownVariant import software.amazon.smithy.rust.codegen.core.smithy.isOptional import software.amazon.smithy.rust.codegen.core.smithy.protocols.shapeFunctionName import software.amazon.smithy.rust.codegen.core.smithy.rustType +import software.amazon.smithy.rust.codegen.core.util.PANIC import software.amazon.smithy.rust.codegen.core.util.dq import software.amazon.smithy.rust.codegen.core.util.hasTrait import software.amazon.smithy.rust.codegen.core.util.isTargetUnit import software.amazon.smithy.rust.codegen.core.util.letIf import software.amazon.smithy.rust.codegen.core.util.toPascalCase -val SerdeFeature = Feature("serde", false, listOf("dep:serde")) -val Module = - RustModule.public( - "serde_impl", - additionalAttributes = listOf(Attribute.featureGate(SerdeFeature.name)), - documentationOverride = "Implementations of `serde` for model types", - ) - -class KotlinClientSerdeDecorator : ClientCodegenDecorator { - override val name: String = "ClientSerdeDecorator" - override val order: Byte = 0 - - override fun extras( - codegenContext: ClientCodegenContext, - rustCrate: RustCrate, - ) { - rustCrate.mergeFeature(SerdeFeature) - val generator = SerializeImplGenerator(codegenContext) - rustCrate.withModule(Module) { - serializationRoots(codegenContext).forEach { - generator.generateRootSerializerForShape( - it, - )(this) - } - addDependency(SupportStructures.serializeRedacted().toSymbol()) - addDependency(SupportStructures.serializeUnredacted().toSymbol()) - } - } -} - -/** - * All entry points for serialization in the service closure. - */ -fun serializationRoots(ctx: CodegenContext): List { - val serviceShape = ctx.serviceShape - val walker = Walker(ctx.model) - return walker.walkShapes(serviceShape).filter { it.hasTrait() } -} - class SerializeImplGenerator(private val codegenContext: CodegenContext) { fun generateRootSerializerForShape(shape: Shape): Writable = serializerFn(shape, null) @@ -111,6 +64,7 @@ class SerializeImplGenerator(private val codegenContext: CodegenContext) { is StructureShape -> RuntimeType.forInlineFun(name, Module, structSerdeImpl(shape)) is UnionShape -> RuntimeType.forInlineFun(name, Module, serializeUnionImpl(shape)) is TimestampShape -> serializeDateTime(shape) + is BlobShape -> serializeBlob(shape) is StringShape, is NumberShape -> directSerde(shape) is DocumentShape -> serializeDocument(shape) else -> null @@ -122,7 +76,7 @@ class SerializeImplGenerator(private val codegenContext: CodegenContext) { shape is MapShape -> serializeMap(shape) shape is CollectionShape -> serializeList(shape) // Need to figure out the best default here. - else -> serializeWithTodo(shape) + else -> PANIC("No serializer supported for $shape") } if (wrapper != null && applyTo != null) { rustTemplate("&#{wrapper}(#{applyTo})", "wrapper" to wrapper, "applyTo" to applyTo) @@ -133,14 +87,6 @@ class SerializeImplGenerator(private val codegenContext: CodegenContext) { } } - private fun serializeWithTodo(shape: Shape): RuntimeType = - serializeWithWrapper(shape) { _ -> - // PANIC("cant serialize $shape") - writable { - rust("serializer.serialize_str(\"todo\")") - } - } - private fun serializeMap(shape: MapShape): RuntimeType = serializeWithWrapper(shape) { value -> writable { @@ -299,15 +245,9 @@ class SerializeImplGenerator(private val codegenContext: CodegenContext) { ) } if (codegenContext.symbolProvider.toSymbol(member).isOptional()) { - rustTemplate( - "if let Some($field) = &inner.$fieldName { #{serializeField} }", - "serializeField" to fieldSerialization, - ) + rust("if let Some($field) = &inner.$fieldName { #T }", fieldSerialization) } else { - rustTemplate( - "let $field = &inner.$fieldName; #{serializeField}", - "serializeField" to fieldSerialization, - ) + rust("let $field = &inner.$fieldName; #T", fieldSerialization) } } rust("s.end()") @@ -362,6 +302,22 @@ class SerializeImplGenerator(private val codegenContext: CodegenContext) { } } + private fun serializeBlob(shape: BlobShape): RuntimeType = + RuntimeType.forInlineFun("SerializeBlob", Module) { + implSerializeConfigured(codegenContext.symbolProvider.toSymbol(shape)) { + rustTemplate( + """ + if serializer.is_human_readable() { + serializer.serialize_str(&#{base64_encode}(&self.value.as_ref())) + } else { + serializer.serialize_bytes(&self.value.as_ref()) + } + """, + "base64_encode" to RuntimeType.base64Encode(codegenContext.runtimeConfig), + ) + } + } + private fun serializeDocument(shape: DocumentShape): RuntimeType = RuntimeType.forInlineFun("SerializeDocument", Module) { implSerializeConfigured(codegenContext.symbolProvider.toSymbol(shape)) { @@ -444,211 +400,3 @@ class SerializeImplGenerator(private val codegenContext: CodegenContext) { } } } - -object SupportStructures { - private val supportModule = - RustModule.public("support", Module, documentationOverride = "Support traits and structures for serde") - - private val serde = CargoDependency.Serde.copy(scope = DependencyScope.Compile, optional = true).toType() - - val codegenScope = - arrayOf( - *preludeScope, - "ConfigurableSerde" to configurableSerde(), - "SerializeConfigured" to serializeConfigured(), - "ConfigurableSerdeRef" to configurableSerdeRef(), - "SerializationSettings" to serializationSettings(), - "Sensitive" to sensitive(), - "serde" to serde, - "serialize_redacted" to serializeRedacted(), - "serialize_unredacted" to serializeUnredacted(), - ) - - fun serializeRedacted(): RuntimeType = - RuntimeType.forInlineFun("serialize_redacted", supportModule) { - rustTemplate( - """ - /// Serialize a value redacting sensitive fields - /// - /// This function is intended to be used by `serde(serialize_with = "serialize_redacted")` - pub fn serialize_redacted<'a, T, S: #{serde}::Serializer>(value: &'a T, serializer: S) -> Result - where - T: #{SerializeConfigured}, - { - use #{serde}::Serialize; - value - .serialize_ref(&#{SerializationSettings} { redact_sensitive_fields: true }) - .serialize(serializer) - } - """, - "serde" to serde, - "SerializeConfigured" to serializeConfigured(), - "SerializationSettings" to serializationSettings(), - ) - } - - fun serializeUnredacted(): RuntimeType = - RuntimeType.forInlineFun("serialize_unredacted", supportModule) { - rustTemplate( - """ - /// Serialize a value without redacting sensitive fields - /// - /// This function is intended to be used by `serde(serialize_with = "serialize_unredacted")` - pub fn serialize_unredacted<'a, T, S: #{serde}::Serializer>(value: &'a T, serializer: S) -> Result - where - T: #{SerializeConfigured}, - { - use #{serde}::Serialize; - value - .serialize_ref(&#{SerializationSettings} { redact_sensitive_fields: false }) - .serialize(serializer) - } - """, - "serde" to serde, - "SerializeConfigured" to serializeConfigured(), - "SerializationSettings" to serializationSettings(), - ) - } - - private fun serializeConfigured(): RuntimeType = - RuntimeType.forInlineFun("SerializeConfigured", supportModule) { - rustTemplate( - """ - /// Trait that allows configuring serialization - /// **This trait should not be implemented directly!** Instead, `impl Serialize for ConfigurableSerdeRef` - pub trait SerializeConfigured { - /// Return a `Serialize` implementation for this object that owns the object. - /// - /// Use this if you need to create `Arc` or similar. - fn serialize_owned(self, settings: #{SerializationSettings}) -> impl #{serde}::Serialize; - - /// Return a `Serialize` implementation for this object that borrows from the given object - fn serialize_ref<'a>(&'a self, settings: &'a #{SerializationSettings}) -> impl #{serde}::Serialize + 'a; - } - - /// Blanket implementation for all `T` that implement `ConfigurableSerdeRef` - impl SerializeConfigured for T - where - for<'a> #{ConfigurableSerdeRef}<'a, T>: #{serde}::Serialize, - { - fn serialize_owned( - self, - settings: #{SerializationSettings}, - ) -> impl #{serde}::Serialize { - #{ConfigurableSerde} { - value: self, - settings, - } - } - - fn serialize_ref<'a>( - &'a self, - settings: &'a #{SerializationSettings}, - ) -> impl #{serde}::Serialize + 'a { - #{ConfigurableSerdeRef} { value: self, settings } - } - } - """, - "ConfigurableSerde" to configurableSerde(), - "ConfigurableSerdeRef" to configurableSerdeRef(), - "SerializationSettings" to serializationSettings(), - "serde" to serde, - ) - } - - private fun sensitive() = - RuntimeType.forInlineFun("Sensitive", supportModule) { - rustTemplate( - """ - pub(crate) struct Sensitive(pub(crate) T); - - impl<'a, T> ::serde::Serialize for ConfigurableSerdeRef<'a, Sensitive> - where - T: #{serde}::Serialize, - { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self.settings.redact_sensitive_fields { - true => serializer.serialize_str(""), - false => self.value.0.serialize(serializer), - } - } - } - """, - "serde" to CargoDependency.Serde.toType(), - ) - } - - private fun configurableSerde() = - RuntimeType.forInlineFun("ConfigurableSerde", supportModule) { - rustTemplate( - """ - ##[allow(missing_docs)] - pub(crate) struct ConfigurableSerde { - pub(crate) value: T, - pub(crate) settings: #{SerializationSettings} - } - - impl #{serde}::Serialize for ConfigurableSerde where for <'a> ConfigurableSerdeRef<'a, T>: #{serde}::Serialize { - fn serialize(&self, serializer: S) -> Result - where - S: #{serde}::Serializer, - { - #{ConfigurableSerdeRef} { - value: &self.value, - settings: &self.settings, - } - .serialize(serializer) - } - } - - """, - "SerializationSettings" to serializationSettings(), - "ConfigurableSerdeRef" to configurableSerdeRef(), - "serde" to CargoDependency.Serde.toType(), - ) - } - - private fun configurableSerdeRef() = - RuntimeType.forInlineFun("ConfigurableSerdeRef", supportModule) { - rustTemplate( - """ - ##[allow(missing_docs)] - pub(crate) struct ConfigurableSerdeRef<'a, T> { - pub(crate) value: &'a T, - pub(crate) settings: &'a SerializationSettings - } - """, - ) - } - - private fun serializationSettings() = - RuntimeType.forInlineFun("SerializationSettings", supportModule) { - // TODO(serde): Add a builder for this structure and make it non-exhaustive - // TODO(serde): Consider removing `derive(Default)` - rustTemplate( - """ - /// Settings for use when serializing structures - ##[non_exhaustive] - ##[derive(Copy, Clone, Debug, Default)] - pub struct SerializationSettings { - /// Replace all sensitive fields with `` during serialization - pub redact_sensitive_fields: bool, - } - - impl SerializationSettings { - /// Replace all `@sensitive` fields with `` when serializing. - /// - /// Note: This may alter the type of the serialized output and make it impossible to deserialize as - /// numerical fields will be replaced with strings. - pub fn redact_sensitive_fields() -> Self { Self { redact_sensitive_fields: true } } - - /// Preserve the contents of sensitive fields during serializing - pub fn leak_sensitive_fields() -> Self { Self { redact_sensitive_fields: false } } - } - """, - ) - } -} diff --git a/codegen-serde/src/main/kotlin/software/amazon/smithy/rust/codegen/serde/SupportStructures.kt b/codegen-serde/src/main/kotlin/software/amazon/smithy/rust/codegen/serde/SupportStructures.kt new file mode 100644 index 00000000000..fa0c650d590 --- /dev/null +++ b/codegen-serde/src/main/kotlin/software/amazon/smithy/rust/codegen/serde/SupportStructures.kt @@ -0,0 +1,235 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.serde + +import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency +import software.amazon.smithy.rust.codegen.core.rustlang.DependencyScope +import software.amazon.smithy.rust.codegen.core.rustlang.RustModule +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType + +object SupportStructures { + private val supportModule = + RustModule.public("support", Module, documentationOverride = "Support traits and structures for serde") + + private val serde = CargoDependency.Serde.copy(scope = DependencyScope.Compile, optional = true).toType() + + val codegenScope = + arrayOf( + *RuntimeType.preludeScope, + "ConfigurableSerde" to configurableSerde(), + "SerializeConfigured" to serializeConfigured(), + "ConfigurableSerdeRef" to configurableSerdeRef(), + "SerializationSettings" to serializationSettings(), + "Sensitive" to sensitive(), + "serde" to serde, + "serialize_redacted" to serializeRedacted(), + "serialize_unredacted" to serializeUnredacted(), + ) + + fun serializeRedacted(): RuntimeType = + RuntimeType.forInlineFun("serialize_redacted", supportModule) { + rustTemplate( + """ + /// Serialize a value redacting sensitive fields + /// + /// This function is intended to be used by `serde(serialize_with = "serialize_redacted")` + pub fn serialize_redacted<'a, T, S: #{serde}::Serializer>(value: &'a T, serializer: S) -> Result + where + T: #{SerializeConfigured}, + { + use #{serde}::Serialize; + value + .serialize_ref(&#{SerializationSettings} { redact_sensitive_fields: true }) + .serialize(serializer) + } + """, + "serde" to serde, + "SerializeConfigured" to serializeConfigured(), + "SerializationSettings" to serializationSettings(), + ) + } + + fun serializeUnredacted(): RuntimeType = + RuntimeType.forInlineFun("serialize_unredacted", supportModule) { + rustTemplate( + """ + /// Serialize a value without redacting sensitive fields + /// + /// This function is intended to be used by `serde(serialize_with = "serialize_unredacted")` + pub fn serialize_unredacted<'a, T, S: #{serde}::Serializer>(value: &'a T, serializer: S) -> Result + where + T: #{SerializeConfigured}, + { + use #{serde}::Serialize; + value + .serialize_ref(&#{SerializationSettings} { redact_sensitive_fields: false }) + .serialize(serializer) + } + """, + "serde" to serde, + "SerializeConfigured" to serializeConfigured(), + "SerializationSettings" to serializationSettings(), + ) + } + + private fun serializeConfigured(): RuntimeType = + RuntimeType.forInlineFun("SerializeConfigured", supportModule) { + rustTemplate( + """ + /// Trait that allows configuring serialization + /// **This trait should not be implemented directly!** Instead, `impl Serialize for ConfigurableSerdeRef` + pub trait SerializeConfigured { + /// Return a `Serialize` implementation for this object that owns the object. + /// + /// Use this if you need to create `Arc` or similar. + fn serialize_owned(self, settings: #{SerializationSettings}) -> impl #{serde}::Serialize; + + /// Return a `Serialize` implementation for this object that borrows from the given object + fn serialize_ref<'a>(&'a self, settings: &'a #{SerializationSettings}) -> impl #{serde}::Serialize + 'a; + } + + /// Blanket implementation for all `T` that implement `ConfigurableSerdeRef` + impl SerializeConfigured for T + where + for<'a> #{ConfigurableSerdeRef}<'a, T>: #{serde}::Serialize, + { + fn serialize_owned( + self, + settings: #{SerializationSettings}, + ) -> impl #{serde}::Serialize { + #{ConfigurableSerde} { + value: self, + settings, + } + } + + fn serialize_ref<'a>( + &'a self, + settings: &'a #{SerializationSettings}, + ) -> impl #{serde}::Serialize + 'a { + #{ConfigurableSerdeRef} { value: self, settings } + } + } + """, + "ConfigurableSerde" to configurableSerde(), + "ConfigurableSerdeRef" to configurableSerdeRef(), + "SerializationSettings" to serializationSettings(), + "serde" to serde, + ) + } + + private fun sensitive() = + RuntimeType.forInlineFun("Sensitive", supportModule) { + rustTemplate( + """ + pub(crate) struct Sensitive(pub(crate) T); + + impl<'a, T> ::serde::Serialize for ConfigurableSerdeRef<'a, Sensitive> + where + T: #{serde}::Serialize, + { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self.settings.redact_sensitive_fields { + true => serializer.serialize_str(""), + false => self.value.0.serialize(serializer), + } + } + } + """, + "serde" to CargoDependency.Serde.toType(), + ) + } + + private fun configurableSerde() = + RuntimeType.forInlineFun("ConfigurableSerde", supportModule) { + rustTemplate( + """ + ##[allow(missing_docs)] + pub(crate) struct ConfigurableSerde { + pub(crate) value: T, + pub(crate) settings: #{SerializationSettings} + } + + impl #{serde}::Serialize for ConfigurableSerde where for <'a> ConfigurableSerdeRef<'a, T>: #{serde}::Serialize { + fn serialize(&self, serializer: S) -> Result + where + S: #{serde}::Serializer, + { + #{ConfigurableSerdeRef} { + value: &self.value, + settings: &self.settings, + } + .serialize(serializer) + } + } + impl<'a, T> #{serde}::Serialize for #{ConfigurableSerdeRef}<'a, Option> + where + T: #{SerializeConfigured}, + { + fn serialize(&self, serializer: S) -> Result + where + S: #{serde}::Serializer, + { + match self.value { + Some(value) => serializer.serialize_some(&value.serialize_ref(self.settings)), + None => serializer.serialize_none(), + } + } + } + + """, + "SerializationSettings" to serializationSettings(), + "ConfigurableSerdeRef" to configurableSerdeRef(), + "SerializeConfigured" to serializeConfigured(), + "serde" to CargoDependency.Serde.toType(), + ) + } + + private fun configurableSerdeRef() = + RuntimeType.forInlineFun("ConfigurableSerdeRef", supportModule) { + rustTemplate( + """ + ##[allow(missing_docs)] + pub(crate) struct ConfigurableSerdeRef<'a, T> { + pub(crate) value: &'a T, + pub(crate) settings: &'a SerializationSettings + } + """, + ) + } + + private fun serializationSettings() = + RuntimeType.forInlineFun("SerializationSettings", supportModule) { + // TODO(serde): Add a builder for this structure and make it non-exhaustive + // TODO(serde): Consider removing `derive(Default)` + rustTemplate( + """ + /// Settings for use when serializing structures + ##[non_exhaustive] + ##[derive(Copy, Clone, Debug, Default)] + pub struct SerializationSettings { + /// Replace all sensitive fields with `` during serialization + pub redact_sensitive_fields: bool, + } + + impl SerializationSettings { + /// Replace all `@sensitive` fields with `` when serializing. + /// + /// Note: This may alter the type of the serialized output and make it impossible to deserialize as + /// numerical fields will be replaced with strings. + pub fn redact_sensitive_fields() -> Self { Self { redact_sensitive_fields: true } } + + /// Preserve the contents of sensitive fields during serializing + pub fn leak_sensitive_fields() -> Self { Self { redact_sensitive_fields: false } } + } + """, + ) + } +} diff --git a/codegen-serde/src/main/resources/META-INF/services/software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator b/codegen-serde/src/main/resources/META-INF/services/software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator index 3fb941ca267..18732561a71 100644 --- a/codegen-serde/src/main/resources/META-INF/services/software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator +++ b/codegen-serde/src/main/resources/META-INF/services/software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator @@ -1,4 +1,4 @@ # This file lists the names of classes which implement extensions to the rust-client-codegen extension # The source code we are extending is found in https://github.com/smithy-lang/smithy-rs/blob/d5ea2cdd91e9b74232e08bd3137e3cd6aa2c3d01/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/ClientCodegenDecorator.kt -software.amazon.smithy.rust.codegen.serde.KotlinClientSerdeDecorator +software.amazon.smithy.rust.codegen.serde.ClientSerdeDecorator diff --git a/codegen-serde/src/main/resources/META-INF/services/software.amazon.smithy.rust.codegen.server.smithy.customize.ServerCodegenDecorator b/codegen-serde/src/main/resources/META-INF/services/software.amazon.smithy.rust.codegen.server.smithy.customize.ServerCodegenDecorator new file mode 100644 index 00000000000..a424b5a95e1 --- /dev/null +++ b/codegen-serde/src/main/resources/META-INF/services/software.amazon.smithy.rust.codegen.server.smithy.customize.ServerCodegenDecorator @@ -0,0 +1,4 @@ +# This file lists the names of classes which implement extensions to the rust-server-codegen extension +# The source code we are extending is found in https://github.com/awslabs/smithy-rs/blob/release-2023-04-26/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customize/ServerCodegenDecorator.kt + +software.amazon.smithy.rust.codegen.serde.ServerSerdeDecorator diff --git a/codegen-serde/src/test/kotlin/software/amazon/smithy/rust/codegen/serde/KotlinClientSerdeDecoratorTest.kt b/codegen-serde/src/test/kotlin/software/amazon/smithy/rust/codegen/serde/KotlinClientSerdeDecoratorTest.kt deleted file mode 100644 index 0f4f38670a4..00000000000 --- a/codegen-serde/src/test/kotlin/software/amazon/smithy/rust/codegen/serde/KotlinClientSerdeDecoratorTest.kt +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.rust.codegen.serde - -import org.junit.jupiter.api.Test -import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest -import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency -import software.amazon.smithy.rust.codegen.core.rustlang.CratesIo -import software.amazon.smithy.rust.codegen.core.rustlang.RustType -import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate -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.unitTest - -class KotlinClientSerdeDecoratorTest { - private val simpleModel = - """ - namespace com.example - use smithy.rust#serde - use aws.protocols#awsJson1_0 - use smithy.framework#ValidationException - @awsJson1_0 - service HelloService { - operations: [SayHello, SayGoodBye], - version: "1" - } - @optionalAuth - operation SayHello { - input: TestInput - errors: [ValidationException] - } - @serde - structure TestInput { - foo: SensitiveString, - e: TestEnum, - nested: Nested, - union: U, - document: Document - } - - @sensitive - string SensitiveString - - @sensitive - enum TestEnum { - A, - B, - C, - D - } - - @sensitive - union U { - nested: Nested, - enum: TestEnum - } - - structure Nested { - @required - int: Integer, - sensitive: Timestamps, - notSensitive: AlsoTimestamps, - manyEnums: TestEnumList - } - - list TestEnumList { - member: TestEnum - } - - map Timestamps { - key: String - value: SensitiveTimestamp - } - - map AlsoTimestamps { - key: String - value: Timestamp - } - - @sensitive - timestamp SensitiveTimestamp - - operation SayGoodBye { - input: NotSerde - } - structure NotSerde {} - """.asSmithyModel(smithyVersion = "2") - - @Test - fun generateSerializersThatWorkClient() { - clientIntegrationTest(simpleModel, params = IntegrationTestParams(cargoCommand = "cargo test --all-features")) { ctx, crate -> - val codegenScope = - arrayOf( - "crate" to RustType.Opaque(ctx.moduleUseName()), - "serde_json" to CargoDependency("serde_json", CratesIo("1")).toDevDependency().toType(), - ) - - crate.integrationTest("test_serde") { - unitTest("input_serialized") { - rustTemplate( - """ - use #{crate}::types::{Nested, U}; - use #{crate}::serde_impl::support::*; - use std::time::UNIX_EPOCH; - use aws_smithy_types::{DateTime, Document}; - let input = #{crate}::operation::say_hello::SayHelloInput::builder() - .foo("foo-value") - .e("A".into()) - .document(Document::String("hello!".into())) - .nested(Nested::builder() - .int(5) - .sensitive("a", DateTime::from(UNIX_EPOCH)) - .not_sensitive("a", DateTime::from(UNIX_EPOCH)) - .many_enums("A".into()) - .build().unwrap() - ) - .union(U::Enum("B".into())) - .build() - .unwrap(); - let mut settings = #{crate}::serde_impl::support::SerializationSettings::default(); - let serialized = #{serde_json}::to_string(&input.serialize_ref(&settings)).expect("failed to serialize"); - assert_eq!(serialized, "{\"foo\":\"foo-value\",\"e\":\"A\",\"nested\":{\"int\":5,\"sensitive\":{\"a\":\"1970-01-01T00:00:00Z\"},\"notSensitive\":{\"a\":\"1970-01-01T00:00:00Z\"},\"manyEnums\":[\"A\"]},\"union\":{\"enum\":\"B\"},\"document\":\"hello!\"}"); - settings.redact_sensitive_fields = true; - let serialized = #{serde_json}::to_string(&input.serialize_ref(&settings)).expect("failed to serialize"); - assert_eq!(serialized, "{\"foo\":\"\",\"e\":\"\",\"nested\":{\"int\":5,\"sensitive\":{\"a\":\"\"},\"notSensitive\":{\"a\":\"1970-01-01T00:00:00Z\"},\"manyEnums\":[\"\"]},\"union\":\"\",\"document\":\"hello!\"}"); - """, - *codegenScope, - ) - } - - unitTest("delegated_serde") { - rustTemplate( - """ - use #{crate}::operation::say_hello::SayHelloInput; - use #{crate}::serde_impl::support::*; - ##[derive(serde::Serialize)] - struct MyRecord { - ##[serde(serialize_with = "serialize_redacted")] - redact_field: SayHelloInput, - ##[serde(serialize_with = "serialize_unredacted")] - unredacted_field: SayHelloInput - } - let input = SayHelloInput::builder().foo("foo-value").build().unwrap(); - - let field = MyRecord { - redact_field: input.clone(), - unredacted_field: input - }; - let serialized = #{serde_json}::to_string(&field).expect("failed to serialize"); - assert_eq!(serialized, r##"{"redact_field":{"foo":""},"unredacted_field":{"foo":"foo-value"}}"##); - """, - *codegenScope, - ) - } - } - } - } -} diff --git a/codegen-serde/src/test/kotlin/software/amazon/smithy/rust/codegen/serde/SerdeDecoratorTest.kt b/codegen-serde/src/test/kotlin/software/amazon/smithy/rust/codegen/serde/SerdeDecoratorTest.kt new file mode 100644 index 00000000000..6ee25f190b3 --- /dev/null +++ b/codegen-serde/src/test/kotlin/software/amazon/smithy/rust/codegen/serde/SerdeDecoratorTest.kt @@ -0,0 +1,298 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.serde + +import org.junit.jupiter.api.Test +import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest +import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency +import software.amazon.smithy.rust.codegen.core.rustlang.CratesIo +import software.amazon.smithy.rust.codegen.core.rustlang.RustType +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +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.unitTest +import software.amazon.smithy.rust.codegen.core.util.dq +import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverIntegrationTest + +class SerdeDecoratorTest { + private val simpleModel = + """ + namespace com.example + use smithy.rust#serde + use aws.protocols#awsJson1_0 + use smithy.framework#ValidationException + @awsJson1_0 + service HelloService { + operations: [SayHello, SayGoodBye], + version: "1" + } + @optionalAuth + operation SayHello { + input: TestInput + errors: [ValidationException] + } + @serde + structure TestInput { + foo: SensitiveString, + e: TestEnum, + nested: Nested, + union: U, + document: Document, + blob: SensitiveBlob + } + + @sensitive + blob SensitiveBlob + + @sensitive + string SensitiveString + + @sensitive + enum TestEnum { + A, + B, + C, + D + } + + @sensitive + union U { + nested: Nested, + enum: TestEnum + } + + structure Nested { + @required + int: Integer, + sensitive: Timestamps, + notSensitive: AlsoTimestamps, + manyEnums: TestEnumList, + sparse: SparseList + } + + list TestEnumList { + member: TestEnum + } + + map Timestamps { + key: String + value: SensitiveTimestamp + } + + map AlsoTimestamps { + key: String + value: Timestamp + } + + @sensitive + timestamp SensitiveTimestamp + + @sparse + list SparseList { + member: TestEnum + } + + operation SayGoodBye { + input: NotSerde + } + structure NotSerde {} + """.asSmithyModel(smithyVersion = "2") + + @Test + fun generateSerializersThatWorkServer() { + serverIntegrationTest(simpleModel, params = IntegrationTestParams(cargoCommand = "cargo test --all-features")) { ctx, crate -> + val codegenScope = + arrayOf( + "crate" to RustType.Opaque(ctx.moduleUseName()), + "serde_json" to CargoDependency("serde_json", CratesIo("1")).toDevDependency().toType(), + ) + + crate.integrationTest("test_serde") { + unitTest("input_serialized") { + rustTemplate( + """ + use #{crate}::model::{Nested, U, TestEnum}; + use #{crate}::input::SayHelloInput; + use #{crate}::serde_impl::support::*; + use std::collections::HashMap; + use std::time::UNIX_EPOCH; + use aws_smithy_types::{DateTime, Document, Blob}; + let sensitive_map = HashMap::from([("a".to_string(), DateTime::from(UNIX_EPOCH))]); + let input = SayHelloInput::builder() + .foo(Some("foo-value".to_string())) + .e(Some(TestEnum::A)) + .document(Some(Document::String("hello!".into()))) + .blob(Some(Blob::new("hello"))) + .nested(Some(Nested::builder() + .int(5) + .sensitive(Some(sensitive_map.clone())) + .not_sensitive(Some(sensitive_map)) + .many_enums(Some(vec![TestEnum::A])) + .sparse(Some(vec![None, Some(TestEnum::A), Some(TestEnum::B)])) + .build().unwrap() + )) + .union(Some(U::Enum(TestEnum::B))) + .build() + .unwrap(); + let mut settings = #{crate}::serde_impl::support::SerializationSettings::default(); + let serialized = #{serde_json}::to_string(&input.serialize_ref(&settings)).expect("failed to serialize"); + assert_eq!(serialized, ${expectedNoRedactions.dq()}); + settings.redact_sensitive_fields = true; + let serialized = #{serde_json}::to_string(&input.serialize_ref(&settings)).expect("failed to serialize"); + assert_eq!(serialized, ${expectedRedacted.dq()}); + """, + *codegenScope, + ) + } + + unitTest("delegated_serde") { + rustTemplate( + """ + use #{crate}::input::SayHelloInput; + use #{crate}::serde_impl::support::*; + ##[derive(serde::Serialize)] + struct MyRecord { + ##[serde(serialize_with = "serialize_redacted")] + redact_field: SayHelloInput, + ##[serde(serialize_with = "serialize_unredacted")] + unredacted_field: SayHelloInput + } + let input = SayHelloInput::builder().foo(Some("foo-value".to_string())).build().unwrap(); + + let field = MyRecord { + redact_field: input.clone(), + unredacted_field: input + }; + let serialized = #{serde_json}::to_string(&field).expect("failed to serialize"); + assert_eq!(serialized, r##"{"redact_field":{"foo":""},"unredacted_field":{"foo":"foo-value"}}"##); + """, + *codegenScope, + ) + } + } + } + } + + private val expectedNoRedactions = + """{ + "foo": "foo-value", + "e": "A", + "nested": { + "int": 5, + "sensitive": { + "a": "1970-01-01T00:00:00Z" + }, + "notSensitive": { + "a": "1970-01-01T00:00:00Z" + }, + "manyEnums": [ + "A" + ], + "sparse": [null, "A", "B"] + }, + "union": { + "enum": "B" + }, + "document": "hello!", + "blob": "aGVsbG8=" + }""".replace("\\s".toRegex(), "") + + private val expectedRedacted = + """{ + "foo": "", + "e": "", + "nested": { + "int": 5, + "sensitive": { + "a": "" + }, + "notSensitive": { + "a": "1970-01-01T00:00:00Z" + }, + "manyEnums": [ + "" + ], + "sparse": ["", "", ""] + }, + "union": "", + "document": "hello!", + "blob": "" + } + """.replace("\\s".toRegex(), "") + + @Test + fun generateSerializersThatWorkClient() { + clientIntegrationTest(simpleModel, params = IntegrationTestParams(cargoCommand = "cargo test --all-features")) { ctx, crate -> + val codegenScope = + arrayOf( + "crate" to RustType.Opaque(ctx.moduleUseName()), + "serde_json" to CargoDependency("serde_json", CratesIo("1")).toDevDependency().toType(), + ) + + crate.integrationTest("test_serde") { + unitTest("input_serialized") { + rustTemplate( + """ + use #{crate}::types::{Nested, U, TestEnum}; + use #{crate}::serde_impl::support::*; + use std::time::UNIX_EPOCH; + use aws_smithy_types::{DateTime, Document, Blob}; + let input = #{crate}::operation::say_hello::SayHelloInput::builder() + .foo("foo-value") + .e("A".into()) + .document(Document::String("hello!".into())) + .blob(Blob::new("hello")) + .nested(Nested::builder() + .int(5) + .sensitive("a", DateTime::from(UNIX_EPOCH)) + .not_sensitive("a", DateTime::from(UNIX_EPOCH)) + .many_enums("A".into()) + .sparse(None).sparse(Some(TestEnum::A)).sparse(Some(TestEnum::B)) + .build().unwrap() + ) + .union(U::Enum("B".into())) + .build() + .unwrap(); + let mut settings = #{crate}::serde_impl::support::SerializationSettings::default(); + let serialized = #{serde_json}::to_string(&input.serialize_ref(&settings)).expect("failed to serialize"); + assert_eq!(serialized, ${expectedNoRedactions.dq()}); + settings.redact_sensitive_fields = true; + let serialized = #{serde_json}::to_string(&input.serialize_ref(&settings)).expect("failed to serialize"); + assert_eq!(serialized, ${expectedRedacted.dq()}); + """, + *codegenScope, + ) + } + + unitTest("delegated_serde") { + rustTemplate( + """ + use #{crate}::operation::say_hello::SayHelloInput; + use #{crate}::serde_impl::support::*; + ##[derive(serde::Serialize)] + struct MyRecord { + ##[serde(serialize_with = "serialize_redacted")] + redact_field: SayHelloInput, + ##[serde(serialize_with = "serialize_unredacted")] + unredacted_field: SayHelloInput + } + let input = SayHelloInput::builder().foo("foo-value").build().unwrap(); + + let field = MyRecord { + redact_field: input.clone(), + unredacted_field: input + }; + let serialized = #{serde_json}::to_string(&field).expect("failed to serialize"); + assert_eq!(serialized, r##"{"redact_field":{"foo":""},"unredacted_field":{"foo":"foo-value"}}"##); + """, + *codegenScope, + ) + } + } + } + } +}