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-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/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) 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 = 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 }