Skip to content

Commit

Permalink
Add Fluent API (#251)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
rcoh authored Mar 16, 2021
1 parent 5fde528 commit 3f4f44c
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ val DECORATORS = listOf(
UserAgentDecorator(),
SigV4SigningDecorator(),
RetryPolicyDecorator(),
IntegrationTestDecorator()
IntegrationTestDecorator(),
FluentClientDecorator()
)

class AwsCodegenDecorator : CombinedCodegenDecorator(DECORATORS) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Handle>
}
""",
"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<MemberShape> = input.allMembers.values.toList()

rust(
"""
pub struct $name {
handle: std::sync::Arc<super::Handle>,
inner: #T
}""",
input.builderSymbol(symbolProvider)
)

rustBlock("impl $name") {
rustTemplate(
"""
pub(crate) fn new(handle: std::sync::Arc<super::Handle>) -> 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<RustType.Option>()
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")
}
}
}
}
}
}
}
3 changes: 1 addition & 2 deletions aws/sdk/examples/dynamo-helloworld/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
56 changes: 25 additions & 31 deletions aws/sdk/examples/dynamo-helloworld/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn Error>> {
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(
"<fill me in2>",
"<fill me in>",
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(())
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -51,6 +52,8 @@ interface RustCodegenDecorator {
baseCustomizations: List<LibRsCustomization>
): List<LibRsCustomization> = baseCustomizations

fun extras(protocolConfig: ProtocolConfig, rustCrate: RustCrate) {}

fun protocols(serviceId: ShapeId, currentProtocols: ProtocolMap): ProtocolMap = currentProtocols

fun symbolProvider(baseProvider: RustSymbolProvider): RustSymbolProvider = baseProvider
Expand Down Expand Up @@ -109,6 +112,10 @@ open class CombinedCodegenDecorator(decorators: List<RustCodegenDecorator>) : 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}
Expand Down

0 comments on commit 3f4f44c

Please sign in to comment.