Skip to content

Commit

Permalink
Add initial implementation of a Serde Decorator (#3753)
Browse files Browse the repository at this point in the history
## Motivation and Context
Customers want to be able to use `serde` with Smithy models. For
details, see the
[RFC](https://github.com/smithy-lang/smithy-rs/blob/configurable-serde/design/src/rfcs/rfc0045_configurable_serde.md)

## Description
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here -->
Implementation of `serde::Serialize` for smithy-rs code generators. This
takes the approach of a trait, `SerializeConfigured` which _returns_ a
type that implements `Serialize`. This allows customers to control serde
behavior for their use case.

```rust
/// Trait that allows configuring serialization
/// **This trait should not be implemented directly!** Instead, `impl Serialize for ConfigurableSerdeRef<T>`**
pub trait SerializeConfigured {
    /// Return a `Serialize` implementation for this object that owns the object. This is what you want
    /// If you need to pass something that `impl`s serialize elsewhere.
    fn serialize_owned(self, settings: SerializationSettings) -> impl Serialize;

    /// Return a `Serialize` implementation for this object that borrows from the given object
    fn serialize_ref<'a>(&'a self, settings: &'a SerializationSettings) -> impl Serialize + 'a;
}
```

This can be used as follows:
```rust
serde_json::to_string(&my_struct.serialize_ref(&SerializationSettings::redact_sensitive_fields());
```


Currently, this codegen plugin is not used by anything. It can be
enabled by bringing it in scope during code generation & adding the
`@serde` trait to the model for the fields where serialization is
desired. This will allow the SDK (if they choose to) roll it out only
for specific types or services that have customer demand.

There are a number of follow on items required:
- [x] Ensure the generated impls work for `Server` codegen
- [ ] Generate `Deserialize` implementations
- [x] Test the implementation all Smithy protocol test models (ensure it
compiles)


## Testing
Unit test testing serialization with a kitchen-sink model

## Checklist
<!--- If a checkbox below is not applicable, then please DELETE it
rather than leaving it unchecked -->
- [ ] I have updated `CHANGELOG.next.toml` if I made changes to the
smithy-rs codegen or runtime crates
- [ ] I have updated `CHANGELOG.next.toml` if I made changes to the AWS
SDK, generated SDK code, or SDK runtime crates

----

_By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice._

---------

Co-authored-by: david-perez <[email protected]>
  • Loading branch information
rcoh and david-perez authored Aug 12, 2024
1 parent 40d6fb9 commit a107c01
Show file tree
Hide file tree
Showing 23 changed files with 1,845 additions and 56 deletions.
2 changes: 1 addition & 1 deletion aws/rust-runtime/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

package software.amazon.smithy.rust.codegen.core.smithy

import software.amazon.smithy.codegen.core.Symbol
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.model.shapes.Shape
import software.amazon.smithy.model.shapes.ShapeId
import software.amazon.smithy.rust.codegen.core.smithy.generators.BuilderInstantiator
import software.amazon.smithy.rust.codegen.core.smithy.generators.StructSettings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ class ProtocolFunctions(
}

/** Creates a module name for a ser/de function. */
internal fun RustSymbolProvider.shapeModuleName(
fun RustSymbolProvider.shapeModuleName(
serviceShape: ServiceShape?,
shape: Shape,
): String =
Expand Down
95 changes: 95 additions & 0 deletions codegen-serde/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import org.gradle.api.tasks.testing.logging.TestExceptionFormat

plugins {
kotlin("jvm")
`maven-publish`
}

description = "Plugin to generate `serde` implementations. NOTE: This is separate from generalized serialization."
extra["displayName"] = "Smithy :: Rust :: Codegen Serde"
extra["moduleName"] = "software.amazon.smithy.rust.codegen.client"

group = "software.amazon.smithy.rust.codegen.serde"
version = "0.1.0"

val smithyVersion: String by project

dependencies {
implementation(project(":codegen-core"))
implementation(project(":codegen-client"))
implementation(project(":codegen-server"))
}

java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}

tasks.compileKotlin {
kotlinOptions.jvmTarget = "11"
}

// Reusable license copySpec
val licenseSpec = copySpec {
from("${project.rootDir}/LICENSE")
from("${project.rootDir}/NOTICE")
}

// Configure jars to include license related info
tasks.jar {
metaInf.with(licenseSpec)
inputs.property("moduleName", project.name)
manifest {
attributes["Automatic-Module-Name"] = project.name
}
}

val sourcesJar by tasks.creating(Jar::class) {
group = "publishing"
description = "Assembles Kotlin sources jar"
archiveClassifier.set("sources")
from(sourceSets.getByName("main").allSource)
}

val isTestingEnabled: String by project
if (isTestingEnabled.toBoolean()) {
val kotestVersion: String by project

dependencies {
runtimeOnly(project(":rust-runtime"))
testImplementation("org.junit.jupiter:junit-jupiter:5.6.1")
testImplementation("software.amazon.smithy:smithy-validation-model:$smithyVersion")
testImplementation("software.amazon.smithy:smithy-aws-protocol-tests:$smithyVersion")
testImplementation("io.kotest:kotest-assertions-core-jvm:$kotestVersion")
}

tasks.compileTestKotlin {
kotlinOptions.jvmTarget = "11"
}

tasks.test {
useJUnitPlatform()
testLogging {
events("failed")
exceptionFormat = TestExceptionFormat.FULL
showCauses = true
showExceptions = true
showStackTraces = true
}
}
}

publishing {
publications {
create<MavenPublication>("default") {
from(components["java"])
artifact(sourcesJar)
}
}
repositories { maven { url = uri(layout.buildDirectory.dir("repository")) } }
}
Original file line number Diff line number Diff line change
@@ -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 SerdeModule =
RustModule.public(
"serde",
additionalAttributes = listOf(Attribute.featureGate(SerdeFeature.name)),
documentationOverride = "Implementations of `serde` for model types. NOTE: These implementations are NOT used for wire serialization as part of a Smithy protocol and WILL NOT match the wire format. They are provided for convenience only.",
)

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(SerdeModule) {
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(SerdeModule) {
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<Shape> {
val serviceShape = ctx.serviceShape
val walker = Walker(ctx.model)
return walker.walkShapes(serviceShape).filter { it.hasTrait<SerdeTrait>() }
}
Loading

0 comments on commit a107c01

Please sign in to comment.