Feature: Customizable Operations (#1647)

feature: customizable operations
update: CHANGELOG.next.toml
update: RFC0017
update: add IntelliJ idea folder to .gitignore
add: GenericsGenerator with tests and docs
add: rustTypeParameters helper fn with tests and docs
add: RetryPolicy optional arg to FluentClientGenerator
move: FluentClientGenerator into its own file
This commit is contained in:
Zelda Hessler 2022-09-02 17:47:25 -05:00 committed by GitHub
parent b266e05939
commit 50d88a5bf5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1325 additions and 432 deletions

3
.gitignore vendored
View File

@ -45,3 +45,6 @@ gradle-app.setting
# Rust build artifacts
target/
# IDEs
.idea/

View File

@ -72,3 +72,100 @@ wired up by default if none is provided.
references = ["smithy-rs#1603", "aws-sdk-rust#586"]
meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client" }
author = "jdisanti"
[[aws-sdk-rust]]
message = """
Implemented customizable operations per [RFC-0017](https://awslabs.github.io/smithy-rs/design/rfcs/rfc0017_customizable_client_operations.html).
Before this change, modifying operations before sending them required using lower-level APIs:
```rust
let input = SomeOperationInput::builder().some_value(5).build()?;
let operation = {
let op = input.make_operation(&service_config).await?;
let (request, response) = op.into_request_response();
let request = request.augment(|req, _props| {
req.headers_mut().insert(
HeaderName::from_static("x-some-header"),
HeaderValue::from_static("some-value")
);
Result::<_, Infallible>::Ok(req)
})?;
Operation::from_parts(request, response)
};
let response = smithy_client.call(operation).await?;
```
Now, users may easily modify operations before sending with the `customize` method:
```rust
let response = client.some_operation()
.some_value(5)
.customize()
.await?
.mutate_request(|mut req| {
req.headers_mut().insert(
HeaderName::from_static("x-some-header"),
HeaderValue::from_static("some-value")
);
})
.send()
.await?;
```
"""
references = ["smithy-rs#1647", "smithy-rs#1112"]
meta = { "breaking" = false, "tada" = true, "bug" = false }
author = "Velfi"
[[smithy-rs]]
message = """
Implemented customizable operations per [RFC-0017](https://awslabs.github.io/smithy-rs/design/rfcs/rfc0017_customizable_client_operations.html).
Before this change, modifying operations before sending them required using lower-level APIs:
```rust
let input = SomeOperationInput::builder().some_value(5).build()?;
let operation = {
let op = input.make_operation(&service_config).await?;
let (request, response) = op.into_request_response();
let request = request.augment(|req, _props| {
req.headers_mut().insert(
HeaderName::from_static("x-some-header"),
HeaderValue::from_static("some-value")
);
Result::<_, Infallible>::Ok(req)
})?;
Operation::from_parts(request, response)
};
let response = smithy_client.call(operation).await?;
```
Now, users may easily modify operations before sending with the `customize` method:
```rust
let response = client.some_operation()
.some_value(5)
.customize()
.await?
.mutate_request(|mut req| {
req.headers_mut().insert(
HeaderName::from_static("x-some-header"),
HeaderValue::from_static("some-value")
);
})
.send()
.await?;
```
"""
references = ["smithy-rs#1647", "smithy-rs#1112"]
meta = { "breaking" = false, "tada" = true, "bug" = false, "target" = "client"}
author = "Velfi"

View File

@ -25,6 +25,8 @@ import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.smithy.RustCrate
import software.amazon.smithy.rust.codegen.smithy.customize.RustCodegenDecorator
import software.amazon.smithy.rust.codegen.smithy.generators.GenericTypeArg
import software.amazon.smithy.rust.codegen.smithy.generators.GenericsGenerator
import software.amazon.smithy.rust.codegen.smithy.generators.LibRsCustomization
import software.amazon.smithy.rust.codegen.smithy.generators.LibRsSection
import software.amazon.smithy.rust.codegen.smithy.generators.client.FluentClientCustomization
@ -73,6 +75,10 @@ private class AwsClientGenerics(private val types: Types) : FluentClientGenerics
/** Bounds for generated `send()` functions */
override fun sendBounds(input: Symbol, output: Symbol, error: RuntimeType): Writable = writable { }
override fun toGenericsGenerator(): GenericsGenerator {
return GenericsGenerator()
}
}
class AwsFluentClientDecorator : RustCodegenDecorator<ClientCodegenContext> {
@ -82,15 +88,21 @@ class AwsFluentClientDecorator : RustCodegenDecorator<ClientCodegenContext> {
override val order: Byte = (AwsPresigningDecorator.ORDER + 1).toByte()
override fun extras(codegenContext: ClientCodegenContext, rustCrate: RustCrate) {
val types = Types(codegenContext.runtimeConfig)
val runtimeConfig = codegenContext.runtimeConfig
val types = Types(runtimeConfig)
val generics = AwsClientGenerics(types)
FluentClientGenerator(
codegenContext,
generics = AwsClientGenerics(types),
generics,
customizations = listOf(
AwsPresignedFluentBuilderMethod(codegenContext.runtimeConfig),
AwsPresignedFluentBuilderMethod(runtimeConfig),
AwsFluentClientDocs(codegenContext),
),
retryPolicyType = runtimeConfig.awsHttp().asType().member("retry::AwsErrorRetryPolicy"),
).render(rustCrate)
rustCrate.withModule(FluentClientGenerator.customizableOperationModule) { writer ->
renderCustomizableOperationSendMethod(runtimeConfig, generics, writer)
}
rustCrate.withModule(FluentClientGenerator.clientModule) { writer ->
AwsFluentClientExtensions(types).render(writer)
}
@ -254,3 +266,43 @@ private class AwsFluentClientDocs(private val coreCodegenContext: CoreCodegenCon
}
}
}
private fun renderCustomizableOperationSendMethod(
runtimeConfig: RuntimeConfig,
generics: FluentClientGenerics,
writer: RustWriter,
) {
val smithyHttp = CargoDependency.SmithyHttp(runtimeConfig).asType()
val operationGenerics = GenericsGenerator(GenericTypeArg("O"), GenericTypeArg("Retry"))
val handleGenerics = generics.toGenericsGenerator()
val combinedGenerics = operationGenerics + handleGenerics
val codegenScope = arrayOf(
"combined_generics_decl" to combinedGenerics.declaration(),
"handle_generics_bounds" to handleGenerics.bounds(),
"SdkSuccess" to smithyHttp.member("result::SdkSuccess"),
"ClassifyResponse" to smithyHttp.member("retry::ClassifyResponse"),
"ParseHttpResponse" to smithyHttp.member("response::ParseHttpResponse"),
)
writer.rustTemplate(
"""
impl#{combined_generics_decl:W} CustomizableOperation#{combined_generics_decl:W}
where
#{handle_generics_bounds:W}
{
/// Sends this operation's request
pub async fn send<T, E>(self) -> Result<T, SdkError<E>>
where
E: std::error::Error,
O: #{ParseHttpResponse}<Output = Result<T, E>> + Send + Sync + Clone + 'static,
Retry: #{ClassifyResponse}<#{SdkSuccess}<T>, SdkError<E>> + Send + Sync + Clone,
{
self.handle.client.call(self.operation).await
}
}
""",
*codegenScope,
)
}

View File

@ -0,0 +1,75 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
use aws_http::user_agent::AwsUserAgent;
use aws_sdk_s3::{Credentials, Region};
use aws_smithy_async::rt::sleep::TokioSleep;
use aws_smithy_client::test_connection::capture_request;
use std::convert::Infallible;
use std::sync::Arc;
use std::time::{Duration, UNIX_EPOCH};
#[tokio::test]
async fn test_s3_ops_are_customizable() -> Result<(), aws_sdk_s3::Error> {
let creds = Credentials::new(
"ANOTREAL",
"notrealrnrELgWzOk3IfjzDKtFBhDby",
Some("notarealsessiontoken".to_string()),
None,
"test",
);
let conf = aws_sdk_s3::Config::builder()
.credentials_provider(creds)
.region(Region::new("us-east-1"))
.sleep_impl(Arc::new(TokioSleep::new()))
.build();
let (conn, rcvr) = capture_request(None);
let client = aws_sdk_s3::Client::from_conf_conn(conf, conn);
let op = client
.list_buckets()
.customize()
.await
.expect("list_buckets is customizable")
.map_operation(|mut op| {
op.properties_mut()
.insert(UNIX_EPOCH + Duration::from_secs(1624036048));
op.properties_mut().insert(AwsUserAgent::for_tests());
Result::<_, Infallible>::Ok(op)
})
.expect("inserting into the property bag is infallible");
// The response from the fake connection won't return the expected XML but we don't care about
// that error in this test
let _ = op
.send()
.await
.expect_err("this will fail due to not receiving a proper XML response.");
let expected_req = rcvr.expect_request();
let auth_header = expected_req
.headers()
.get("Authorization")
.unwrap()
.to_owned();
// This is a snapshot test taken from a known working test result
let snapshot_signature =
"Signature=c2028dc806248952fc533ab4b1d9f1bafcdc9b3380ed00482f9935541ae11671";
assert!(
auth_header
.to_str()
.unwrap()
.contains(snapshot_signature),
"authorization header signature did not match expected signature: got {}, expected it to contain {}",
auth_header.to_str().unwrap(),
snapshot_signature
);
Ok(())
}

View File

@ -150,6 +150,7 @@ object RustReservedWords : ReservedWords {
"abstract",
"become",
"box",
"customize",
"do",
"final",
"macro",

View File

@ -42,6 +42,10 @@ sealed class RustType {
open val namespace: kotlin.String? = null
object Unit : RustType() {
override val name: kotlin.String = "()"
}
object Bool : RustType() {
override val name: kotlin.String = "bool"
}
@ -173,6 +177,7 @@ fun RustType.render(fullyQualified: Boolean = true): String {
this.namespace?.let { "$it::" } ?: ""
} else ""
val base = when (this) {
is RustType.Unit -> this.name
is RustType.Bool -> this.name
is RustType.Float -> this.name
is RustType.Integer -> this.name
@ -282,7 +287,7 @@ data class RustMetadata(
this.copy(derives = derives.copy(derives = derives.derives + newDerive))
fun withoutDerives(vararg withoutDerives: RuntimeType) =
this.copy(derives = derives.copy(derives = derives.derives - withoutDerives))
this.copy(derives = derives.copy(derives = derives.derives - withoutDerives.toSet()))
private fun attributes(): List<Attribute> = additionalAttributes + derives

View File

@ -22,6 +22,7 @@ import software.amazon.smithy.model.traits.DeprecatedTrait
import software.amazon.smithy.model.traits.DocumentationTrait
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.smithy.isOptional
import software.amazon.smithy.rust.codegen.smithy.letIf
import software.amazon.smithy.rust.codegen.smithy.rustType
import software.amazon.smithy.rust.codegen.util.getTrait
import software.amazon.smithy.rust.codegen.util.orNull
@ -86,9 +87,10 @@ fun <T : AbstractCodeWriter<T>> T.withBlockTemplate(
private fun <T : AbstractCodeWriter<T>, U> T.withTemplate(
template: String,
scope: Array<out Pair<String, Any>>,
trim: Boolean = true,
f: T.(String) -> U,
): U {
val contents = transformTemplate(template, scope)
val contents = transformTemplate(template, scope, trim)
pushState()
this.putContext(scope.toMap().mapKeys { (k, _) -> k.lowercase() })
val out = f(contents)
@ -137,9 +139,9 @@ fun <T : AbstractCodeWriter<T>> T.rust(
}
/* rewrite #{foo} to #{foo:T} (the smithy template format) */
private fun transformTemplate(template: String, scope: Array<out Pair<String, Any>>): String {
private fun transformTemplate(template: String, scope: Array<out Pair<String, Any>>, trim: Boolean = true): String {
check(scope.distinctBy { it.first.lowercase() }.size == scope.size) { "Duplicate cased keys not supported" }
return template.replace(Regex("""#\{([a-zA-Z_0-9]+)(:\w)?\}""")) { matchResult ->
val output = template.replace(Regex("""#\{([a-zA-Z_0-9]+)(:\w)?\}""")) { matchResult ->
val keyName = matchResult.groupValues[1]
val templateType = matchResult.groupValues[2].ifEmpty { ":T" }
if (!scope.toMap().keys.contains(keyName)) {
@ -150,7 +152,9 @@ private fun transformTemplate(template: String, scope: Array<out Pair<String, An
)
}
"#{${keyName.lowercase()}$templateType}"
}.trim()
}
return output.letIf(trim) { output.trim() }
}
/**
@ -194,6 +198,20 @@ fun RustWriter.rustTemplate(
}
}
/**
* An API for templating inline Rust code.
*
* Works just like [RustWriter.rustTemplate] but won't write a newline at the end and won't trim the input
*/
fun RustWriter.rustInlineTemplate(
@Language("Rust", prefix = "macro_rules! foo { () => {{ ", suffix = "}}}") contents: String,
vararg ctx: Pair<String, Any>,
) {
withTemplate(contents, ctx, trim = false) { template ->
writeInline(template)
}
}
/*
* Writes a Rust-style block, demarcated by curly braces
*/
@ -317,21 +335,6 @@ private fun Element.changeInto(tagName: String) {
*/
fun RustWriter.raw(text: String) = writeInline(escape(text))
typealias Writable = RustWriter.() -> Unit
/** Helper to allow coercing the Writeable signature
* writable { rust("fn foo() { }")
*/
fun writable(w: Writable): Writable = w
fun writable(w: String): Writable = writable { rust(w) }
fun Writable.isEmpty(): Boolean {
val writer = RustWriter.root()
this(writer)
return writer.toString() == RustWriter.root().toString()
}
/**
* Rustdoc doesn't support `r#` for raw identifiers.
* This function adjusts doc links to refer to raw identifiers directly.
@ -376,8 +379,9 @@ class RustWriter private constructor(
override fun write(content: Any?, vararg args: Any?): RustWriter {
// TODO(https://github.com/rust-lang/rustfmt/issues/5425): The second condition introduced here is to prevent
// this rustfmt bug
if (debugMode && (content as? String?)?.let { it.trim() != "," } ?: false) {
// this rustfmt bug
val contentIsNotJustAComma = (content as? String?)?.let { it.trim() != "," } ?: false
if (debugMode && contentIsNotJustAComma) {
val location = Thread.currentThread().stackTrace
location.first { it.isRelevant() }?.let { "/* ${it.fileName}:${it.lineNumber} */" }
?.also { super.writeInline(it) }
@ -471,12 +475,15 @@ class RustWriter private constructor(
block(derefName)
}
}
shape is NumberShape -> rustBlock("if ${outerField.removePrefix("&")} != 0") {
block(outerField)
}
shape is BooleanShape -> rustBlock("if ${outerField.removePrefix("&")}") {
block(outerField)
}
else -> this.block(outerField)
}
}
@ -555,10 +562,16 @@ class RustWriter private constructor(
// for now, use the fully qualified type name
t.fullyQualifiedName()
}
is Symbol -> {
addDepsRecursively(t)
t.rustType().render(fullyQualified = true)
}
is RustType -> {
t.render(fullyQualified = true)
}
else -> throw CodegenException("Invalid type provided to RustSymbolFormatter: $t")
// escaping generates `##` sequences for all the common cases where
// it will be run through templating, but in this context, we won't be escaped

View File

@ -0,0 +1,75 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rust.codegen.rustlang
import software.amazon.smithy.codegen.core.Symbol
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.smithy.generators.GenericsGenerator
import software.amazon.smithy.rust.codegen.util.PANIC
typealias Writable = RustWriter.() -> Unit
/**
* Helper to allow coercing the Writeable signature
* writable { rust("fn foo() { }")
*/
fun writable(w: Writable): Writable = w
fun writable(w: String): Writable = writable { rust(w) }
fun Writable.isEmpty(): Boolean {
val writer = RustWriter.root()
this(writer)
return writer.toString() == RustWriter.root().toString()
}
/**
* Combine multiple writable types into a Rust generic type parameter list
*
* e.g.
*
* ```kotlin
* rustTemplate(
* "some_fn::<#{type_params:W}>();",
* "type_params" to rustTypeParameters(
* symbolProvider.toSymbol(operation),
* RustType.Unit,
* runtimeConfig.smithyHttp().member("body::SdkBody"),
* GenericsGenerator(GenericTypeArg("A"), GenericTypeArg("B")),
* )
* )
* ```
* would write out something like:
* ```rust
* some_fn::<crate::operation::SomeOperation, (), aws_smithy_http::body::SdkBody, A, B>();
* ```
*/
fun rustTypeParameters(
vararg typeParameters: Any,
): Writable = writable {
if (typeParameters.isNotEmpty()) {
rustInlineTemplate("<")
val iterator: Iterator<Any> = typeParameters.iterator()
while (iterator.hasNext()) {
when (val typeParameter = iterator.next()) {
is Symbol, is RustType.Unit, is RuntimeType -> rustInlineTemplate("#{it}", "it" to typeParameter)
is String -> rustInlineTemplate(typeParameter)
is GenericsGenerator -> rustInlineTemplate(
"#{gg:W}",
"gg" to typeParameter.declaration(withAngleBrackets = false),
)
else -> PANIC("Unhandled type '$typeParameter' encountered by rustTypeParameters writer")
}
if (iterator.hasNext()) {
rustInlineTemplate(", ")
}
}
rustInlineTemplate(">")
}
}

View File

@ -0,0 +1,153 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rust.codegen.smithy.generators
import software.amazon.smithy.rust.codegen.rustlang.rustInlineTemplate
import software.amazon.smithy.rust.codegen.rustlang.rustTemplate
import software.amazon.smithy.rust.codegen.rustlang.writable
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
data class GenericTypeArg(
val typeArg: String,
val bound: RuntimeType? = null,
)
/**
* A "writable" collection of Rust generic type args and their bounds.
*
* e.g.
* ```
* val generics = GenericsGenerator(
* GenericTypeArg("P", testRT("Pineapple")),
* GenericTypeArg("C", testRT("fruits::melon::Cantaloupe™")),
* GenericTypeArg("T"),
* )
*
* rustTemplate("fn eat_fruit_salad#{decl}()", "decl" to generics.declaration())
* // Writes "fn eat_fruit_salad<P, C, T>()"
*
* rustTemplate("fn eat_fruit_salad<#{decl}>()", "decl" to generics.declaration(withAngleBrackets = false))
* // Writes "fn eat_fruit_salad<P, C, T>()"
*
* rustTemplate("""
* pub struct FruitSalad;
*
* impl<#{decl}> FruitSalad
* where:
* #{bounds}
* {
* pub fn new#{decl}() { todo!() }
* }
* """, "decl" to generics.declaration(), "bounds" to generics.bounds())
* // Writes:
* // pub struct FruitSalad;
* //
* // impl<P, C, T> FruitSalad
* // where:
* // P: Pineapple,
* // C: fruits::melon::Cantaloupe,
* // {
* // pub fn new<P, C, T>() { todo!() }
* // }
* ```
*/
class GenericsGenerator(vararg genericTypeArgs: GenericTypeArg) {
private val typeArgs: List<GenericTypeArg>
init {
typeArgs = genericTypeArgs.toList()
}
/**
* Returns the generics type args formatted for use in declarations.
*
* e.g.
*
* ```
* rustTemplate("fn eat_fruit_salad#{decl}()", "decl" to generics.declaration())
* // Writes "fn eat_fruit_salad<P, C, T>()"
*
* rustTemplate("fn eat_fruit_salad<#{decl}>()", "decl" to generics.declaration(withAngleBrackets = false))
* // Writes "fn eat_fruit_salad<P, C, T>()"
* ```
*/
fun declaration(withAngleBrackets: Boolean = true) = writable {
// Write nothing if this generator is empty
if (typeArgs.isNotEmpty()) {
val typeArgs = typeArgs.joinToString(", ") { it.typeArg }
if (withAngleBrackets) {
rustInlineTemplate("<")
}
rustInlineTemplate(typeArgs)
if (withAngleBrackets) {
rustInlineTemplate(">")
}
}
}
/**
* Returns bounded generic type args formatted for use in a "where" clause.
* Type args with no bound will not be written.
*
* e.g.
*
* ```
* * rustTemplate("""
* pub struct FruitSalad;
*
* impl<#{decl}> FruitSalad
* where:
* #{bounds}
* {
* pub fn new#{decl}() { todo!() }
* }
* """, "decl" to generics.declaration(), "bounds" to generics.bounds())
* // Writes:
* // pub struct FruitSalad;
* //
* // impl<P, C, T> FruitSalad
* // where:
* // P: Pineapple,
* // C: fruits::melon::Cantaloupe,
* // {
* // pub fn new<P, C, T>() { todo!() }
* // }
* ```
*/
fun bounds() = writable {
// Only write bounds for generic type params with a bound
for ((typeArg, bound) in typeArgs) {
if (bound != null) {
rustTemplate("$typeArg: #{bound},\n", "bound" to bound)
}
}
}
/**
* Combine two `GenericsGenerator`s into one. Type args for the first `GenericsGenerator` will appear before
* type args from the second `GenericsGenerator`.
*
* e.g.
*
* ```
* val ggA = GenericsGenerator(
* GenericTypeArg("A", testRT("Apple")),
* )
* val ggB = GenericsGenerator(
* GenericTypeArg("B", testRT("Banana")),
* )
*
* rustTemplate("fn eat_fruit#{decl}()", "decl" to (ggA + ggB).declaration())
* // Writes "fn eat_fruit<A, B>()"
*
*/
operator fun plus(operationGenerics: GenericsGenerator): GenericsGenerator {
return GenericsGenerator(*(typeArgs + operationGenerics.typeArgs).toTypedArray())
}
}

View File

@ -5,62 +5,24 @@
package software.amazon.smithy.rust.codegen.smithy.generators.client
import software.amazon.smithy.codegen.core.SymbolProvider
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.knowledge.TopDownIndex
import software.amazon.smithy.model.shapes.MemberShape
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.model.shapes.StructureShape
import software.amazon.smithy.model.traits.DocumentationTrait
import software.amazon.smithy.rust.codegen.rustlang.CargoDependency
import software.amazon.smithy.rust.codegen.rustlang.Feature
import software.amazon.smithy.rust.codegen.rustlang.RustMetadata
import software.amazon.smithy.rust.codegen.rustlang.RustModule
import software.amazon.smithy.rust.codegen.rustlang.RustReservedWords
import software.amazon.smithy.rust.codegen.rustlang.RustType
import software.amazon.smithy.rust.codegen.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.rustlang.Visibility
import software.amazon.smithy.rust.codegen.rustlang.Writable
import software.amazon.smithy.rust.codegen.rustlang.asArgumentType
import software.amazon.smithy.rust.codegen.rustlang.asOptional
import software.amazon.smithy.rust.codegen.rustlang.asType
import software.amazon.smithy.rust.codegen.rustlang.deprecatedShape
import software.amazon.smithy.rust.codegen.rustlang.docLink
import software.amazon.smithy.rust.codegen.rustlang.docs
import software.amazon.smithy.rust.codegen.rustlang.documentShape
import software.amazon.smithy.rust.codegen.rustlang.escape
import software.amazon.smithy.rust.codegen.rustlang.normalizeHtml
import software.amazon.smithy.rust.codegen.rustlang.qualifiedName
import software.amazon.smithy.rust.codegen.rustlang.render
import software.amazon.smithy.rust.codegen.rustlang.rust
import software.amazon.smithy.rust.codegen.rustlang.rustBlockTemplate
import software.amazon.smithy.rust.codegen.rustlang.rustTemplate
import software.amazon.smithy.rust.codegen.rustlang.stripOuter
import software.amazon.smithy.rust.codegen.rustlang.writable
import software.amazon.smithy.rust.codegen.smithy.ClientCodegenContext
import software.amazon.smithy.rust.codegen.smithy.CoreCodegenContext
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.smithy.RustCrate
import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider
import software.amazon.smithy.rust.codegen.smithy.customize.NamedSectionGenerator
import software.amazon.smithy.rust.codegen.smithy.customize.RustCodegenDecorator
import software.amazon.smithy.rust.codegen.smithy.customize.Section
import software.amazon.smithy.rust.codegen.smithy.customize.writeCustomizations
import software.amazon.smithy.rust.codegen.smithy.expectRustMetadata
import software.amazon.smithy.rust.codegen.smithy.generators.CodegenTarget
import software.amazon.smithy.rust.codegen.smithy.generators.LibRsCustomization
import software.amazon.smithy.rust.codegen.smithy.generators.LibRsSection
import software.amazon.smithy.rust.codegen.smithy.generators.PaginatorGenerator
import software.amazon.smithy.rust.codegen.smithy.generators.builderSymbol
import software.amazon.smithy.rust.codegen.smithy.generators.error.errorSymbol
import software.amazon.smithy.rust.codegen.smithy.generators.isPaginated
import software.amazon.smithy.rust.codegen.smithy.generators.setterName
import software.amazon.smithy.rust.codegen.smithy.rustType
import software.amazon.smithy.rust.codegen.util.inputShape
import software.amazon.smithy.rust.codegen.util.orNull
import software.amazon.smithy.rust.codegen.util.outputShape
import software.amazon.smithy.rust.codegen.util.toSnakeCase
class FluentClientDecorator : RustCodegenDecorator<ClientCodegenContext> {
override val name: String = "FluentClient"
@ -287,363 +249,3 @@ class GenericFluentClient(coreCodegenContext: CoreCodegenContext) : FluentClient
}
}
}
class FluentClientGenerator(
private val coreCodegenContext: CoreCodegenContext,
private val generics: FluentClientGenerics = FlexibleClientGenerics(
connectorDefault = null,
middlewareDefault = null,
retryDefault = CargoDependency.SmithyClient(coreCodegenContext.runtimeConfig).asType().member("retry::Standard"),
client = CargoDependency.SmithyClient(coreCodegenContext.runtimeConfig).asType(),
),
private val customizations: List<FluentClientCustomization> = emptyList(),
) {
companion object {
fun clientOperationFnName(operationShape: OperationShape, symbolProvider: RustSymbolProvider): String =
RustReservedWords.escapeIfNeeded(symbolProvider.toSymbol(operationShape).name.toSnakeCase())
val clientModule = RustModule(
"client",
RustMetadata(visibility = Visibility.PUBLIC),
documentation = "Client and fluent builders for calling the service.",
)
}
private val serviceShape = coreCodegenContext.serviceShape
private val operations =
TopDownIndex.of(coreCodegenContext.model).getContainedOperations(serviceShape).sortedBy { it.id }
private val symbolProvider = coreCodegenContext.symbolProvider
private val model = coreCodegenContext.model
private val clientDep = CargoDependency.SmithyClient(coreCodegenContext.runtimeConfig)
private val runtimeConfig = coreCodegenContext.runtimeConfig
private val core = FluentClientCore(model)
fun render(crate: RustCrate) {
crate.withModule(clientModule) { writer ->
renderFluentClient(writer)
}
}
private fun renderFluentClient(writer: RustWriter) {
writer.rustTemplate(
"""
##[derive(Debug)]
pub(crate) struct Handle#{generics_decl:W} {
pub(crate) client: #{client}::Client#{smithy_inst:W},
pub(crate) conf: crate::Config,
}
#{client_docs:W}
##[derive(std::fmt::Debug)]
pub struct Client#{generics_decl:W} {
handle: std::sync::Arc<Handle${generics.inst}>
}
impl${generics.inst} std::clone::Clone for Client${generics.inst} {
fn clone(&self) -> Self {
Self { handle: self.handle.clone() }
}
}
##[doc(inline)]
pub use #{client}::Builder;
impl${generics.inst} From<#{client}::Client#{smithy_inst:W}> for Client${generics.inst} {
fn from(client: #{client}::Client#{smithy_inst:W}) -> Self {
Self::with_config(client, crate::Config::builder().build())
}
}
impl${generics.inst} Client${generics.inst} {
/// Creates a client with the given service configuration.
pub fn with_config(client: #{client}::Client#{smithy_inst:W}, conf: crate::Config) -> Self {
Self {
handle: std::sync::Arc::new(Handle {
client,
conf,
})
}
}
/// Returns the client's configuration.
pub fn conf(&self) -> &crate::Config {
&self.handle.conf
}
}
""",
"generics_decl" to generics.decl,
"smithy_inst" to generics.smithyInst,
"client" to clientDep.asType(),
"client_docs" to writable
{
customizations.forEach {
it.section(
FluentClientSection.FluentClientDocs(
serviceShape,
),
)(this)
}
},
)
writer.rustBlockTemplate(
"impl${generics.inst} Client${generics.inst} #{bounds:W}",
"client" to clientDep.asType(),
"bounds" to generics.bounds,
) {
operations.forEach { operation ->
val name = symbolProvider.toSymbol(operation).name
val fullPath = operation.fullyQualifiedFluentBuilder(symbolProvider)
val maybePaginated = if (operation.isPaginated(model)) {
"\n/// This operation supports pagination; See [`into_paginator()`]($fullPath::into_paginator)."
} else ""
val output = operation.outputShape(model)
val operationOk = symbolProvider.toSymbol(output)
val operationErr = operation.errorSymbol(model, symbolProvider, CodegenTarget.CLIENT).toSymbol()
val inputFieldsBody = generateOperationShapeDocs(writer, symbolProvider, operation, model).joinToString("\n") {
"/// - $it"
}
val inputFieldsHead = if (inputFieldsBody.isNotEmpty()) {
"The fluent builder is configurable:"
} else {
"The fluent builder takes no input, just [`send`]($fullPath::send) it."
}
val outputFieldsBody = generateShapeMemberDocs(writer, symbolProvider, output, model).joinToString("\n") {
"/// - $it"
}
var outputFieldsHead = "On success, responds with [`${operationOk.name}`]($operationOk)"
if (outputFieldsBody.isNotEmpty()) {
outputFieldsHead += " with field(s):"
}
rustTemplate(
"""
/// Constructs a fluent builder for the [`$name`]($fullPath) operation.$maybePaginated
///
/// - $inputFieldsHead
$inputFieldsBody
/// - $outputFieldsHead
$outputFieldsBody
/// - On failure, responds with [`SdkError<${operationErr.name}>`]($operationErr)
""",
)
rust(
"""
pub fn ${
clientOperationFnName(
operation,
symbolProvider,
)
}(&self) -> fluent_builders::$name${generics.inst} {
fluent_builders::$name::new(self.handle.clone())
}
""",
)
}
}
writer.withModule("fluent_builders") {
docs(
"""
Utilities to ergonomically construct a request to the service.
Fluent builders are created through the [`Client`](crate::client::Client) by calling
one if its operation methods. After parameters are set using the builder methods,
the `send` method can be called to initiate the request.
""".trim(),
newlinePrefix = "//! ",
)
operations.forEach { operation ->
val operationSymbol = symbolProvider.toSymbol(operation)
val input = operation.inputShape(model)
val baseDerives = symbolProvider.toSymbol(input).expectRustMetadata().derives
val derives = baseDerives.derives.intersect(setOf(RuntimeType.Clone)) + RuntimeType.Debug
rust(
"""
/// Fluent builder constructing a request to `${operationSymbol.name}`.
///
""",
)
documentShape(operation, model, autoSuppressMissingDocs = false)
deprecatedShape(operation)
baseDerives.copy(derives = derives).render(this)
rustTemplate(
"""
pub struct ${operationSymbol.name}#{generics:W} {
handle: std::sync::Arc<super::Handle${generics.inst}>,
inner: #{Inner}
}
""",
"Inner" to input.builderSymbol(symbolProvider),
"client" to clientDep.asType(),
"generics" to generics.decl,
"operation" to operationSymbol,
)
rustBlockTemplate(
"impl${generics.inst} ${operationSymbol.name}${generics.inst} #{bounds:W}",
"client" to clientDep.asType(),
"bounds" to generics.bounds,
) {
val inputType = symbolProvider.toSymbol(operation.inputShape(model))
val outputType = symbolProvider.toSymbol(operation.outputShape(model))
val errorType = operation.errorSymbol(model, symbolProvider, CodegenTarget.CLIENT)
rustTemplate(
"""
/// Creates a new `${operationSymbol.name}`.
pub(crate) fn new(handle: std::sync::Arc<super::Handle${generics.inst}>) -> Self {
Self { handle, inner: Default::default() }
}
/// Sends the request and returns the response.
///
/// If an error occurs, an `SdkError` will be returned with additional details that
/// can be matched against.
///
/// By default, any retryable failures will be retried twice. Retry behavior
/// is configurable with the [RetryConfig](aws_smithy_types::retry::RetryConfig), which can be
/// set when configuring the client.
pub async fn send(self) -> std::result::Result<#{ok}, #{sdk_err}<#{operation_err}>>
#{send_bounds:W} {
let op = self.inner.build().map_err(|err|#{sdk_err}::ConstructionFailure(err.into()))?
.make_operation(&self.handle.conf)
.await
.map_err(|err|#{sdk_err}::ConstructionFailure(err.into()))?;
self.handle.client.call(op).await
}
""",
"ok" to outputType,
"operation_err" to errorType,
"sdk_err" to CargoDependency.SmithyHttp(runtimeConfig).asType()
.copy(name = "result::SdkError"),
"send_bounds" to generics.sendBounds(inputType, outputType, errorType),
)
PaginatorGenerator.paginatorType(coreCodegenContext, generics, operation)?.also { paginatorType ->
rustTemplate(
"""
/// Create a paginator for this request
///
/// Paginators are used by calling [`send().await`](#{Paginator}::send) which returns a [`Stream`](tokio_stream::Stream).
pub fn into_paginator(self) -> #{Paginator}${generics.inst} {
#{Paginator}::new(self.handle, self.inner)
}
""",
"Paginator" to paginatorType,
)
}
writeCustomizations(
customizations,
FluentClientSection.FluentBuilderImpl(
operation,
operation.errorSymbol(model, symbolProvider, CodegenTarget.CLIENT),
),
)
input.members().forEach { member ->
val memberName = symbolProvider.toMemberName(member)
// All fields in the builder are optional
val memberSymbol = symbolProvider.toSymbol(member)
val outerType = memberSymbol.rustType()
when (val coreType = outerType.stripOuter<RustType.Option>()) {
is RustType.Vec -> with(core) { renderVecHelper(member, memberName, coreType) }
is RustType.HashMap -> with(core) { renderMapHelper(member, memberName, coreType) }
else -> with(core) { renderInputHelper(member, memberName, coreType) }
}
// pure setter
val setterName = member.setterName()
val optionalInputType = outerType.asOptional()
with(core) { renderInputHelper(member, setterName, optionalInputType) }
}
}
}
}
}
}
/**
* For a given `operation` shape, return a list of strings where each string describes the name and input type of one of
* the operation's corresponding fluent builder methods as well as that method's documentation from the smithy model
*
* _NOTE: This function generates the docs that appear under **"The fluent builder is configurable:"**_
*/
fun generateOperationShapeDocs(writer: RustWriter, symbolProvider: SymbolProvider, operation: OperationShape, model: Model): List<String> {
val input = operation.inputShape(model)
val fluentBuilderFullyQualifiedName = operation.fullyQualifiedFluentBuilder(symbolProvider)
return input.members().map { memberShape ->
val builderInputDoc = memberShape.asFluentBuilderInputDoc(symbolProvider)
val builderInputLink = docLink("$fluentBuilderFullyQualifiedName::${symbolProvider.toMemberName(memberShape)}")
val builderSetterDoc = memberShape.asFluentBuilderSetterDoc(symbolProvider)
val builderSetterLink = docLink("$fluentBuilderFullyQualifiedName::${memberShape.setterName()}")
val docTrait = memberShape.getMemberTrait(model, DocumentationTrait::class.java).orNull()
val docs = when (docTrait?.value?.isNotBlank()) {
true -> normalizeHtml(writer.escape(docTrait.value)).replace("\n", " ")
else -> "(undocumented)"
}
"[`$builderInputDoc`]($builderInputLink) / [`$builderSetterDoc`]($builderSetterLink): $docs"
}
}
/**
* For a give `struct` shape, return a list of strings where each string describes the name and type of a struct field
* as well as that field's documentation from the smithy model
*
* * _NOTE: This function generates the list of types that appear under **"On success, responds with"**_
*/
fun generateShapeMemberDocs(writer: RustWriter, symbolProvider: SymbolProvider, shape: StructureShape, model: Model): List<String> {
val structName = symbolProvider.toSymbol(shape).rustType().qualifiedName()
return shape.members().map { memberShape ->
val name = symbolProvider.toMemberName(memberShape)
val member = symbolProvider.toSymbol(memberShape).rustType().render(fullyQualified = false)
val docTrait = memberShape.getMemberTrait(model, DocumentationTrait::class.java).orNull()
val docs = when (docTrait?.value?.isNotBlank()) {
true -> normalizeHtml(writer.escape(docTrait.value)).replace("\n", " ")
else -> "(undocumented)"
}
"[`$name($member)`](${docLink("$structName::$name")}): $docs"
}
}
/**
* Generate a valid fully-qualified Type for a fluent builder e.g.
* `OperationShape(AssumeRole)` -> `"crate::client::fluent_builders::AssumeRole"`
*
* * _NOTE: This function generates the links that appear under **"The fluent builder is configurable:"**_
*/
fun OperationShape.fullyQualifiedFluentBuilder(symbolProvider: SymbolProvider): String {
val operationName = symbolProvider.toSymbol(this).name
return "crate::client::fluent_builders::$operationName"
}
/**
* Generate a string that looks like a Rust function pointer for documenting a fluent builder method e.g.
* `<MemberShape representing a struct method>` -> `"method_name(MethodInputType)"`
*
* _NOTE: This function generates the type names that appear under **"The fluent builder is configurable:"**_
*/
fun MemberShape.asFluentBuilderInputDoc(symbolProvider: SymbolProvider): String {
val memberName = symbolProvider.toMemberName(this)
val outerType = symbolProvider.toSymbol(this).rustType()
return "$memberName(${outerType.stripOuter<RustType.Option>().asArgumentType(fullyQualified = false)})"
}
/**
* Generate a string that looks like a Rust function pointer for documenting a fluent builder setter method e.g.
* `<MemberShape representing a struct method>` -> `"set_method_name(Option<MethodInputType>)"`
*
* _NOTE: This function generates the setter type names that appear under **"The fluent builder is configurable:"**_
*/
fun MemberShape.asFluentBuilderSetterDoc(symbolProvider: SymbolProvider): String {
val memberName = this.setterName()
val outerType = symbolProvider.toSymbol(this).rustType()
return "$memberName(${outerType.asArgumentType(fullyQualified = false)})"
}

View File

@ -0,0 +1,598 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rust.codegen.smithy.generators.client
import software.amazon.smithy.codegen.core.SymbolProvider
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.knowledge.TopDownIndex
import software.amazon.smithy.model.shapes.MemberShape
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.shapes.StructureShape
import software.amazon.smithy.model.traits.DocumentationTrait
import software.amazon.smithy.rust.codegen.rustlang.CargoDependency
import software.amazon.smithy.rust.codegen.rustlang.RustModule
import software.amazon.smithy.rust.codegen.rustlang.RustReservedWords
import software.amazon.smithy.rust.codegen.rustlang.RustType
import software.amazon.smithy.rust.codegen.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.rustlang.asArgumentType
import software.amazon.smithy.rust.codegen.rustlang.asOptional
import software.amazon.smithy.rust.codegen.rustlang.asType
import software.amazon.smithy.rust.codegen.rustlang.deprecatedShape
import software.amazon.smithy.rust.codegen.rustlang.docLink
import software.amazon.smithy.rust.codegen.rustlang.docs
import software.amazon.smithy.rust.codegen.rustlang.documentShape
import software.amazon.smithy.rust.codegen.rustlang.escape
import software.amazon.smithy.rust.codegen.rustlang.normalizeHtml
import software.amazon.smithy.rust.codegen.rustlang.qualifiedName
import software.amazon.smithy.rust.codegen.rustlang.render
import software.amazon.smithy.rust.codegen.rustlang.rust
import software.amazon.smithy.rust.codegen.rustlang.rustBlockTemplate
import software.amazon.smithy.rust.codegen.rustlang.rustTemplate
import software.amazon.smithy.rust.codegen.rustlang.rustTypeParameters
import software.amazon.smithy.rust.codegen.rustlang.stripOuter
import software.amazon.smithy.rust.codegen.rustlang.writable
import software.amazon.smithy.rust.codegen.smithy.ClientCodegenContext
import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.smithy.RustCrate
import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider
import software.amazon.smithy.rust.codegen.smithy.customize.writeCustomizations
import software.amazon.smithy.rust.codegen.smithy.expectRustMetadata
import software.amazon.smithy.rust.codegen.smithy.generators.CodegenTarget
import software.amazon.smithy.rust.codegen.smithy.generators.GenericTypeArg
import software.amazon.smithy.rust.codegen.smithy.generators.GenericsGenerator
import software.amazon.smithy.rust.codegen.smithy.generators.PaginatorGenerator
import software.amazon.smithy.rust.codegen.smithy.generators.builderSymbol
import software.amazon.smithy.rust.codegen.smithy.generators.error.errorSymbol
import software.amazon.smithy.rust.codegen.smithy.generators.isPaginated
import software.amazon.smithy.rust.codegen.smithy.generators.setterName
import software.amazon.smithy.rust.codegen.smithy.generators.smithyHttp
import software.amazon.smithy.rust.codegen.smithy.rustType
import software.amazon.smithy.rust.codegen.util.inputShape
import software.amazon.smithy.rust.codegen.util.orNull
import software.amazon.smithy.rust.codegen.util.outputShape
import software.amazon.smithy.rust.codegen.util.toSnakeCase
class FluentClientGenerator(
private val codegenContext: ClientCodegenContext,
private val generics: FluentClientGenerics = FlexibleClientGenerics(
connectorDefault = null,
middlewareDefault = null,
retryDefault = CargoDependency.SmithyClient(codegenContext.runtimeConfig).asType()
.member("retry::Standard"),
client = CargoDependency.SmithyClient(codegenContext.runtimeConfig).asType(),
),
private val customizations: List<FluentClientCustomization> = emptyList(),
private val retryPolicyType: RuntimeType? = null,
) {
companion object {
fun clientOperationFnName(operationShape: OperationShape, symbolProvider: RustSymbolProvider): String =
RustReservedWords.escapeIfNeeded(symbolProvider.toSymbol(operationShape).name.toSnakeCase())
val clientModule = RustModule.public(
"client",
"Client and fluent builders for calling the service.",
)
val customizableOperationModule = RustModule.public(
"customizable_operation",
"Wrap operations in a special type allowing for the modification of operations and the requests inside before sending them",
)
}
private val serviceShape = codegenContext.serviceShape
private val operations =
TopDownIndex.of(codegenContext.model).getContainedOperations(serviceShape).sortedBy { it.id }
private val symbolProvider = codegenContext.symbolProvider
private val model = codegenContext.model
private val clientDep = CargoDependency.SmithyClient(codegenContext.runtimeConfig)
private val runtimeConfig = codegenContext.runtimeConfig
private val core = FluentClientCore(model)
fun render(crate: RustCrate) {
crate.withModule(clientModule) { writer ->
renderFluentClient(writer)
}
crate.withModule(customizableOperationModule) { writer ->
renderCustomizableOperationModule(runtimeConfig, generics, writer)
if (codegenContext.settings.codegenConfig.includeFluentClient) {
renderCustomizableOperationSend(runtimeConfig, generics, writer)
}
}
}
private fun renderFluentClient(writer: RustWriter) {
writer.rustTemplate(
"""
##[derive(Debug)]
pub(crate) struct Handle#{generics_decl:W} {
pub(crate) client: #{client}::Client#{smithy_inst:W},
pub(crate) conf: crate::Config,
}
#{client_docs:W}
##[derive(std::fmt::Debug)]
pub struct Client#{generics_decl:W} {
handle: std::sync::Arc<Handle${generics.inst}>
}
impl${generics.inst} std::clone::Clone for Client${generics.inst} {
fn clone(&self) -> Self {
Self { handle: self.handle.clone() }
}
}
##[doc(inline)]
pub use #{client}::Builder;
impl${generics.inst} From<#{client}::Client#{smithy_inst:W}> for Client${generics.inst} {
fn from(client: #{client}::Client#{smithy_inst:W}) -> Self {
Self::with_config(client, crate::Config::builder().build())
}
}
impl${generics.inst} Client${generics.inst} {
/// Creates a client with the given service configuration.
pub fn with_config(client: #{client}::Client#{smithy_inst:W}, conf: crate::Config) -> Self {
Self {
handle: std::sync::Arc::new(Handle {
client,
conf,
})
}
}
/// Returns the client's configuration.
pub fn conf(&self) -> &crate::Config {
&self.handle.conf
}
}
""",
"generics_decl" to generics.decl,
"smithy_inst" to generics.smithyInst,
"client" to clientDep.asType(),
"client_docs" to writable
{
customizations.forEach {
it.section(
FluentClientSection.FluentClientDocs(
serviceShape,
),
)(this)
}
},
)
writer.rustBlockTemplate(
"impl${generics.inst} Client${generics.inst} #{bounds:W}",
"client" to clientDep.asType(),
"bounds" to generics.bounds,
) {
operations.forEach { operation ->
val name = symbolProvider.toSymbol(operation).name
val fullPath = operation.fullyQualifiedFluentBuilder(symbolProvider)
val maybePaginated = if (operation.isPaginated(model)) {
"\n/// This operation supports pagination; See [`into_paginator()`]($fullPath::into_paginator)."
} else ""
val output = operation.outputShape(model)
val operationOk = symbolProvider.toSymbol(output)
val operationErr = operation.errorSymbol(model, symbolProvider, CodegenTarget.CLIENT).toSymbol()
val inputFieldsBody =
generateOperationShapeDocs(writer, symbolProvider, operation, model).joinToString("\n") {
"/// - $it"
}
val inputFieldsHead = if (inputFieldsBody.isNotEmpty()) {
"The fluent builder is configurable:"
} else {
"The fluent builder takes no input, just [`send`]($fullPath::send) it."
}
val outputFieldsBody =
generateShapeMemberDocs(writer, symbolProvider, output, model).joinToString("\n") {
"/// - $it"
}
var outputFieldsHead = "On success, responds with [`${operationOk.name}`]($operationOk)"
if (outputFieldsBody.isNotEmpty()) {
outputFieldsHead += " with field(s):"
}
rustTemplate(
"""
/// Constructs a fluent builder for the [`$name`]($fullPath) operation.$maybePaginated
///
/// - $inputFieldsHead
$inputFieldsBody
/// - $outputFieldsHead
$outputFieldsBody
/// - On failure, responds with [`SdkError<${operationErr.name}>`]($operationErr)
""",
)
rust(
"""
pub fn ${
clientOperationFnName(
operation,
symbolProvider,
)
}(&self) -> fluent_builders::$name${generics.inst} {
fluent_builders::$name::new(self.handle.clone())
}
""",
)
}
}
writer.withModule("fluent_builders") {
docs(
"""
Utilities to ergonomically construct a request to the service.
Fluent builders are created through the [`Client`](crate::client::Client) by calling
one if its operation methods. After parameters are set using the builder methods,
the `send` method can be called to initiate the request.
""".trim(),
newlinePrefix = "//! ",
)
operations.forEach { operation ->
val operationSymbol = symbolProvider.toSymbol(operation)
val input = operation.inputShape(model)
val baseDerives = symbolProvider.toSymbol(input).expectRustMetadata().derives
val derives = baseDerives.derives.intersect(setOf(RuntimeType.Clone)) + RuntimeType.Debug
rust(
"""
/// Fluent builder constructing a request to `${operationSymbol.name}`.
///
""",
)
documentShape(operation, model, autoSuppressMissingDocs = false)
deprecatedShape(operation)
baseDerives.copy(derives = derives).render(this)
rustTemplate(
"""
pub struct ${operationSymbol.name}#{generics:W} {
handle: std::sync::Arc<super::Handle${generics.inst}>,
inner: #{Inner}
}
""",
"Inner" to input.builderSymbol(symbolProvider),
"client" to clientDep.asType(),
"generics" to generics.decl,
"operation" to operationSymbol,
)
rustBlockTemplate(
"impl${generics.inst} ${operationSymbol.name}${generics.inst} #{bounds:W}",
"client" to clientDep.asType(),
"bounds" to generics.bounds,
) {
val inputType = symbolProvider.toSymbol(operation.inputShape(model))
val outputType = symbolProvider.toSymbol(operation.outputShape(model))
val errorType = operation.errorSymbol(model, symbolProvider, CodegenTarget.CLIENT)
// Have to use fully-qualified result here or else it could conflict with an op named Result
rustTemplate(
"""
/// Creates a new `${operationSymbol.name}`.
pub(crate) fn new(handle: std::sync::Arc<super::Handle${generics.inst}>) -> Self {
Self { handle, inner: Default::default() }
}
/// Consume this builder, creating a customizable operation that can be modified before being
/// sent. The operation's inner [http::Request] can be modified as well.
pub async fn customize(self) -> std::result::Result<
crate::customizable_operation::CustomizableOperation#{customizable_op_type_params:W},
#{SdkError}<#{OperationError}>
> #{send_bounds:W} {
let handle = self.handle.clone();
let operation = self.inner.build().map_err(|err|#{SdkError}::ConstructionFailure(err.into()))?
.make_operation(&handle.conf)
.await
.map_err(|err|#{SdkError}::ConstructionFailure(err.into()))?;
Ok(crate::customizable_operation::CustomizableOperation { handle, operation })
}
/// Sends the request and returns the response.
///
/// If an error occurs, an `SdkError` will be returned with additional details that
/// can be matched against.
///
/// By default, any retryable failures will be retried twice. Retry behavior
/// is configurable with the [RetryConfig](aws_smithy_types::retry::RetryConfig), which can be
/// set when configuring the client.
pub async fn send(self) -> std::result::Result<#{OperationOutput}, #{SdkError}<#{OperationError}>>
#{send_bounds:W} {
let op = self.inner.build().map_err(|err|#{SdkError}::ConstructionFailure(err.into()))?
.make_operation(&self.handle.conf)
.await
.map_err(|err|#{SdkError}::ConstructionFailure(err.into()))?;
self.handle.client.call(op).await
}
""",
"ClassifyResponse" to runtimeConfig.smithyHttp().member("retry::ClassifyResponse"),
"OperationError" to errorType,
"OperationOutput" to outputType,
"SdkError" to runtimeConfig.smithyHttp().member("result::SdkError"),
"SdkSuccess" to runtimeConfig.smithyHttp().member("result::SdkSuccess"),
"send_bounds" to generics.sendBounds(inputType, outputType, errorType),
"customizable_op_type_params" to rustTypeParameters(
symbolProvider.toSymbol(operation),
retryPolicyType ?: RustType.Unit,
generics.toGenericsGenerator(),
),
)
PaginatorGenerator.paginatorType(codegenContext, generics, operation)?.also { paginatorType ->
rustTemplate(
"""
/// Create a paginator for this request
///
/// Paginators are used by calling [`send().await`](#{Paginator}::send) which returns a [`Stream`](tokio_stream::Stream).
pub fn into_paginator(self) -> #{Paginator}${generics.inst} {
#{Paginator}::new(self.handle, self.inner)
}
""",
"Paginator" to paginatorType,
)
}
writeCustomizations(
customizations,
FluentClientSection.FluentBuilderImpl(
operation,
operation.errorSymbol(model, symbolProvider, CodegenTarget.CLIENT),
),
)
input.members().forEach { member ->
val memberName = symbolProvider.toMemberName(member)
// All fields in the builder are optional
val memberSymbol = symbolProvider.toSymbol(member)
val outerType = memberSymbol.rustType()
when (val coreType = outerType.stripOuter<RustType.Option>()) {
is RustType.Vec -> with(core) { renderVecHelper(member, memberName, coreType) }
is RustType.HashMap -> with(core) { renderMapHelper(member, memberName, coreType) }
else -> with(core) { renderInputHelper(member, memberName, coreType) }
}
// pure setter
val setterName = member.setterName()
val optionalInputType = outerType.asOptional()
with(core) { renderInputHelper(member, setterName, optionalInputType) }
}
}
}
}
}
}
private fun renderCustomizableOperationModule(
runtimeConfig: RuntimeConfig,
generics: FluentClientGenerics,
writer: RustWriter,
) {
val smithyHttp = CargoDependency.SmithyHttp(runtimeConfig).asType()
val operationGenerics = GenericsGenerator(GenericTypeArg("O"), GenericTypeArg("Retry"))
val handleGenerics = generics.toGenericsGenerator()
val combinedGenerics = operationGenerics + handleGenerics
val codegenScope = arrayOf(
// SDK Types
"http_result" to smithyHttp.member("result"),
"http_body" to smithyHttp.member("body"),
"http_operation" to smithyHttp.member("operation"),
"HttpRequest" to CargoDependency.Http.asType().member("Request"),
"handle_generics_decl" to handleGenerics.declaration(),
"handle_generics_bounds" to handleGenerics.bounds(),
"operation_generics_decl" to operationGenerics.declaration(),
"combined_generics_decl" to combinedGenerics.declaration(),
)
writer.rustTemplate(
"""
use crate::client::Handle;
use #{http_body}::SdkBody;
use #{http_operation}::Operation;
use #{http_result}::SdkError;
use std::convert::Infallible;
use std::sync::Arc;
/// A wrapper type for [`Operation`](aws_smithy_http::operation::Operation)s that allows for
/// customization of the operation before it is sent. A `CustomizableOperation` may be sent
/// by calling its [`.send()`][crate::customizable_operation::CustomizableOperation::send] method.
##[derive(Debug)]
pub struct CustomizableOperation#{combined_generics_decl:W} {
pub(crate) handle: Arc<Handle#{handle_generics_decl:W}>,
pub(crate) operation: Operation#{operation_generics_decl:W},
}
impl#{combined_generics_decl:W} CustomizableOperation#{combined_generics_decl:W}
where
#{handle_generics_bounds:W}
{
/// Allows for customizing the operation's request
pub fn map_request<E>(
mut self,
f: impl FnOnce(#{HttpRequest}<SdkBody>) -> Result<#{HttpRequest}<SdkBody>, E>,
) -> Result<Self, E> {
let (request, response) = self.operation.into_request_response();
let request = request.augment(|req, _props| f(req))?;
self.operation = Operation::from_parts(request, response);
Ok(self)
}
/// Convenience for `map_request` where infallible direct mutation of request is acceptable
pub fn mutate_request<E>(self, f: impl FnOnce(&mut #{HttpRequest}<SdkBody>)) -> Self {
self.map_request(|mut req| {
f(&mut req);
Result::<_, Infallible>::Ok(req)
})
.expect("infallible")
}
/// Allows for customizing the entire operation
pub fn map_operation<E>(
mut self,
f: impl FnOnce(Operation#{operation_generics_decl:W}) -> Result<Operation#{operation_generics_decl:W}, E>,
) -> Result<Self, E> {
self.operation = f(self.operation)?;
Ok(self)
}
/// Direct access to read the HTTP request
pub fn request(&self) -> &#{HttpRequest}<SdkBody> {
self.operation.request()
}
/// Direct access to mutate the HTTP request
pub fn request_mut(&mut self) -> &mut #{HttpRequest}<SdkBody> {
self.operation.request_mut()
}
}
""",
*codegenScope,
)
}
private fun renderCustomizableOperationSend(
runtimeConfig: RuntimeConfig,
generics: FluentClientGenerics,
writer: RustWriter,
) {
val smithyHttp = CargoDependency.SmithyHttp(runtimeConfig).asType()
val smithyClient = CargoDependency.SmithyClient(runtimeConfig).asType()
val operationGenerics = GenericsGenerator(GenericTypeArg("O"), GenericTypeArg("Retry"))
val handleGenerics = generics.toGenericsGenerator()
val combinedGenerics = operationGenerics + handleGenerics
val codegenScope = arrayOf(
"combined_generics_decl" to combinedGenerics.declaration(),
"handle_generics_bounds" to handleGenerics.bounds(),
"ParseHttpResponse" to smithyHttp.member("response::ParseHttpResponse"),
"NewRequestPolicy" to smithyClient.member("retry::NewRequestPolicy"),
"SmithyRetryPolicy" to smithyClient.member("bounds::SmithyRetryPolicy"),
)
writer.rustTemplate(
"""
impl#{combined_generics_decl:W} CustomizableOperation#{combined_generics_decl:W}
where
#{handle_generics_bounds:W}
{
/// Sends this operation's request
pub async fn send<T, E>(self) -> Result<T, SdkError<E>>
where
E: std::error::Error,
O: #{ParseHttpResponse}<Output = Result<T, E>> + Send + Sync + Clone + 'static,
Retry: Send + Sync + Clone,
<R as #{NewRequestPolicy}>::Policy: #{SmithyRetryPolicy}<O, T, E, Retry> + Clone,
{
self.handle.client.call(self.operation).await
}
}
""",
*codegenScope,
)
}
/**
* For a given `operation` shape, return a list of strings where each string describes the name and input type of one of
* the operation's corresponding fluent builder methods as well as that method's documentation from the smithy model
*
* _NOTE: This function generates the docs that appear under **"The fluent builder is configurable:"**_
*/
private fun generateOperationShapeDocs(
writer: RustWriter,
symbolProvider: SymbolProvider,
operation: OperationShape,
model: Model,
): List<String> {
val input = operation.inputShape(model)
val fluentBuilderFullyQualifiedName = operation.fullyQualifiedFluentBuilder(symbolProvider)
return input.members().map { memberShape ->
val builderInputDoc = memberShape.asFluentBuilderInputDoc(symbolProvider)
val builderInputLink = docLink("$fluentBuilderFullyQualifiedName::${symbolProvider.toMemberName(memberShape)}")
val builderSetterDoc = memberShape.asFluentBuilderSetterDoc(symbolProvider)
val builderSetterLink = docLink("$fluentBuilderFullyQualifiedName::${memberShape.setterName()}")
val docTrait = memberShape.getMemberTrait(model, DocumentationTrait::class.java).orNull()
val docs = when (docTrait?.value?.isNotBlank()) {
true -> normalizeHtml(writer.escape(docTrait.value)).replace("\n", " ")
else -> "(undocumented)"
}
"[`$builderInputDoc`]($builderInputLink) / [`$builderSetterDoc`]($builderSetterLink): $docs"
}
}
/**
* For a give `struct` shape, return a list of strings where each string describes the name and type of a struct field
* as well as that field's documentation from the smithy model
*
* * _NOTE: This function generates the list of types that appear under **"On success, responds with"**_
*/
private fun generateShapeMemberDocs(
writer: RustWriter,
symbolProvider: SymbolProvider,
shape: StructureShape,
model: Model,
): List<String> {
val structName = symbolProvider.toSymbol(shape).rustType().qualifiedName()
return shape.members().map { memberShape ->
val name = symbolProvider.toMemberName(memberShape)
val member = symbolProvider.toSymbol(memberShape).rustType().render(fullyQualified = false)
val docTrait = memberShape.getMemberTrait(model, DocumentationTrait::class.java).orNull()
val docs = when (docTrait?.value?.isNotBlank()) {
true -> normalizeHtml(writer.escape(docTrait.value)).replace("\n", " ")
else -> "(undocumented)"
}
"[`$name($member)`](${docLink("$structName::$name")}): $docs"
}
}
/**
* Generate a valid fully-qualified Type for a fluent builder e.g.
* `OperationShape(AssumeRole)` -> `"crate::client::fluent_builders::AssumeRole"`
*
* * _NOTE: This function generates the links that appear under **"The fluent builder is configurable:"**_
*/
private fun OperationShape.fullyQualifiedFluentBuilder(symbolProvider: SymbolProvider): String {
val operationName = symbolProvider.toSymbol(this).name
return "crate::client::fluent_builders::$operationName"
}
/**
* Generate a string that looks like a Rust function pointer for documenting a fluent builder method e.g.
* `<MemberShape representing a struct method>` -> `"method_name(MethodInputType)"`
*
* _NOTE: This function generates the type names that appear under **"The fluent builder is configurable:"**_
*/
private fun MemberShape.asFluentBuilderInputDoc(symbolProvider: SymbolProvider): String {
val memberName = symbolProvider.toMemberName(this)
val outerType = symbolProvider.toSymbol(this).rustType()
return "$memberName(${outerType.stripOuter<RustType.Option>().asArgumentType(fullyQualified = false)})"
}
/**
* Generate a string that looks like a Rust function pointer for documenting a fluent builder setter method e.g.
* `<MemberShape representing a struct method>` -> `"set_method_name(Option<MethodInputType>)"`
*
* _NOTE: This function generates the setter type names that appear under **"The fluent builder is configurable:"**_
*/
private fun MemberShape.asFluentBuilderSetterDoc(symbolProvider: SymbolProvider): String {
val memberName = this.setterName()
val outerType = symbolProvider.toSymbol(this).rustType()
return "$memberName(${outerType.asArgumentType(fullyQualified = false)})"
}

View File

@ -11,6 +11,8 @@ import software.amazon.smithy.rust.codegen.rustlang.rust
import software.amazon.smithy.rust.codegen.rustlang.rustTemplate
import software.amazon.smithy.rust.codegen.rustlang.writable
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.smithy.generators.GenericTypeArg
import software.amazon.smithy.rust.codegen.smithy.generators.GenericsGenerator
interface FluentClientGenerics {
/** Declaration with defaults set */
@ -27,6 +29,9 @@ interface FluentClientGenerics {
/** Bounds for generated `send()` functions */
fun sendBounds(input: Symbol, output: Symbol, error: RuntimeType): Writable
/** Convert this `FluentClientGenerics` into the more general `GenericsGenerator` */
fun toGenericsGenerator(): GenericsGenerator
}
data class FlexibleClientGenerics(
@ -38,7 +43,7 @@ data class FlexibleClientGenerics(
/** Declaration with defaults set */
override val decl = writable {
rustTemplate(
"<C #{c:W}, M#{m:W}, R#{r:W}>",
"<C#{c:W}, M#{m:W}, R#{r:W}>",
"c" to defaultType(connectorDefault),
"m" to defaultType(middlewareDefault),
"r" to defaultType(retryDefault),
@ -83,6 +88,12 @@ data class FlexibleClientGenerics(
)
}
override fun toGenericsGenerator(): GenericsGenerator = GenericsGenerator(
GenericTypeArg("C", client.member("bounds::SmithyConnector")),
GenericTypeArg("M", client.member("bounds::SmithyMiddleware<C>")),
GenericTypeArg("R", client.member("retry::NewRequestPolicy")),
)
private fun defaultType(default: RuntimeType?) = writable {
default?.also { rust("= #T", default) }
}

View File

@ -10,6 +10,7 @@ import software.amazon.smithy.model.shapes.BlobShape
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.rust.codegen.rustlang.Attribute
import software.amazon.smithy.rust.codegen.rustlang.CargoDependency
import software.amazon.smithy.rust.codegen.rustlang.RustType
import software.amazon.smithy.rust.codegen.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.rustlang.asType
import software.amazon.smithy.rust.codegen.rustlang.docs
@ -39,7 +40,7 @@ open class MakeOperationGenerator(
private val protocol: Protocol,
private val bodyGenerator: ProtocolPayloadGenerator,
private val public: Boolean,
/** Whether or not to include default values for content-length and content-type */
/** Whether to include default values for content-length and content-type */
private val includeDefaultPayloadHeaders: Boolean,
private val functionName: String = "make_operation",
) {
@ -151,7 +152,7 @@ open class MakeOperationGenerator(
writer.format(symbolProvider.toSymbol(shape))
private fun buildOperationTypeRetry(writer: RustWriter, customizations: List<OperationCustomization>): String =
customizations.firstNotNullOfOrNull { it.retryType() }?.let { writer.format(it) } ?: "()"
(customizations.firstNotNullOfOrNull { it.retryType() } ?: RustType.Unit).let { writer.format(it) }
private fun needsContentLength(operationShape: OperationShape): Boolean {
return protocol.httpBindingResolver.requestBindings(operationShape)

View File

@ -8,6 +8,7 @@ package software.amazon.smithy.rust.codegen.smithy.generators.protocol
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.shapes.StructureShape
import software.amazon.smithy.rust.codegen.rustlang.Attribute
import software.amazon.smithy.rust.codegen.rustlang.RustType
import software.amazon.smithy.rust.codegen.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.rustlang.docLink
import software.amazon.smithy.rust.codegen.rustlang.rust
@ -197,5 +198,5 @@ open class ProtocolGenerator(
writer.format(symbolProvider.toSymbol(shape))
private fun buildOperationTypeRetry(writer: RustWriter, customizations: List<OperationCustomization>): String =
customizations.firstNotNullOfOrNull { it.retryType() }?.let { writer.format(it) } ?: "()"
(customizations.firstNotNullOfOrNull { it.retryType() } ?: RustType.Unit).let { writer.format(it) }
}

View File

@ -0,0 +1,133 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rust.codegen.generators
import io.kotest.matchers.string.shouldContain
import org.junit.jupiter.api.Test
import software.amazon.smithy.rust.codegen.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.rustlang.rustTemplate
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.smithy.generators.GenericTypeArg
import software.amazon.smithy.rust.codegen.smithy.generators.GenericsGenerator
class GenericsGeneratorTest {
@Test
fun `declaration is correct for no args`() {
val gg = GenericsGenerator()
val writer = RustWriter.forModule("model")
writer.rustTemplate("A#{decl:W}B", "decl" to gg.declaration())
writer.toString() shouldContain "AB"
}
@Test
fun `declaration is correct for 1 arg`() {
val gg = GenericsGenerator(GenericTypeArg("T"))
val writer = RustWriter.forModule("model")
writer.rustTemplate("#{decl:W}", "decl" to gg.declaration())
writer.toString() shouldContain "<T>"
}
@Test
fun `declaration is correct for several args`() {
val gg = GenericsGenerator(GenericTypeArg("T"), GenericTypeArg("U"), GenericTypeArg("V"))
val writer = RustWriter.forModule("model")
writer.rustTemplate("#{decl:W}", "decl" to gg.declaration())
writer.toString() shouldContain "<T, U, V>"
}
@Test
fun `bounds is correct for no args`() {
val gg = GenericsGenerator()
val writer = RustWriter.forModule("model")
writer.rustTemplate("A#{bounds:W}B", "bounds" to gg.bounds())
writer.toString() shouldContain "AB"
}
@Test
fun `bounds is correct for 1 arg`() {
val gg = GenericsGenerator(GenericTypeArg("T", testRT("Test")))
val writer = RustWriter.forModule("model")
writer.rustTemplate("#{bounds:W}", "bounds" to gg.bounds())
writer.toString() shouldContain "T: test::Test,"
}
@Test
fun `bounds is correct for several args`() {
val gg = GenericsGenerator(
GenericTypeArg("A", testRT("Apple")),
GenericTypeArg("PL", testRT("Plum")),
GenericTypeArg("PE", testRT("Pear")),
)
val writer = RustWriter.forModule("model")
writer.rustTemplate("#{bounds:W}", "bounds" to gg.bounds())
writer.toString() shouldContain """
A: test::Apple,
PL: test::Plum,
PE: test::Pear,
""".trimIndent()
}
@Test
fun `bounds skips arg with no bounds`() {
val gg = GenericsGenerator(
GenericTypeArg("A", testRT("Apple")),
GenericTypeArg("PL"),
GenericTypeArg("PE", testRT("Pear")),
)
val writer = RustWriter.forModule("model")
writer.rustTemplate("#{bounds:W}", "bounds" to gg.bounds())
writer.toString() shouldContain """
A: test::Apple,
PE: test::Pear,
""".trimIndent()
}
@Test
fun `bounds generates nothing if all args are skipped`() {
val gg = GenericsGenerator(
GenericTypeArg("A"),
GenericTypeArg("PL"),
GenericTypeArg("PE"),
)
val writer = RustWriter.forModule("model")
writer.rustTemplate("A#{bounds:W}B", "bounds" to gg.bounds())
writer.toString() shouldContain "AB"
}
@Test
fun `Adding GenericGenerators works`() {
val ggA = GenericsGenerator(
GenericTypeArg("A", testRT("Apple")),
)
val ggB = GenericsGenerator(
GenericTypeArg("B", testRT("Banana")),
)
RustWriter.forModule("model").let { writer ->
writer.rustTemplate("#{bounds:W}", "bounds" to (ggA + ggB).bounds())
writer.toString() shouldContain """
A: test::Apple,
B: test::Banana,
""".trimIndent()
}
RustWriter.forModule("model").let { writer ->
writer.rustTemplate("#{decl:W}", "decl" to (ggA + ggB).declaration())
writer.toString() shouldContain "<A, B>"
}
}
private fun testRT(name: String): RuntimeType = RuntimeType(name, null, "test")
}

View File

@ -0,0 +1,63 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rust.codegen.rustlang
import io.kotest.matchers.string.shouldContain
import org.junit.jupiter.api.Test
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.smithy.generators.GenericTypeArg
import software.amazon.smithy.rust.codegen.smithy.generators.GenericsGenerator
internal class RustTypeParametersTest {
private fun forInputExpectOutput(input: Any, expectedOutput: String) {
val writer = RustWriter.forModule("model")
writer.rustTemplate("#{typeParameters:W}", "typeParameters" to rustTypeParameters(input))
writer.toString() shouldContain expectedOutput
}
@Test
fun `rustTypeParameters accepts RustType Unit`() {
forInputExpectOutput(RustType.Unit, "()")
}
@Test
fun `rustTypeParameters accepts Symbol`() {
val symbol = RuntimeType("Operation", namespace = "crate::operation", dependency = null).toSymbol()
forInputExpectOutput(symbol, "<crate::operation::Operation>")
}
@Test
fun `rustTypeParameters accepts RuntimeType`() {
val runtimeType = RuntimeType("String", namespace = "std::string", dependency = null)
forInputExpectOutput(runtimeType, "<std::string::String>")
}
@Test
fun `rustTypeParameters accepts String`() {
forInputExpectOutput("Option<Vec<String>>", "<Option<Vec<String>>>")
}
@Test
fun `rustTypeParameters accepts GenericsGenerator`() {
forInputExpectOutput(GenericsGenerator(GenericTypeArg("A"), GenericTypeArg("B")), "<A, B>")
}
@Test
fun `rustTypeParameters accepts heterogeneous inputs`() {
val writer = RustWriter.forModule("model")
val tps = rustTypeParameters(
RuntimeType("Operation", namespace = "crate::operation", dependency = null).toSymbol(),
RustType.Unit,
RuntimeType("String", namespace = "std::string", dependency = null),
"T",
GenericsGenerator(GenericTypeArg("A"), GenericTypeArg("B")),
)
writer.rustTemplate("#{typeParameters:W}", "typeParameters" to tps)
writer.toString() shouldContain "<crate::operation::Operation, (), std::string::String, T, A, B>"
}
}

View File

@ -1,7 +1,7 @@
RFC: Customizable Client Operations
===================================
> Status: Accepted
> Status: Implemented
For a summarized list of proposed changes, see the [Changes Checklist](#changes-checklist) section.
@ -190,9 +190,9 @@ that would conflict with the new function, so adding it would not be a breaking
Changes Checklist
-----------------
- [ ] Create `CustomizableOperation` as an inlinable, and code generate it into `client` so that it has access to `Handle`
- [ ] Code generate the `customize` method on fluent builders
- [ ] Update the `RustReservedWords` class to include `customize`
- [ ] Add ability to mutate the HTTP request on `Operation`
- [x] Create `CustomizableOperation` as an inlinable, and code generate it into `client` so that it has access to `Handle`
- [x] Code generate the `customize` method on fluent builders
- [x] Update the `RustReservedWords` class to include `customize`
- [x] Add ability to mutate the HTTP request on `Operation`
- [ ] Add examples for both approaches
- [ ] Comment on older discussions asking about how to do this with this improved approach

View File

@ -188,6 +188,16 @@ impl<H, R> Operation<H, R> {
self.request.properties()
}
/// Gives mutable access to the underlying HTTP request.
pub fn request_mut(&mut self) -> &mut http::Request<SdkBody> {
self.request.http_mut()
}
/// Gives readonly access to the underlying HTTP request.
pub fn request(&self) -> &http::Request<SdkBody> {
self.request.http()
}
pub fn with_metadata(mut self, metadata: Metadata) -> Self {
self.parts.metadata = Some(metadata);
self