Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Fluent API #251

Merged
merged 20 commits into from
Mar 16, 2021
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.CargoDependency
import software.amazon.smithy.rust.codegen.rustlang.Cfg
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(Cfg("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 execute(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")
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ internal class EndpointConfigCustomizationTest {
@Test
fun `write an endpoint into the config`() {
val project = stubConfigProject(EndpointConfigCustomization(TestRuntimeConfig, model.lookup("test#TestService")))
project.useFileWriter("src/lib.rs", "crate") {
project.lib {
it.addDependency(awsTypes(TestRuntimeConfig))
it.addDependency(CargoDependency.Http)
it.unitTest(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ internal class SigV4SigningCustomizationTest {
@Test
fun `generates a valid config`() {
val project = stubConfigProject(SigV4SigningConfig(SigV4Trait.builder().name("test-service").build()))
project.useFileWriter("src/lib.rs", "crate") {
project.lib {
it.unitTest(
"""
let conf = crate::config::Config::builder().build();
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().execute().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(),
)
.execute()
rcoh marked this conversation as resolved.
Show resolved Hide resolved
.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 @@ -106,13 +106,16 @@ class InlineDependency(
fun CargoDependency.asType(): RuntimeType =
RuntimeType(null, dependency = this, namespace = this.name.replace("-", "_"))

data class Feature(val name: String, val default: Boolean, val deps: List<String>)

/**
* A dependency on an internal or external Cargo Crate
*/
data class CargoDependency(
override val name: String,
private val location: DependencyLocation,
val scope: DependencyScope = DependencyScope.Compile,
val optional: Boolean = false,
private val features: List<String> = listOf()
) : RustDependency(name) {

Expand All @@ -137,6 +140,9 @@ data class CargoDependency(
attribs["features"] = this
}
}
if (optional) {
attribs["optional"] = true
}
return attribs
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,8 @@ data class RustModule(val name: String, val rustMetadata: RustMetadata) {
}*/
return RustModule(name, RustMetadata(public = public))
}

val Config = default("config", public = true)
val Error = default("error", public = true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package software.amazon.smithy.rust.codegen.rustlang

import software.amazon.smithy.rust.codegen.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.util.dq

/**
* A hierarchy of types handled by Smithy codegen
Expand Down Expand Up @@ -149,14 +150,15 @@ inline fun <reified T : RustType.Container> RustType.stripOuter(): RustType {
* Meta information about a Rust construction (field, struct, or enum)
*/
data class RustMetadata(
val derives: Derives = Derives.Empty,
val derives: Attribute.Derives = Attribute.Derives.Empty,
val additionalAttributes: List<Attribute> = listOf(),
val public: Boolean
) {
fun withDerives(vararg newDerive: RuntimeType): RustMetadata =
this.copy(derives = derives.copy(derives = derives.derives + newDerive))

fun attributes(): List<Attribute> = additionalAttributes + derives
private fun attributes(): List<Attribute> = additionalAttributes + derives

fun renderAttributes(writer: RustWriter): RustMetadata {
attributes().forEach {
it.render(writer)
Expand Down Expand Up @@ -201,33 +203,40 @@ sealed class Attribute {
val NonExhaustive = Custom("non_exhaustive")
val AllowUnused = Custom("allow(dead_code)")
}
}

data class Derives(val derives: Set<RuntimeType>) : Attribute() {
override fun render(writer: RustWriter) {
if (derives.isEmpty()) {
return
data class Derives(val derives: Set<RuntimeType>) : Attribute() {
override fun render(writer: RustWriter) {
if (derives.isEmpty()) {
return
}
writer.raw("#[derive(")
derives.sortedBy { it.name }.forEach { derive ->
writer.writeInline("#T, ", derive)
}
writer.write(")]")
}
writer.raw("#[derive(")
derives.sortedBy { it.name }.forEach { derive ->
writer.writeInline("#T, ", derive)

companion object {
val Empty = Derives(setOf())
}
writer.write(")]")
}

companion object {
val Empty = Derives(setOf())
data class Custom(val annotation: String, val symbols: List<RuntimeType> = listOf()) : Attribute() {
override fun render(writer: RustWriter) {
writer.raw("#[$annotation]")
symbols.forEach {
writer.addDependency(it.dependency)
}
}
}
}

data class Custom(val annot: String, val symbols: List<RuntimeType> = listOf()) : Attribute() {
override fun render(writer: RustWriter) {
writer.raw("#[")
writer.writeInline(annot)
writer.write("]")
data class Cfg(val cond: String) : Attribute() {
override fun render(writer: RustWriter) {
writer.raw("#[cfg($cond)]")
}

symbols.forEach {
writer.addDependency(it.dependency)
companion object {
fun feature(feature: String) = Cfg("feature = ${feature.dq()}")
}
}
}
Loading