From 3f4f44c323b3894b05c4a0835c9421c58e6191c6 Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Tue, 16 Mar 2021 18:50:50 -0400 Subject: [PATCH] Add Fluent API (#251) * Fully qualify types from models For a long time, we've been fully qualifying depdency types but not model types. This produces code that's fine, but it's always been a confusing inconsistency and it posed problems trying to render Opaque types that specified namespaces (because the namespace was ignored). This removes that inconsistency. * A few more small refactorings * Fix test failures * Wip fluent API * Fluent builders working! * fix handle visibility * Delete unused imports * Lots of refactorings & making the fluent client an optional feature * Cleanup module handling & add support for Cargo features * Fix AWS tests * Remove unused modules customization * Set optional in the Cargo toml * .execute() -> .send() * Fix issues from refactoring out cargo features implementation --- .../smithy/rustsdk/AwsCodegenDecorator.kt | 3 +- .../smithy/rustsdk/FluentClientGenerator.kt | 147 ++++++++++++++++++ aws/sdk/examples/dynamo-helloworld/Cargo.toml | 3 +- .../examples/dynamo-helloworld/src/main.rs | 56 +++---- .../rust/codegen/smithy/CodegenVisitor.kt | 1 + .../smithy/customize/RustCodegenDecorator.kt | 7 + .../smithy/rust/codegen/testutil/Rust.kt | 2 +- 7 files changed, 184 insertions(+), 35 deletions(-) create mode 100644 aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/FluentClientGenerator.kt diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt index 93bdf1449b..de6e8ddeb8 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt @@ -14,7 +14,8 @@ val DECORATORS = listOf( UserAgentDecorator(), SigV4SigningDecorator(), RetryPolicyDecorator(), - IntegrationTestDecorator() + IntegrationTestDecorator(), + FluentClientDecorator() ) class AwsCodegenDecorator : CombinedCodegenDecorator(DECORATORS) { diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/FluentClientGenerator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/FluentClientGenerator.kt new file mode 100644 index 0000000000..484124e16b --- /dev/null +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/FluentClientGenerator.kt @@ -0,0 +1,147 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package software.amazon.smithy.rustsdk + +import software.amazon.smithy.model.knowledge.TopDownIndex +import software.amazon.smithy.model.shapes.MemberShape +import software.amazon.smithy.rust.codegen.rustlang.Attribute +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.RustType +import software.amazon.smithy.rust.codegen.rustlang.RustWriter +import software.amazon.smithy.rust.codegen.rustlang.asType +import software.amazon.smithy.rust.codegen.rustlang.documentShape +import software.amazon.smithy.rust.codegen.rustlang.render +import software.amazon.smithy.rust.codegen.rustlang.rust +import software.amazon.smithy.rust.codegen.rustlang.rustBlock +import software.amazon.smithy.rust.codegen.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.rustlang.stripOuter +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.ProtocolConfig +import software.amazon.smithy.rust.codegen.smithy.generators.builderSymbol +import software.amazon.smithy.rust.codegen.smithy.generators.errorSymbol +import software.amazon.smithy.rust.codegen.smithy.rustType +import software.amazon.smithy.rust.codegen.util.inputShape +import software.amazon.smithy.rust.codegen.util.outputShape +import software.amazon.smithy.rust.codegen.util.toSnakeCase + +class FluentClientDecorator : RustCodegenDecorator { + override val name: String = "FluentClient" + override val order: Byte = 0 + + override fun extras(protocolConfig: ProtocolConfig, rustCrate: RustCrate) { + val module = RustMetadata(additionalAttributes = listOf(Attribute.Cfg.feature("fluent")), public = true) + rustCrate.withModule(RustModule("fluent", module)) { writer -> + FluentClientGenerator(protocolConfig).render(writer) + } + rustCrate.addFeature(Feature("fluent", true, listOf(protocolConfig.runtimeConfig.awsHyper().name))) + } +} + +class FluentClientGenerator(protocolConfig: ProtocolConfig) { + private val serviceShape = protocolConfig.serviceShape + private val operations = + TopDownIndex.of(protocolConfig.model).getContainedOperations(serviceShape).sortedBy { it.id } + private val symbolProvider = protocolConfig.symbolProvider + private val model = protocolConfig.model + private val hyperDep = protocolConfig.runtimeConfig.awsHyper().copy(optional = true) + private val runtimeConfig = protocolConfig.runtimeConfig + + fun render(writer: RustWriter) { + writer.rustTemplate( + """ + pub(crate) struct Handle { + client: #{aws_hyper}::Client<#{aws_hyper}::conn::Standard>, + conf: crate::Config + } + + pub struct Client { + handle: std::sync::Arc + } + """, + "aws_hyper" to hyperDep.asType() + ) + writer.rustBlock("impl Client") { + rustTemplate( + """ + pub fn from_env() -> Self { + Self::from_conf_conn(crate::Config::builder().build(), #{aws_hyper}::conn::Standard::https()) + } + + pub fn from_conf_conn(conf: crate::Config, conn: #{aws_hyper}::conn::Standard) -> Self { + let client = #{aws_hyper}::Client::new(conn); + Self { handle: std::sync::Arc::new(Handle { conf, client })} + } + + """, + "aws_hyper" to hyperDep.asType() + ) + operations.forEach { operation -> + val name = symbolProvider.toSymbol(operation).name + rust( + """ + pub fn ${name.toSnakeCase()}(&self) -> fluent_builders::$name { + fluent_builders::$name::new(self.handle.clone()) + }""" + ) + } + } + writer.withModule("fluent_builders") { + operations.forEach { operation -> + val name = symbolProvider.toSymbol(operation).name + val input = operation.inputShape(model) + val members: List = input.allMembers.values.toList() + + rust( + """ + pub struct $name { + handle: std::sync::Arc, + inner: #T + }""", + input.builderSymbol(symbolProvider) + ) + + rustBlock("impl $name") { + rustTemplate( + """ + pub(crate) fn new(handle: std::sync::Arc) -> Self { + Self { handle, inner: Default::default() } + } + + pub async fn send(self) -> Result<#{ok}, #{sdk_err}<#{operation_err}>> { + let op = self.inner.build(&self.handle.conf); + self.handle.client.call(op).await + } + """, + "ok" to symbolProvider.toSymbol(operation.outputShape(model)), + "operation_err" to operation.errorSymbol(symbolProvider), + "sdk_err" to CargoDependency.SmithyHttp(runtimeConfig).asType().copy(name = "result::SdkError") + ) + members.forEach { member -> + val memberName = symbolProvider.toMemberName(member) + // All fields in the builder are optional + val memberSymbol = symbolProvider.toSymbol(member) + val outerType = memberSymbol.rustType() + val coreType = outerType.stripOuter() + val signature = when (coreType) { + is RustType.String, + is RustType.Box -> "(mut self, inp: impl Into<${coreType.render(true)}>) -> Self" + else -> "(mut self, inp: ${coreType.render(true)}) -> Self" + } + documentShape(member, model) + rustBlock("pub fn $memberName$signature") { + write("self.inner = self.inner.$memberName(inp);") + write("self") + } + } + } + } + } + } +} diff --git a/aws/sdk/examples/dynamo-helloworld/Cargo.toml b/aws/sdk/examples/dynamo-helloworld/Cargo.toml index 643466c25e..472fe394b2 100644 --- a/aws/sdk/examples/dynamo-helloworld/Cargo.toml +++ b/aws/sdk/examples/dynamo-helloworld/Cargo.toml @@ -7,8 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dynamodb = { path = "../../build/aws-sdk/dynamodb" } -aws-hyper = { path = "../../build/aws-sdk/aws-hyper"} +dynamodb = { path = "../../build/aws-sdk/dynamodb", features = ["fluent"] } tokio = { version = "1", features = ["full"] } # used only for static endpoint configuration: diff --git a/aws/sdk/examples/dynamo-helloworld/src/main.rs b/aws/sdk/examples/dynamo-helloworld/src/main.rs index c9c983b032..3b896fdc74 100644 --- a/aws/sdk/examples/dynamo-helloworld/src/main.rs +++ b/aws/sdk/examples/dynamo-helloworld/src/main.rs @@ -5,45 +5,39 @@ use std::error::Error; -use dynamodb::operation::{CreateTable, ListTables}; -use dynamodb::{Credentials, Endpoint, Region}; -use env_logger::Env; -use dynamodb::model::{KeySchemaElement, KeyType, ProvisionedThroughput, AttributeDefinition, ScalarAttributeType}; +use dynamodb::model::{ + AttributeDefinition, KeySchemaElement, KeyType, ProvisionedThroughput, ScalarAttributeType, +}; #[tokio::main] async fn main() -> Result<(), Box> { - env_logger::init_from_env(Env::default().default_filter_or("info")); - println!("DynamoDB client version: {}", dynamodb::PKG_VERSION); - let config = dynamodb::Config::builder() - .region(Region::new("us-east-1")) - // To load credentials from environment variables, delete this line - .credentials_provider(Credentials::from_keys( - "", - "", - None, - )) - // To use real DynamoDB, delete this line: - .endpoint_resolver(Endpoint::immutable(http::Uri::from_static( - "http://localhost:8000", - ))) - .build(); - let client = aws_hyper::Client::https(); + let client = dynamodb::fluent::Client::from_env(); + let tables = client.list_tables().send().await?; - let op = ListTables::builder().build(&config); - // Currently this fails, pending the merge of https://github.com/awslabs/smithy-rs/pull/202 - let tables = client.call(op).await?; println!("Current DynamoDB tables: {:?}", tables); let new_table = client - .call( - CreateTable::builder() - .table_name("test-table") - .key_schema(vec![KeySchemaElement::builder().attribute_name("k").key_type(KeyType::Hash).build()]) - .attribute_definitions(vec![AttributeDefinition::builder().attribute_name("k").attribute_type(ScalarAttributeType::S).build()]) - .provisioned_throughput(ProvisionedThroughput::builder().write_capacity_units(10).read_capacity_units(10).build()) - .build(&config), + .create_table() + .table_name("test-table") + .key_schema(vec![KeySchemaElement::builder() + .attribute_name("k") + .key_type(KeyType::Hash) + .build()]) + .attribute_definitions(vec![AttributeDefinition::builder() + .attribute_name("k") + .attribute_type(ScalarAttributeType::S) + .build()]) + .provisioned_throughput( + ProvisionedThroughput::builder() + .write_capacity_units(10) + .read_capacity_units(10) + .build(), ) + .send() .await?; - println!("new table: {:#?}", &new_table.table_description.unwrap().table_arn.unwrap()); + println!( + "new table: {:#?}", + &new_table.table_description.unwrap().table_arn.unwrap() + ); Ok(()) } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenVisitor.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenVisitor.kt index f40f580f10..272cf2d6c3 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenVisitor.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenVisitor.kt @@ -81,6 +81,7 @@ class CodegenVisitor(context: PluginContext, private val codegenDecorator: RustC val service = settings.getService(model) val serviceShapes = Walker(model).walkShapes(service) serviceShapes.forEach { it.accept(this) } + codegenDecorator.extras(protocolConfig, rustCrate) // TODO: if we end up with a lot of these on-by-default customizations, we may want to refactor them somewhere rustCrate.finalize( settings, diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customize/RustCodegenDecorator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customize/RustCodegenDecorator.kt index a794739d17..6a663ecf62 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customize/RustCodegenDecorator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customize/RustCodegenDecorator.kt @@ -8,6 +8,7 @@ package software.amazon.smithy.rust.codegen.smithy.customize import software.amazon.smithy.build.PluginContext import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.rust.codegen.smithy.RustCrate import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider import software.amazon.smithy.rust.codegen.smithy.generators.LibRsCustomization import software.amazon.smithy.rust.codegen.smithy.generators.OperationCustomization @@ -51,6 +52,8 @@ interface RustCodegenDecorator { baseCustomizations: List ): List = baseCustomizations + fun extras(protocolConfig: ProtocolConfig, rustCrate: RustCrate) {} + fun protocols(serviceId: ShapeId, currentProtocols: ProtocolMap): ProtocolMap = currentProtocols fun symbolProvider(baseProvider: RustSymbolProvider): RustSymbolProvider = baseProvider @@ -109,6 +112,10 @@ open class CombinedCodegenDecorator(decorators: List) : Ru return orderedDecorators.foldRight(baseProvider) { decorator, provider -> decorator.symbolProvider(provider) } } + override fun extras(protocolConfig: ProtocolConfig, rustCrate: RustCrate) { + return orderedDecorators.forEach { it.extras(protocolConfig, rustCrate) } + } + companion object { private val logger = Logger.getLogger("RustCodegenSPILoader") fun fromClasspath(context: PluginContext): RustCodegenDecorator { diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/Rust.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/Rust.kt index 46c18d1d38..9dde86206e 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/Rust.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/Rust.kt @@ -165,7 +165,7 @@ fun TestWriterDelegator.compileAndTest() { build = BuildSettings.Default(), model = stubModel, ), - libRsCustomizations = listOf() + libRsCustomizations = listOf(), ) "cargo test".runCommand(baseDir, mapOf("RUSTFLAGS" to "-A dead_code")) }