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 921aec4b85b1191772853d67a3cd13870209046c..d7a594c4a659508049fcc7fb5ecf54529f47265d 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 b1832103ade65d1b0ab58110e2976c0963d0e728..0d648de507988e9d9235124c4a138b96643cbb96 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 1ca23f608b5a663f73d976bf4234edde473e323d..f175d591e3137b13a8f7285563a2a53d23e63a53 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 3dd32fe1ceda3460fdaa0515b51ceede14ad9c72..f255db693e0ed54d407c081ae7b9684df960d628 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 4bbd217388f862ec1e7f2707a693a7d599b05d57..0ecb02033f220c632161b3a8a6c6c02c403fc61b 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 de573752b84820ebb5f298c1d173bf444e887da8..85a573525c19c7f43b28bd701177b5e0617902b8 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 45db83784952411011f5a6b3417064c427aeaac5..483ca91a597dd7c2f5495dbc7d4ac7a69c83a383 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 74dd59712530cc07d63e01e996485be144880d28..b5e863a40ba1bc61f072d7505d6e145f4e0b3898 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 655f2ca51d86cd78277d7fd96de5bceff34e539d..bf9effc384e0c8fe7f806fcbb611e1e84821d5cf 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 1f645bbabc817dd06f2fffdb5987c0616e831b06..d8da5f9fdcef7b302d1a1bcbf9619055621d82d8 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 00cef113c023f8e2ecbb0b3766768f79dc32a459..bce0a7421826761d37cdce441aef2880a1aa5eee 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 0000000000000000000000000000000000000000..9464502e990647565947d1c497f41c92e40e29bc --- /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 0000000000000000000000000000000000000000..3a3c5bb9fd3e25da01cdc465c0af290ed7db8eef --- /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 0000000000000000000000000000000000000000..82976bdd2fe7f4968cdd129b8d44de21e4e6034a --- /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 0000000000000000000000000000000000000000..ef74d8bd631d7715099c108ba171627d0ec130d5 --- /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() + } + } + } +}