Repository: smithy-lang/smithy-rs (language: rust)

Issue:
Add Accessors to Builders
Currently, there is no way to get the information in a Builder while creating a request. It would be nice to have some way to get that information. This would be helpful in testing, to be able to verify the contents of a Builder without mocking the request. It may also be useful for implementing a multi stage request, where one builder is passed through several functions that make changes to the request based on prior information.

For an example of a test that would be improved by this pattern, [see this commit](https://github.com/DavidSouther/aws-doc-sdk-examples/commit/017e8a2e407eba6a4c259bca0334f1356ab822d9).


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml
index aec1605b37..c1389a7463 100644
--- a/CHANGELOG.next.toml
+++ b/CHANGELOG.next.toml
@@ -11,6 +11,18 @@
 # meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client | server | all"}
 # author = "rcoh"
 
+ [[aws-sdk-rust]]
+ message = "Add accessors to Builders"
+ references = ["smithy-rs#2791"]
+ meta = { "breaking" = false, "tada" = false, "bug" = false }
+ author = "davidsouther"
+
+ [[smithy-rs]]
+ message = "Add accessors to Builders"
+ references = ["smithy-rs#2791"]
+ meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client"}
+ author = "davidsouther"
+
 [[smithy-rs]]
 message = "Avoid intermediate vec allocations in AggregatedBytes::to_vec."
 author = "yotamofek"
diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientCore.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientCore.kt
index fb9750119b..3073413efe 100644
--- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientCore.kt
+++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientCore.kt
@@ -15,6 +15,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.docs
 import software.amazon.smithy.rust.codegen.core.rustlang.documentShape
 import software.amazon.smithy.rust.codegen.core.rustlang.rust
 import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock
+import software.amazon.smithy.rust.codegen.core.rustlang.withBlockTemplate
 import software.amazon.smithy.rust.codegen.core.smithy.generators.setterName
 
 class FluentClientCore(private val model: Model) {
@@ -67,4 +68,15 @@ class FluentClientCore(private val model: Model) {
             write("self")
         }
     }
+
+    /**
+     * Generate and write Rust code for a getter method that returns a reference to the inner data.
+     */
+    fun RustWriter.renderGetterHelper(member: MemberShape, memberName: String, coreType: RustType) {
+        documentShape(member, model)
+        deprecatedShape(member)
+        withBlockTemplate("pub fn $memberName(&self) -> &#{CoreType} {", "}", "CoreType" to coreType) {
+            write("self.inner.$memberName()")
+        }
+    }
 }
diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerator.kt
index 7a4dc7b707..3f2f451f44 100644
--- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerator.kt
+++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerator.kt
@@ -47,6 +47,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.RustCrate
 import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider
 import software.amazon.smithy.rust.codegen.core.smithy.customize.writeCustomizations
 import software.amazon.smithy.rust.codegen.core.smithy.expectRustMetadata
+import software.amazon.smithy.rust.codegen.core.smithy.generators.getterName
 import software.amazon.smithy.rust.codegen.core.smithy.generators.setterName
 import software.amazon.smithy.rust.codegen.core.smithy.rustType
 import software.amazon.smithy.rust.codegen.core.util.inputShape
@@ -409,6 +410,14 @@ class FluentClientGenerator(
                 }
             }
 
