mirror of https://github.com/smithy-lang/smithy-rs
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:
parent
b266e05939
commit
50d88a5bf5
|
@ -45,3 +45,6 @@ gradle-app.setting
|
|||
|
||||
# Rust build artifacts
|
||||
target/
|
||||
|
||||
# IDEs
|
||||
.idea/
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
|
@ -150,6 +150,7 @@ object RustReservedWords : ReservedWords {
|
|||
"abstract",
|
||||
"become",
|
||||
"box",
|
||||
"customize",
|
||||
"do",
|
||||
"final",
|
||||
"macro",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(">")
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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)})"
|
||||
}
|
||||
|
|
|
@ -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)})"
|
||||
}
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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>"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue