Change `make_operation(..)` to be an async function (#797)

* Change `make_operation(..)` to be an async function

As part of the work towards glacier customizations, calling `make_operation(..)` needs to be able to perform asyncrhonous operations like potentially reading the body. To enable this and other future requirements, we now always generate make_operation as an async function.

* Add CR to changelog

* Fix server tests
This commit is contained in:
Russell Cohen 2021-10-21 17:32:17 -04:00 committed by GitHub
parent f77b00ce79
commit d341c6eec7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 103 additions and 51 deletions

View File

@ -1,5 +1,7 @@
vNext (Month Day, Year)
=======================
**Breaking Changes**
- `<operation>.make_operation(&config)` is now an `async` function for all operations. Code should be updated to call `.await`. This will only impact users using the low-level API. (smithy-rs#797)
v0.27 (October 20th, 2021)
==========================

View File

@ -1,5 +1,7 @@
vNext (Month Day, Year)
=======================
**Breaking Changes**
- `<operation>.make_operation(&config)` is now an `async` function for all operations. Code should be updated to call `.await`. This will only impact users using the low-level API. (smithy-rs#797)
v0.0.22-alpha (October 20th, 2021)
==================================

View File

@ -55,6 +55,7 @@ impl AssumeRoleProvider {
.build()
.expect("operation is valid")
.make_operation(&config)
.await
.expect("valid operation");
let assume_role_creds = client_config
.core_client

View File

@ -172,6 +172,7 @@ impl AssumeRoleProvider {
.op
.clone()
.make_operation(&self.conf)
.await
.expect("valid operation");
let assumed = self.sts.call(op).in_current_span().await;

View File

@ -247,6 +247,7 @@ async fn load_credentials(
.build()
.expect("valid operation")
.make_operation(&conf)
.await
.expect("valid operation");
let resp = client.call(operation).await.map_err(|sdk_error| {
tracing::warn!(error = ?sdk_error, "sts returned an error assuming web identity role");

View File

@ -197,6 +197,7 @@ class AwsInputPresignedMethod(
rustTemplate(
"""
let (mut request, _) = self.$makeOperationFn(config)
.await
.map_err(|err| #{SdkError}::ConstructionFailure(err.into()))?
.into_request_response();
""",

View File

@ -62,6 +62,7 @@ class IntegrationTestDependencies(
addDependency(smithyClient)
addDependency(SerdeJson)
addDependency(Tokio)
addDependency(FuturesUtil)
}
if (hasBenches) {
addDependency(Criterion)
@ -93,3 +94,4 @@ private val FuturesCore = CargoDependency("futures-core", CratesIo("0.3"), Depen
private val Hound = CargoDependency("hound", CratesIo("3.4"), DependencyScope.Dev)
private val SerdeJson = CargoDependency("serde_json", CratesIo("1"), features = emptySet(), scope = DependencyScope.Dev)
private val Tokio = CargoDependency("tokio", CratesIo("1"), features = setOf("macros", "test-util"), scope = DependencyScope.Dev)
private val FuturesUtil = CargoDependency("futures-util", CratesIo("0.3"), scope = DependencyScope.Dev)

View File

@ -8,7 +8,13 @@ package software.amazon.smithy.rustsdk
import org.junit.jupiter.api.Test
import software.amazon.smithy.model.node.ObjectNode
import software.amazon.smithy.rust.codegen.rustlang.CargoDependency
import software.amazon.smithy.rust.codegen.testutil.*
import software.amazon.smithy.rust.codegen.testutil.TestWorkspace
import software.amazon.smithy.rust.codegen.testutil.asSmithyModel
import software.amazon.smithy.rust.codegen.testutil.compileAndTest
import software.amazon.smithy.rust.codegen.testutil.stubConfigProject
import software.amazon.smithy.rust.codegen.testutil.testCodegenContext
import software.amazon.smithy.rust.codegen.testutil.unitTest
import software.amazon.smithy.rust.codegen.testutil.validateConfigCustomizations
import software.amazon.smithy.rust.codegen.util.lookup
internal class EndpointConfigCustomizationTest {

View File

@ -285,7 +285,7 @@ async fn main() -> Result<(), Error> {
let raw_client = aws_hyper::Client::https();
raw_client
.call(wait_for_ready_table(&table, client.conf()))
.call(wait_for_ready_table(&table, client.conf()).await)
.await
.expect("table should become ready.");
@ -363,7 +363,7 @@ async fn main() -> Result<(), Error> {
/// Construct a `DescribeTable` request with a policy to retry every second until the table
/// is ready
fn wait_for_ready_table(
async fn wait_for_ready_table(
table_name: &str,
conf: &Config,
) -> Operation<DescribeTable, WaitForReadyTable<AwsErrorRetryPolicy>> {
@ -372,6 +372,7 @@ fn wait_for_ready_table(
.build()
.expect("valid input")
.make_operation(&conf)
.await
.expect("valid operation");
let waiting_policy = WaitForReadyTable {
inner: operation.retry_policy().clone(),

View File

@ -97,7 +97,7 @@ async fn main() -> Result<(), Error> {
}
raw_client
.call(wait_for_ready_table(&table.to_string(), client.conf()))
.call(wait_for_ready_table(&table.to_string(), client.conf()).await)
.await
.expect("table should become ready");
@ -255,7 +255,7 @@ where
/// Construct a `DescribeTable` request with a policy to retry every second until the table
/// is ready
fn wait_for_ready_table(
async fn wait_for_ready_table(
table_name: &str,
conf: &Config,
) -> Operation<DescribeTable, WaitForReadyTable<AwsErrorRetryPolicy>> {
@ -264,6 +264,7 @@ fn wait_for_ready_table(
.build()
.expect("valid input")
.make_operation(&conf)
.await
.expect("valid operation");
let waiting_policy = WaitForReadyTable {
inner: operation.retry_policy().clone(),

View File

@ -7,6 +7,7 @@ use aws_sdk_dynamodb::input::PutItemInput;
use aws_sdk_dynamodb::model::AttributeValue;
use aws_sdk_dynamodb::Config;
use criterion::{criterion_group, criterion_main, Criterion};
use futures_util::FutureExt;
macro_rules! attr_s {
($str_val:expr) => {
@ -36,6 +37,8 @@ macro_rules! attr_obj {
fn do_bench(config: &Config, input: &PutItemInput) {
let operation = input
.make_operation(&config)
.now_or_never()
.unwrap()
.expect("operation failed to build");
let (http_request, _parts) = operation.into_request_response().0.into_parts();
let body = http_request.body().bytes().unwrap();

View File

@ -148,7 +148,7 @@ where
/// Construct a `DescribeTable` request with a policy to retry every second until the table
/// is ready
fn wait_for_ready_table(
async fn wait_for_ready_table(
table_name: &str,
conf: &Config,
) -> Operation<DescribeTable, WaitForReadyTable<AwsErrorRetryPolicy>> {
@ -157,6 +157,7 @@ fn wait_for_ready_table(
.build()
.unwrap()
.make_operation(&conf)
.await
.expect("valid operation");
let waiting_policy = WaitForReadyTable {
inner: operation.retry_policy().clone(),
@ -197,6 +198,7 @@ async fn movies_it() {
.call(
create_table(table_name)
.make_operation(&conf)
.await
.expect("valid request"),
)
.await
@ -204,7 +206,7 @@ async fn movies_it() {
let waiter_start = tokio::time::Instant::now();
client
.call(wait_for_ready_table(table_name, &conf))
.call(wait_for_ready_table(table_name, &conf).await)
.await
.expect("table should become ready");
@ -220,6 +222,7 @@ async fn movies_it() {
.call(
add_item(table_name, item.clone())
.make_operation(&conf)
.await
.expect("valid request"),
)
.await
@ -229,6 +232,7 @@ async fn movies_it() {
.call(
movies_in_year(table_name, 2222)
.make_operation(&conf)
.await
.expect("valid request"),
)
.await
@ -240,6 +244,7 @@ async fn movies_it() {
.call(
movies_in_year(table_name, 2013)
.make_operation(&conf)
.await
.expect("valid request"),
)
.await

View File

@ -7,13 +7,14 @@ use aws_endpoint::get_endpoint_resolver;
use aws_sdk_iam::Region;
use http::Uri;
#[test]
fn correct_endpoint_resolver() {
#[tokio::test]
async fn correct_endpoint_resolver() {
let conf = aws_sdk_iam::Config::builder().build();
let operation = aws_sdk_iam::operation::ListRoles::builder()
.build()
.unwrap()
.make_operation(&conf)
.await
.expect("valid operation");
let props = operation.properties();
let resolver = get_endpoint_resolver(&props).expect("operation should have endpoint resolver");

View File

@ -84,6 +84,7 @@ async fn generate_random() {
.build()
.unwrap()
.make_operation(&conf)
.await
.expect("valid operation");
op.properties_mut()
.insert(UNIX_EPOCH + Duration::from_secs(1614952162));
@ -126,6 +127,7 @@ async fn generate_random_malformed_response() {
.build()
.unwrap()
.make_operation(&conf)
.await
.expect("valid operation");
client.call(op).await.expect_err("response was malformed");
}
@ -171,6 +173,7 @@ async fn generate_random_keystore_not_found() {
.build()
.unwrap()
.make_operation(&conf)
.await
.expect("valid operation");
op.properties_mut()

View File

@ -71,6 +71,7 @@ async fn create_alias_op() -> Parts<CreateAlias, AwsErrorRetryPolicy> {
.build()
.unwrap()
.make_operation(&conf)
.await
.expect("valid request")
.into_request_response();
parts

View File

@ -58,6 +58,7 @@ async fn signv4_use_correct_service_name() {
.build()
.unwrap()
.make_operation(&conf)
.await
.expect("valid operation");
// Fix the request time and user agent so the headers are stable
op.properties_mut()

View File

@ -36,6 +36,7 @@ async fn test_signer() -> Result<(), aws_sdk_s3::Error> {
.build()
.unwrap()
.make_operation(&conf)
.await
.unwrap();
op.properties_mut()
.insert(UNIX_EPOCH + Duration::from_secs(1624036048));

View File

@ -47,6 +47,7 @@ import software.amazon.smithy.rust.codegen.smithy.protocols.RestJson
import software.amazon.smithy.rust.codegen.smithy.protocols.parse.JsonParserGenerator
import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.JsonSerializerGenerator
import software.amazon.smithy.rust.codegen.smithy.transformers.errorMessageMember
import software.amazon.smithy.rust.codegen.testutil.TokioTest
import software.amazon.smithy.rust.codegen.util.dq
import software.amazon.smithy.rust.codegen.util.expectTrait
import software.amazon.smithy.rust.codegen.util.findMemberWithTrait
@ -585,8 +586,8 @@ class RestJson1HttpDeserializerGenerator(
}
private fun RustWriter.renderRequestDeserializerTestCase(testCase: HttpRequestTestCase, operationShape: OperationShape) {
Attribute.Custom("test").render(this)
rustBlock("fn ${testCase.id.toSnakeCase()}()") {
TokioTest.render(this)
rustBlock("async fn ${testCase.id.toSnakeCase()}()") {
val inputShape = operationShape.inputShape(model)
val deserFnName = "deser_${operationShape.id.name.toSnakeCase()}_request"
val customToken =
@ -599,7 +600,7 @@ class RestJson1HttpDeserializerGenerator(
instantiator.render(this, inputShape, testCase.params)
write(";")
rust(
"""let op = expected.make_operation(&config).expect("failed to build operation");"""
"""let op = expected.make_operation(&config).await.expect("failed to build operation");"""
)
rust("let (request, parts) = op.into_request_response().0.into_parts();")
rustTemplate(

View File

@ -270,13 +270,13 @@ class RustWriter private constructor(
init {
expressionStart = '#'
if (filename.endsWith(".rs")) {
require(namespace.startsWith("crate")) { "We can only write into files in the crate (got $namespace)" }
require(namespace.startsWith("crate") || filename.startsWith("tests/")) { "We can only write into files in the crate (got $namespace)" }
}
putFormatter('T', formatter)
putFormatter('D', RustDocLinker())
}
fun module(): String? = if (filename.endsWith(".rs")) {
fun module(): String? = if (filename.startsWith("src") && filename.endsWith(".rs")) {
filename.removeSuffix(".rs").split('/').last()
} else null

View File

@ -84,6 +84,8 @@ open class CombinedCodegenDecorator(decorators: List<RustCodegenDecorator>) : Ru
override val order: Byte
get() = 0
fun withDecorator(decorator: RustCodegenDecorator) = CombinedCodegenDecorator(orderedDecorators + decorator)
override fun configCustomizations(
codegenContext: CodegenContext,
baseCustomizations: List<ConfigCustomization>
@ -145,7 +147,7 @@ open class CombinedCodegenDecorator(decorators: List<RustCodegenDecorator>) : Ru
companion object {
private val logger = Logger.getLogger("RustCodegenSPILoader")
fun fromClasspath(context: PluginContext): RustCodegenDecorator {
fun fromClasspath(context: PluginContext): CombinedCodegenDecorator {
val decorators = ServiceLoader.load(
RustCodegenDecorator::class.java,
context.pluginClassLoader.orElse(RustCodegenDecorator::class.java.classLoader)

View File

@ -287,6 +287,7 @@ class FluentClientGenerator(
{
let input = self.inner.build().map_err(|err|#{sdk_err}::ConstructionFailure(err.into()))?;
let op = input.make_operation(&self.handle.conf)
.await
.map_err(|err|#{sdk_err}::ConstructionFailure(err.into()))?;
self.handle.client.call(op).await
}

View File

@ -70,7 +70,7 @@ open class MakeOperationGenerator(
val mut = customizations.any { it.mutSelf() }
val consumes = customizations.any { it.consumesSelf() } || takesOwnership
val self = "self".letIf(mut) { "mut $it" }.letIf(!consumes) { "&$it" }
val fnType = if (public) "pub fn" else "fn"
val fnType = if (public) "pub async fn" else "async fn"
implBlockWriter.docs("Consumes the builder and constructs an Operation<#D>", outputSymbol)
implBlockWriter.rust("##[allow(clippy::let_and_return)]") // For codegen simplicity, allow `let x = ...; x`

View File

@ -21,8 +21,6 @@ import software.amazon.smithy.protocoltests.traits.HttpResponseTestCase
import software.amazon.smithy.protocoltests.traits.HttpResponseTestsTrait
import software.amazon.smithy.rust.codegen.rustlang.Attribute
import software.amazon.smithy.rust.codegen.rustlang.CargoDependency
import software.amazon.smithy.rust.codegen.rustlang.CratesIo
import software.amazon.smithy.rust.codegen.rustlang.DependencyScope
import software.amazon.smithy.rust.codegen.rustlang.RustMetadata
import software.amazon.smithy.rust.codegen.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.rustlang.asType
@ -35,6 +33,7 @@ import software.amazon.smithy.rust.codegen.smithy.CodegenContext
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.smithy.generators.Instantiator
import software.amazon.smithy.rust.codegen.smithy.generators.error.errorSymbol
import software.amazon.smithy.rust.codegen.testutil.TokioTest
import software.amazon.smithy.rust.codegen.util.dq
import software.amazon.smithy.rust.codegen.util.findMemberWithTrait
import software.amazon.smithy.rust.codegen.util.getTrait
@ -144,14 +143,7 @@ class ProtocolTestGenerator(
}
testModuleWriter.write("Test ID: ${testCase.id}")
testModuleWriter.setNewlinePrefix("")
testModuleWriter.writeWithNoFormatting("#[tokio::test]")
val Tokio = CargoDependency(
"tokio",
CratesIo("1"),
features = setOf("macros", "test-util", "rt"),
scope = DependencyScope.Dev
)
testModuleWriter.addDependency(Tokio)
TokioTest.render(testModuleWriter)
val action = when (testCase) {
is HttpResponseTestCase -> Action.Response
is HttpRequestTestCase -> Action.Request
@ -186,7 +178,7 @@ class ProtocolTestGenerator(
writeInline("let input =")
instantiator.render(this, inputShape, httpRequestTestCase.params)
rust(""".make_operation(&config).expect("operation failed to build");""")
rust(""".make_operation(&config).await.expect("operation failed to build");""")
rust("let (http_request, parts) = input.into_request_response().0.into_parts();")
with(httpRequestTestCase) {
host.orNull()?.also { host ->

View File

@ -9,7 +9,12 @@ import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.model.shapes.ShapeId
import software.amazon.smithy.model.shapes.StructureShape
import software.amazon.smithy.rust.codegen.rustlang.Attribute
import software.amazon.smithy.rust.codegen.rustlang.CargoDependency
import software.amazon.smithy.rust.codegen.rustlang.CratesIo
import software.amazon.smithy.rust.codegen.rustlang.DependencyScope
import software.amazon.smithy.rust.codegen.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.rustlang.asType
import software.amazon.smithy.rust.codegen.smithy.CodegenConfig
import software.amazon.smithy.rust.codegen.smithy.CodegenContext
import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig
@ -96,3 +101,12 @@ fun StructureShape.renderWithModelBuilder(model: Model, symbolProvider: RustSymb
modelBuilder.renderConvenienceMethod(this)
}
}
private val Tokio = CargoDependency(
"tokio",
CratesIo("1"),
features = setOf("macros", "test-util", "rt"),
scope = DependencyScope.Dev
)
val TokioTest = Attribute.Custom("tokio::test", listOf(Tokio.asType()))

View File

@ -12,10 +12,14 @@ import software.amazon.smithy.model.traits.EndpointTrait
import software.amazon.smithy.rust.codegen.rustlang.RustModule
import software.amazon.smithy.rust.codegen.rustlang.rust
import software.amazon.smithy.rust.codegen.rustlang.rustBlock
import software.amazon.smithy.rust.codegen.smithy.CodegenContext
import software.amazon.smithy.rust.codegen.smithy.CodegenVisitor
import software.amazon.smithy.rust.codegen.smithy.RustCrate
import software.amazon.smithy.rust.codegen.smithy.customize.CombinedCodegenDecorator
import software.amazon.smithy.rust.codegen.smithy.customize.RustCodegenDecorator
import software.amazon.smithy.rust.codegen.testutil.TestRuntimeConfig
import software.amazon.smithy.rust.codegen.testutil.TestWorkspace
import software.amazon.smithy.rust.codegen.testutil.TokioTest
import software.amazon.smithy.rust.codegen.testutil.asSmithyModel
import software.amazon.smithy.rust.codegen.testutil.compileAndTest
import software.amazon.smithy.rust.codegen.testutil.generatePluginContext
@ -24,8 +28,6 @@ import software.amazon.smithy.rust.codegen.testutil.unitTest
import software.amazon.smithy.rust.codegen.util.lookup
import software.amazon.smithy.rust.codegen.util.runCommand
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.createDirectory
import kotlin.io.path.writeText
internal class EndpointTraitBindingsTest {
@Test
@ -135,30 +137,37 @@ internal class EndpointTraitBindingsTest {
}
""".asSmithyModel()
val (ctx, testDir) = generatePluginContext(model)
val visitor = CodegenVisitor(ctx, CombinedCodegenDecorator.fromClasspath(ctx))
val moduleName = ctx.settings.expectStringMember("module").value.replace('-', '_')
visitor.execute()
testDir.resolve("tests").createDirectory()
testDir.resolve("tests/validate_errors.rs").writeText(
"""
#[test]
fn test_endpoint_prefix() {
let conf = $moduleName::Config::builder().build();
$moduleName::operation::SayHello::builder()
.greeting("hey there!").build().expect("input is valid")
.make_operation(&conf).expect_err("no spaces or exclamation points in ep prefixes");
let op = $moduleName::operation::SayHello::builder()
.greeting("hello")
.build().expect("valid operation")
.make_operation(&conf).expect("hello is a valid prefix");
let properties = op.properties();
let prefix = properties.get::<aws_smithy_http::endpoint::EndpointPrefix>()
.expect("prefix should be in config")
.as_str();
assert_eq!(prefix, "test123.hello.");
val testWriter = object : RustCodegenDecorator {
override val name: String = "add tests"
override val order: Byte = 0
override fun extras(codegenContext: CodegenContext, rustCrate: RustCrate) {
rustCrate.withFile("tests/validate_errors.rs") {
TokioTest.render(it)
it.rust(
"""
async fn test_endpoint_prefix() {
let conf = $moduleName::Config::builder().build();
$moduleName::operation::SayHello::builder()
.greeting("hey there!").build().expect("input is valid")
.make_operation(&conf).await.expect_err("no spaces or exclamation points in ep prefixes");
let op = $moduleName::operation::SayHello::builder()
.greeting("hello")
.build().expect("valid operation")
.make_operation(&conf).await.expect("hello is a valid prefix");
let properties = op.properties();
let prefix = properties.get::<aws_smithy_http::endpoint::EndpointPrefix>()
.expect("prefix should be in config")
.as_str();
assert_eq!(prefix, "test123.hello.");
}
"""
)
}
"""
)
}
}
val visitor = CodegenVisitor(ctx, CombinedCodegenDecorator.fromClasspath(ctx).withDecorator(testWriter))
visitor.execute()
"cargo test".runCommand(testDir)
}
}