+            rust("/// Access the ${operationSymbol.name} as a reference.\n")
+            withBlockTemplate(
+                "pub fn as_input(&self) -> &#{Inner} {", "}",
+                "Inner" to symbolProvider.symbolForBuilder(input),
+            ) {
+                write("&self.inner")
+            }
+
             if (smithyRuntimeMode.generateMiddleware) {
                 val middlewareScope = arrayOf(
                     *preludeScope,
@@ -630,6 +639,9 @@ class FluentClientGenerator(
                 val setterName = member.setterName()
                 val optionalInputType = outerType.asOptional()
                 with(core) { renderInputHelper(member, setterName, optionalInputType) }
+
+                val getterName = member.getterName()
+                with(core) { renderGetterHelper(member, getterName, optionalInputType) }
             }
         }
     }
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 da008a6bee..e86061c4c3 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
@@ -93,6 +93,9 @@ class OperationBuildError(private val runtimeConfig: RuntimeConfig) {
 // Setter names will never hit a reserved word and therefore never need escaping.
 fun MemberShape.setterName() = "set_${this.memberName.toSnakeCase()}"
 
+// Getter names will never hit a reserved word and therefore never need escaping.
+fun MemberShape.getterName() = "get_${this.memberName.toSnakeCase()}"
+
 class BuilderGenerator(
     private val model: Model,
     private val symbolProvider: RustSymbolProvider,
@@ -210,6 +213,28 @@ class BuilderGenerator(
         }
     }
 
+    /**
+     * Render a `get_foo` method. This is useful as a target for code generation, because the argument type
+     * is the same as the resulting member type, and is always optional.
+     */
+    private fun renderBuilderMemberGetterFn(
+        writer: RustWriter,
+        outerType: RustType,
+        member: MemberShape,
+        memberName: String,
+    ) {
+        // TODO(https://github.com/awslabs/smithy-rs/issues/1302): This `asOptional()` call is superfluous except in
+        //  the case where the shape is a `@streaming` blob, because [StreamingTraitSymbolProvider] always generates
+        //  a non `Option`al target type: in all other cases the client generates `Option`al types.
+        val inputType = outerType.asOptional()
+
+        writer.documentShape(member, model)
+        writer.deprecatedShape(member)
+        writer.rustBlock("pub fn ${member.getterName()}(&self) -> &${inputType.render(true)}") {
+            rust("&self.$memberName")
+        }
+    }
+
     private fun renderBuilder(writer: RustWriter) {
         writer.docs("A builder for #D.", structureSymbol)
         metadata.additionalAttributes.render(writer)
@@ -240,6 +265,7 @@ class BuilderGenerator(
                 }
 
                 renderBuilderMemberSetterFn(this, outerType, member, memberName)
+                renderBuilderMemberGetterFn(this, outerType, member, memberName)
             }
             writeCustomizations(customizations, BuilderSection.AdditionalMethods(shape))
             renderBuildFn(this)


Test patch:
diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGeneratorTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGeneratorTest.kt
index 6caee6aa0c..b1a48abc63 100644
--- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGeneratorTest.kt
+++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGeneratorTest.kt
@@ -30,7 +30,10 @@ class FluentClientGeneratorTest {
         }
 
         operation SayHello { input: TestInput }
-        structure TestInput {}
+        structure TestInput {
+           foo: String,
+           byteValue: Byte,
+        }
     """.asSmithyModel()
 
     @Test
@@ -76,4 +79,50 @@ class FluentClientGeneratorTest {
             test = test,
         )
     }
+
+    @Test
+    fun `generate inner builders`() {
+        val test: (ClientCodegenContext, RustCrate) -> Unit = { codegenContext, rustCrate ->
+            rustCrate.integrationTest("inner_builder") {
+                val moduleName = codegenContext.moduleUseName()
+                rustTemplate(
+                    """
+                    ##[test]
+                    fn test() {
+                        let connector = #{TestConnection}::<#{SdkBody}>::new(Vec::new());
+                        let config = $moduleName::Config::builder()
+                            .endpoint_resolver("http://localhost:1234")
+                            #{set_http_connector}
+                            .build();
+                        let smithy_client = aws_smithy_client::Builder::new()
+                            .connector(connector.clone())
+                            .middleware_fn(|r| r)
+                            .build_dyn();
+                        let client = $moduleName::Client::with_config(smithy_client, config);
+
+                        let say_hello_fluent_builder = client.say_hello().byte_value(4).foo("hello!");
+                        assert_eq!(*say_hello_fluent_builder.get_foo(), Some("hello!".to_string()));
+                        let input = say_hello_fluent_builder.as_input();
+                        assert_eq!(*input.get_byte_value(), Some(4));
+                    }
+                    """,
+                    "TestConnection" to CargoDependency.smithyClient(codegenContext.runtimeConfig)
+                        .withFeature("test-util").toType()
+                        .resolve("test_connection::TestConnection"),
+                    "SdkBody" to RuntimeType.sdkBody(codegenContext.runtimeConfig),
+                    "set_http_connector" to writable {
+                        if (codegenContext.smithyRuntimeMode.generateOrchestrator) {
+                            rust(".http_connector(connector.clone())")
+                        }
+                    },
+                )
+            }
+        }
+        clientIntegrationTest(model, TestCodegenSettings.middlewareModeTestParams, test = test)
+        clientIntegrationTest(
+            model,
+            TestCodegenSettings.orchestratorModeTestParams,
+            test = test,
+        )
+    }
 }
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 ec107f3fcb..fcec11601e 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
@@ -40,7 +40,10 @@ internal class BuilderGeneratorTest {
             unitTest("generate_builders") {
                 rust(
                     """
-                    let my_struct = MyStruct::builder().byte_value(4).foo("hello!").build();
+                    let my_struct_builder = MyStruct::builder().byte_value(4).foo("hello!");
+                    assert_eq!(*my_struct_builder.get_byte_value(), Some(4));
+
+                    let my_struct = my_struct_builder.build();
                     assert_eq!(my_struct.foo.unwrap(), "hello!");
                     assert_eq!(my_struct.bar, 0);
                     """,
diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedCollectionGeneratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedCollectionGeneratorTest.kt
index 885b54f61f..feceb42209 100644
--- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedCollectionGeneratorTest.kt
+++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedCollectionGeneratorTest.kt
@@ -18,22 +18,22 @@ class UnconstrainedCollectionGeneratorTest {
         val model =
             """
             namespace test
-            
+
             use aws.protocols#restJson1
             use smithy.framework#ValidationException
-            
+
             @restJson1
             service TestService {
                 operations: ["Operation"]
             }
-            
+
             @http(uri: "/operation", method: "POST")
             operation Operation {
                 input: OperationInputOutput
                 output: OperationInputOutput
                 errors: [ValidationException]
             }
-            
+
             structure OperationInputOutput {
                 list: ListA
             }
@@ -103,7 +103,7 @@ class UnconstrainedCollectionGeneratorTest {
                         let c_builder = crate::model::StructureC::builder();
                         let list_b_unconstrained = crate::unconstrained::list_b_unconstrained::ListBUnconstrained(vec![c_builder]);
                         let list_a_unconstrained = crate::unconstrained::list_a_unconstrained::ListAUnconstrained(vec![list_b_unconstrained]);
-  
+
                         let _list_a: crate::constrained::MaybeConstrained<crate::constrained::list_a_constrained::ListAConstrained> = list_a_unconstrained.into();
                         """,
                     )
diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedMapGeneratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedMapGeneratorTest.kt
index 8b2cbd3907..45f8b33fbc 100644
--- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedMapGeneratorTest.kt
+++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedMapGeneratorTest.kt
@@ -23,22 +23,22 @@ class UnconstrainedMapGeneratorTest {
         val model =
             """
             namespace test
-            
+
             use aws.protocols#restJson1
             use smithy.framework#ValidationException
-            
+
             @restJson1
             service TestService {
                 operations: ["Operation"]
             }
-            
+
             @http(uri: "/operation", method: "POST")
             operation Operation {
                 input: OperationInputOutput
                 output: OperationInputOutput
                 errors: [ValidationException]
             }
-            
+
             structure OperationInputOutput {
                 map: MapA
             }