From 1fa0ecfccef7e73a2886937cc1bb7935c4bf4ec0 Mon Sep 17 00:00:00 2001 From: Matteo Bigoi <1781140+crisidev@users.noreply.github.com> Date: Tue, 2 Nov 2021 18:24:35 +0000 Subject: [PATCH] Runtime and client codegen support for improved server protocol dispatcher (#822) * Runtime and client codegen support for improved server protocol dispatcher Implement Rust runtime defining the traits used by the server to build operations parsers/serializers: * ParseHttpRequest * SerializeHttpResponse * SerializeHttpError Refactor ProtocolSupport to allow server configurations: * requestDeserialization (bool) * requestBodyDeserialization (bool) * responseSerialization(bool) * errorSerialization(bool) Change visibility of HttpBoundProtocolGenerator methods to allow to use them in the server generator Co-authored-by: Russell Cohen Co-authored-by: david-perez --- .../rust/codegen/rustlang/CargoDependency.kt | 1 + .../rust/codegen/smithy/RuntimeTypes.kt | 2 +- .../protocol/ProtocolTestGenerator.kt | 8 +- .../rust/codegen/smithy/protocols/AwsJson.kt | 8 +- .../rust/codegen/smithy/protocols/AwsQuery.kt | 6 + .../rust/codegen/smithy/protocols/Ec2Query.kt | 6 + .../protocols/HttpBoundProtocolGenerator.kt | 10 +- .../rust/codegen/smithy/protocols/RestJson.kt | 8 +- .../rust/codegen/smithy/protocols/RestXml.kt | 8 +- .../protocol/ProtocolTestGeneratorTest.kt | 4 +- rust-runtime/Cargo.toml | 3 +- .../aws-smithy-http-server/Cargo.toml | 20 ++++ .../aws-smithy-http-server/src/lib.rs | 9 ++ .../aws-smithy-http-server/src/request.rs | 111 ++++++++++++++++++ .../aws-smithy-http-server/src/response.rs | 105 +++++++++++++++++ 15 files changed, 296 insertions(+), 13 deletions(-) create mode 100644 rust-runtime/aws-smithy-http-server/Cargo.toml create mode 100644 rust-runtime/aws-smithy-http-server/src/lib.rs create mode 100644 rust-runtime/aws-smithy-http-server/src/request.rs create mode 100644 rust-runtime/aws-smithy-http-server/src/response.rs diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/CargoDependency.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/CargoDependency.kt index 921aec4b8..d7a594c4a 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/CargoDependency.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/CargoDependency.kt @@ -201,6 +201,7 @@ data class CargoDependency( fun SmithyClient(runtimeConfig: RuntimeConfig) = runtimeConfig.runtimeCrate("client") fun SmithyEventStream(runtimeConfig: RuntimeConfig) = runtimeConfig.runtimeCrate("eventstream") fun SmithyHttp(runtimeConfig: RuntimeConfig) = runtimeConfig.runtimeCrate("http") + fun SmithyHttpServer(runtimeConfig: RuntimeConfig) = runtimeConfig.runtimeCrate("http-server") fun SmithyHttpTower(runtimeConfig: RuntimeConfig) = runtimeConfig.runtimeCrate("http-tower") fun SmithyProtocolTestHelpers(runtimeConfig: RuntimeConfig) = diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt index b1832103a..0d648de50 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt @@ -266,7 +266,7 @@ data class RuntimeType(val name: String?, val dependency: RustDependency?, val n fun sdkBody(runtimeConfig: RuntimeConfig): RuntimeType = RuntimeType("SdkBody", dependency = CargoDependency.SmithyHttp(runtimeConfig), "aws_smithy_http::body") - fun parseStrict(runtimeConfig: RuntimeConfig) = RuntimeType( + fun parseStrictResponse(runtimeConfig: RuntimeConfig) = RuntimeType( "ParseStrictResponse", dependency = CargoDependency.SmithyHttp(runtimeConfig), namespace = "aws_smithy_http::response" diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/protocol/ProtocolTestGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/protocol/ProtocolTestGenerator.kt index 1ca23f608..f175d591e 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/protocol/ProtocolTestGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/protocol/ProtocolTestGenerator.kt @@ -46,10 +46,16 @@ import software.amazon.smithy.rust.codegen.util.toSnakeCase import java.util.logging.Logger data class ProtocolSupport( + /* Client support */ val requestSerialization: Boolean, val requestBodySerialization: Boolean, val responseDeserialization: Boolean, - val errorDeserialization: Boolean + val errorDeserialization: Boolean, + /* Server support */ + val requestDeserialization: Boolean, + val requestBodyDeserialization: Boolean, + val responseSerialization: Boolean, + val errorSerialization: Boolean ) /** diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsJson.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsJson.kt index 3dd32fe1c..f255db693 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsJson.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsJson.kt @@ -48,10 +48,16 @@ class AwsJsonFactory(private val version: AwsJsonVersion) : ProtocolGeneratorFac override fun transformModel(model: Model): Model = model override fun support(): ProtocolSupport = ProtocolSupport( + /* Client support */ requestSerialization = true, requestBodySerialization = true, responseDeserialization = true, - errorDeserialization = true + errorDeserialization = true, + /* Server support */ + requestDeserialization = false, + requestBodyDeserialization = false, + responseSerialization = false, + errorSerialization = false ) } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsQuery.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsQuery.kt index 4bbd21738..0ecb02033 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsQuery.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsQuery.kt @@ -36,10 +36,16 @@ class AwsQueryFactory : ProtocolGeneratorFactory { override fun support(): ProtocolSupport { return ProtocolSupport( + /* Client support */ requestSerialization = true, requestBodySerialization = true, responseDeserialization = true, errorDeserialization = true, + /* Server support */ + requestDeserialization = false, + requestBodyDeserialization = false, + responseSerialization = false, + errorSerialization = false ) } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/Ec2Query.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/Ec2Query.kt index de573752b..85a573525 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/Ec2Query.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/Ec2Query.kt @@ -33,10 +33,16 @@ class Ec2QueryFactory : ProtocolGeneratorFactory { override fun support(): ProtocolSupport { return ProtocolSupport( + /* Client support */ requestSerialization = true, requestBodySerialization = true, responseDeserialization = true, errorDeserialization = true, + /* Server support */ + requestDeserialization = false, + requestBodyDeserialization = false, + responseSerialization = false, + errorSerialization = false ) } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/HttpBoundProtocolGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/HttpBoundProtocolGenerator.kt index 45db83784..483ca91a5 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/HttpBoundProtocolGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/HttpBoundProtocolGenerator.kt @@ -69,7 +69,7 @@ class HttpBoundProtocolGenerator( HttpBoundProtocolTraitImplGenerator(codegenContext, protocol), ) -private class HttpBoundProtocolTraitImplGenerator( +class HttpBoundProtocolTraitImplGenerator( private val codegenContext: CodegenContext, private val protocol: Protocol, ) : ProtocolTraitImplGenerator { @@ -80,7 +80,7 @@ private class HttpBoundProtocolTraitImplGenerator( private val operationDeserModule = RustModule.private("operation_deser") private val codegenScope = arrayOf( - "ParseStrict" to RuntimeType.parseStrict(runtimeConfig), + "ParseStrict" to RuntimeType.parseStrictResponse(runtimeConfig), "ParseResponse" to RuntimeType.parseResponse(runtimeConfig), "http" to RuntimeType.http, "operation" to RuntimeType.operationModule(runtimeConfig), @@ -162,7 +162,7 @@ private class HttpBoundProtocolTraitImplGenerator( ) } - private fun parseError(operationShape: OperationShape): RuntimeType { + fun parseError(operationShape: OperationShape): RuntimeType { val fnName = "parse_${operationShape.id.name.toSnakeCase()}_error" val outputShape = operationShape.outputShape(model) val outputSymbol = symbolProvider.toSymbol(outputShape) @@ -260,7 +260,7 @@ private class HttpBoundProtocolTraitImplGenerator( } } - private fun parseResponse(operationShape: OperationShape): RuntimeType { + fun parseResponse(operationShape: OperationShape): RuntimeType { val fnName = "parse_${operationShape.id.name.toSnakeCase()}_response" val outputShape = operationShape.outputShape(model) val outputSymbol = symbolProvider.toSymbol(outputShape) @@ -418,7 +418,7 @@ class HttpBoundProtocolBodyGenerator( private val operationSerModule = RustModule.private("operation_ser") private val codegenScope = arrayOf( - "ParseStrict" to RuntimeType.parseStrict(runtimeConfig), + "ParseStrict" to RuntimeType.parseStrictResponse(runtimeConfig), "ParseResponse" to RuntimeType.parseResponse(runtimeConfig), "http" to RuntimeType.http, "hyper" to CargoDependency.HyperWithStream.asType(), diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestJson.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestJson.kt index 74dd59712..b5e863a40 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestJson.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestJson.kt @@ -30,10 +30,16 @@ class RestJsonFactory : ProtocolGeneratorFactory { override fun support(): ProtocolSupport { return ProtocolSupport( + /* Client support */ requestSerialization = true, requestBodySerialization = true, responseDeserialization = true, - errorDeserialization = true + errorDeserialization = true, + /* Server support */ + requestDeserialization = false, + requestBodyDeserialization = false, + responseSerialization = false, + errorSerialization = false ) } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestXml.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestXml.kt index 655f2ca51..bf9effc38 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestXml.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestXml.kt @@ -34,10 +34,16 @@ class RestXmlFactory(private val generator: (CodegenContext) -> Protocol = { Res override fun support(): ProtocolSupport { return ProtocolSupport( + /* Client support */ requestSerialization = true, requestBodySerialization = true, responseDeserialization = true, - errorDeserialization = true + errorDeserialization = true, + /* Server support */ + requestDeserialization = false, + requestBodyDeserialization = false, + responseSerialization = false, + errorSerialization = false ) } } diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/protocol/ProtocolTestGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/protocol/ProtocolTestGeneratorTest.kt index 1f645bbab..d8da5f9fd 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/protocol/ProtocolTestGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/protocol/ProtocolTestGeneratorTest.kt @@ -59,7 +59,7 @@ private class TestProtocolTraitImplGenerator( ${operationWriter.escape(correctResponse)} } }""", - "parse_strict" to RuntimeType.parseStrict(codegenContext.runtimeConfig), + "parse_strict" to RuntimeType.parseStrictResponse(codegenContext.runtimeConfig), "output" to symbolProvider.toSymbol(operationShape.outputShape(codegenContext.model)), "error" to operationShape.errorSymbol(symbolProvider), "response" to RuntimeType.Http("Response"), @@ -119,7 +119,7 @@ private class TestProtocolFactory( override fun transformModel(model: Model): Model = model override fun support(): ProtocolSupport { - return ProtocolSupport(true, true, true, true) + return ProtocolSupport(true, true, true, true, false, false, false, false) } } diff --git a/rust-runtime/Cargo.toml b/rust-runtime/Cargo.toml index 00cef113c..bce0a7421 100644 --- a/rust-runtime/Cargo.toml +++ b/rust-runtime/Cargo.toml @@ -11,5 +11,6 @@ members = [ "aws-smithy-protocol-test", "aws-smithy-query", "aws-smithy-types", - "aws-smithy-xml" + "aws-smithy-xml", + "aws-smithy-http-server" ] diff --git a/rust-runtime/aws-smithy-http-server/Cargo.toml b/rust-runtime/aws-smithy-http-server/Cargo.toml new file mode 100644 index 000000000..9464502e9 --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "aws-smithy-http-server" +version = "0.1.0" +authors = ["Smithy Rust Server "] +edition = "2018" +description = """ +Server traits and utilities used by the code generator. + +NOTE: THIS IS HIGHLY EXPERIMENTAL AND SHOULD NOT BE USED YET. +""" +# until this is not stable, it is not publishable. +publish = false + +[dependencies] +aws-smithy-http = { path = "../aws-smithy-http" } +bytes = "1.1" +http = "0.2" + +[dev-dependencies] +pretty_assertions = "1" diff --git a/rust-runtime/aws-smithy-http-server/src/lib.rs b/rust-runtime/aws-smithy-http-server/src/lib.rs new file mode 100644 index 000000000..3a3c5bb9f --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/src/lib.rs @@ -0,0 +1,9 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#![cfg_attr(docsrs, feature(doc_cfg))] + +pub mod request; +pub mod response; diff --git a/rust-runtime/aws-smithy-http-server/src/request.rs b/rust-runtime/aws-smithy-http-server/src/request.rs new file mode 100644 index 000000000..82976bdd2 --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/src/request.rs @@ -0,0 +1,111 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +//! Parse HTTP request trait + +use aws_smithy_http::body::SdkBody; +use bytes::Bytes; + +/// `ParseHttpRequest` is a generic trait for parsing structured data from HTTP requests. +/// +/// It is designed to be flexible, because `Input` is unconstrained, it can be used to support +/// event streams, regular request-response style operations, as well as any other HTTP-based +/// protocol that we manage to come up with. +/// +/// It also enables this critical and core trait to avoid being async, and it makes code that uses +/// the trait easier to test. +pub trait ParseHttpRequest { + /// Input type of the HttpRequest + /// + /// For request/response style operations, this is typically something like: + /// `Result` + /// + /// For streaming operations, this is something like: + /// `Result>, Error>` + type Input; + + /// Parse an HTTP request without reading the body. If the body must be provided to proceed, + /// return `None` + /// + /// This exists to serve APIs like S3::GetObject where the body is passed directly into the + /// response and consumed by the client. However, even in the case of S3::GetObject, errors + /// require reading the entire body. + /// + /// This also facilitates `EventStream` and other streaming HTTP protocols by enabling the + /// handler to take ownership of the HTTP request directly. + /// + /// Currently `parse_unloaded` operates on a borrowed HTTP request to enable + /// the caller to provide a raw HTTP response to the caller for inspection after the response is + /// returned. For EventStream-like use cases, the caller can use `mem::swap` to replace + /// the streaming body with an empty body as long as the body implements `std::default::Default`. + /// + /// We should consider if this is too limiting and if this should take an owned request instead. + fn parse_unloaded(&self, request: &mut http::Request) -> Option; + + /// Parse an HTTP request from a fully loaded body. This is for standard request/response style + /// APIs like RestJson1. + /// + /// Using an explicit body type of Bytes here is a conscious decision—If you _really_ need + /// to precisely control how the data is loaded into memory (eg. by using `bytes::Buf`), implement + /// your handler in `parse_unloaded`. + /// + /// Production code will never call `parse_loaded` without first calling `parse_unloaded`. However, + /// in tests it may be easier to use `parse_loaded` directly. It is OK to panic in `parse_loaded` + /// if `parse_unloaded` will never return `None`, however, it may make your code easier to test if an + /// implementation is provided. + fn parse_loaded(&self, request: &http::Request) -> Self::Input; +} + +#[cfg(test)] +mod test { + use super::*; + use bytes::Bytes; + use std::mem; + + #[test] + fn support_non_streaming_body() { + pub struct S3GetObject { + pub body: Bytes, + } + + struct S3GetObjectParser; + + impl ParseHttpRequest for S3GetObjectParser { + type Input = S3GetObject; + + fn parse_unloaded(&self, _request: &mut http::Request) -> Option { + None + } + + fn parse_loaded(&self, request: &http::Request) -> Self::Input { + S3GetObject { + body: request.body().clone(), + } + } + } + } + #[test] + fn supports_streaming_body() { + pub struct S3GetObject { + pub body: SdkBody, + } + + struct S3GetObjectParser; + + impl ParseHttpRequest for S3GetObjectParser { + type Input = S3GetObject; + + fn parse_unloaded(&self, request: &mut http::Request) -> Option { + // For responses that pass on the body, use mem::take to leave behind an empty body + let body = mem::replace(request.body_mut(), SdkBody::taken()); + Some(S3GetObject { body }) + } + + fn parse_loaded(&self, _request: &http::Request) -> Self::Input { + unimplemented!() + } + } + } +} diff --git a/rust-runtime/aws-smithy-http-server/src/response.rs b/rust-runtime/aws-smithy-http-server/src/response.rs new file mode 100644 index 000000000..ef74d8bd6 --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/src/response.rs @@ -0,0 +1,105 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +//! Serialize HTTP response and error traits + +/// `SerializeHttpResponse` is a generic trait for serializing structured data into HTTP responses. +/// +/// It is designed to be flexible, because `Output` and `Struct` are unconstrained, it can be used to support +/// event streams, regular request-response style operations, as well as any other HTTP-based +/// protocol that we manage to come up with. +/// +/// It also enables this critical and core trait to avoid being async, and it makes code that uses +/// the trait easier to test. +/// +/// TODO: streaming and not-fully loaded body serialization is no developed yet +pub trait SerializeHttpResponse { + /// Struct instance to be serialized into the HTTP body + /// + /// For request/response style operations, this is typically something like: + /// `OperationOutput` + type Struct; + /// HTTP response output + /// + /// For request/response style operations, this is typically something like: + /// `Result` + type Output; + + /// Serialize an HTTP response from a fully loaded body. This is for standard request/response style + /// protocols like RestJson1. + fn serialize(&self, output: &Self::Struct) -> Self::Output; +} + +/// `SerializeHttpError` is a generic trait for serializing structured errors into HTTP responses. +/// +/// It is designed to be flexible, because `Output` and `Struct` are unconstrained, it can be used to support +/// event streams, regular request-response style operations, as well as any other HTTP-based +/// protocol that we manage to come up with. +/// +/// It also enables this critical and core trait to avoid being async, and it makes code that uses +/// the trait easier to test. +/// +/// TODO: streaming and not-fully loaded body serialization is not developed yet +pub trait SerializeHttpError { + /// Struct instance to be serialized into the HTTP body + /// + /// For request/response style operations, this is typically something like: + /// `OperationError` + type Struct; + /// HTTP response output + /// + /// For request/response style operations, this is typically something like: + /// `Result` + type Output; + + /// Serialize an HTTP response from a fully loaded body. This is for standard request/response style + /// protocols like RestJson1. + fn serialize(&self, error: &Self::Struct) -> Self::Output; +} + +#[cfg(test)] +mod test { + use super::*; + use bytes::Bytes; + + #[test] + fn support_non_streaming_body() { + pub struct S3GetObject { + pub body: Bytes, + } + + #[allow(dead_code)] + pub enum S3Error { + Something, + } + + impl S3Error { + fn as_bytes(&self) -> Bytes { + match self { + S3Error::Something => Bytes::from_static(b"something"), + } + } + } + + struct S3GetObjectParser; + + impl SerializeHttpResponse for S3GetObjectParser { + type Struct = S3GetObject; + type Output = http::Response; + + fn serialize(&self, output: &Self::Struct) -> Self::Output { + http::Response::builder().body(output.body.clone()).unwrap() + } + } + + impl SerializeHttpError for S3GetObjectParser { + type Struct = S3Error; + type Output = http::Response; + + fn serialize(&self, error: &Self::Struct) -> Self::Output { + http::Response::builder().body(error.as_bytes()).unwrap() + } + } + } +} -- GitLab