Unverified Commit 3f4f44c3 authored by Russell Cohen's avatar Russell Cohen Committed by GitHub
Browse files

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
parent 5fde5282
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -14,7 +14,8 @@ val DECORATORS = listOf(
    UserAgentDecorator(),
    SigV4SigningDecorator(),
    RetryPolicyDecorator(),
    IntegrationTestDecorator()
    IntegrationTestDecorator(),
    FluentClientDecorator()
)

class AwsCodegenDecorator : CombinedCodegenDecorator(DECORATORS) {
+147 −0
Original line number Diff line number Diff line
/*
 * 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")
                        }
                    }
                }
            }
        }
    }
}
+1 −2
Original line number Diff line number Diff line
@@ -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:
+25 −31
Original line number Diff line number Diff line
@@ -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()
        .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())
                .build(&config),
        .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(())
}
+1 −0
Original line number Diff line number Diff line
@@ -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,
Loading