From 1b1f13acef5f87463b12b210ef219fee00474fc6 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Mon, 9 Aug 2021 10:42:44 -0700 Subject: [PATCH 01/17] Add incomplete Event Stream support with working Amazon Transcribe example --- aws/rust-runtime/aws-http/src/lib.rs | 28 ++- aws/rust-runtime/aws-sig-auth/Cargo.toml | 5 + .../aws-sig-auth/src/event_stream.rs | 129 +++++++++++ aws/rust-runtime/aws-sig-auth/src/lib.rs | 3 + .../aws-sig-auth/src/middleware.rs | 4 +- .../smithy/rustsdk/SigV4SigningDecorator.kt | 62 +++-- .../rustsdk/SigV4SigningCustomizationTest.kt | 14 +- .../examples/transcribestreaming/Cargo.toml | 18 ++ .../audio/hello-transcribe-8000.wav | Bin 0 -> 86192 bytes .../examples/transcribestreaming/src/main.rs | 66 ++++++ .../rust/codegen/rustlang/CargoDependency.kt | 12 +- .../smithy/EventStreamSymbolProvider.kt | 65 ++++++ .../rust/codegen/smithy/RuntimeTypes.kt | 12 + .../rust/codegen/smithy/RustCodegenPlugin.kt | 1 + .../smithy/generators/BuilderGenerator.kt | 89 +++++--- .../generators/HttpProtocolGenerator.kt | 3 +- .../smithy/generators/UnionGenerator.kt | 11 +- .../config/ServiceConfigGenerator.kt | 13 +- .../http/ResponseBindingGenerator.kt | 44 +++- .../rust/codegen/smithy/protocols/AwsJson.kt | 9 - .../protocols/HttpBoundProtocolGenerator.kt | 56 ++++- .../parse/EventStreamUnmarshallerGenerator.kt | 213 ++++++++++++++++++ .../EventStreamMarshallerGenerator.kt | 161 +++++++++++++ .../serialize/JsonSerializerGenerator.kt | 4 +- .../serialize/QuerySerializerGenerator.kt | 2 + .../StructuredDataSerializerGenerator.kt | 22 +- .../XmlBindingTraitSerializerGenerator.kt | 13 +- .../RemoveEventStreamOperations.kt | 42 ++-- .../amazon/smithy/rust/codegen/util/Smithy.kt | 26 +++ .../codegen/generators/UnionGeneratorTest.kt | 24 ++ .../http/ResponseBindingGeneratorTest.kt | 5 +- .../smithy/EventStreamSymbolProviderTest.kt | 92 ++++++++ .../XmlBindingTraitSerializerGeneratorTest.kt | 2 +- rust-runtime/inlineable/Cargo.toml | 5 +- rust-runtime/inlineable/src/event_stream.rs | 154 +++++++++++++ rust-runtime/inlineable/src/lib.rs | 2 + rust-runtime/smithy-eventstream/src/error.rs | 4 + rust-runtime/smithy-eventstream/src/frame.rs | 95 +++++++- rust-runtime/smithy-http/src/event_stream.rs | 171 ++++++++++---- rust-runtime/smithy-http/src/middleware.rs | 5 +- rust-runtime/smithy-http/src/operation.rs | 5 + rust-runtime/smithy-http/src/result.rs | 5 +- 42 files changed, 1520 insertions(+), 176 deletions(-) create mode 100644 aws/rust-runtime/aws-sig-auth/src/event_stream.rs create mode 100644 aws/sdk/examples/transcribestreaming/Cargo.toml create mode 100644 aws/sdk/examples/transcribestreaming/audio/hello-transcribe-8000.wav create mode 100644 aws/sdk/examples/transcribestreaming/src/main.rs create mode 100644 codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/EventStreamSymbolProvider.kt create mode 100644 codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt create mode 100644 codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/EventStreamMarshallerGenerator.kt create mode 100644 codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/EventStreamSymbolProviderTest.kt create mode 100644 rust-runtime/inlineable/src/event_stream.rs diff --git a/aws/rust-runtime/aws-http/src/lib.rs b/aws/rust-runtime/aws-http/src/lib.rs index 6546b7dd67..3c5ad304f7 100644 --- a/aws/rust-runtime/aws-http/src/lib.rs +++ b/aws/rust-runtime/aws-http/src/lib.rs @@ -63,14 +63,16 @@ where Err(SdkError::ServiceError { err, raw }) => (err, raw), Err(_) => return RetryKind::NotRetryable, }; - if let Some(retry_after_delay) = response - .http() - .headers() - .get("x-amz-retry-after") - .and_then(|header| header.to_str().ok()) - .and_then(|header| header.parse::().ok()) - { - return RetryKind::Explicit(Duration::from_millis(retry_after_delay)); + if let Some(response) = &response { + if let Some(retry_after_delay) = response + .http() + .headers() + .get("x-amz-retry-after") + .and_then(|header| header.to_str().ok()) + .and_then(|header| header.parse::().ok()) + { + return RetryKind::Explicit(Duration::from_millis(retry_after_delay)); + } } if let Some(kind) = err.retryable_error_kind() { return RetryKind::Error(kind); @@ -83,9 +85,11 @@ where return RetryKind::Error(ErrorKind::TransientError); } }; - if TRANSIENT_ERROR_STATUS_CODES.contains(&response.http().status().as_u16()) { - return RetryKind::Error(ErrorKind::TransientError); - }; + if let Some(status) = response.as_ref().map(|resp| resp.http().status().as_u16()) { + if TRANSIENT_ERROR_STATUS_CODES.contains(&status) { + return RetryKind::Error(ErrorKind::TransientError); + }; + } // TODO: is IDPCommunicationError modeled yet? RetryKind::NotRetryable } @@ -133,7 +137,7 @@ mod test { ) -> Result, SdkError> { Err(SdkError::ServiceError { err, - raw: operation::Response::new(raw.map(|b| SdkBody::from(b))), + raw: Some(operation::Response::new(raw.map(|b| SdkBody::from(b)))), }) } diff --git a/aws/rust-runtime/aws-sig-auth/Cargo.toml b/aws/rust-runtime/aws-sig-auth/Cargo.toml index 9b3bf21e3a..208206a065 100644 --- a/aws/rust-runtime/aws-sig-auth/Cargo.toml +++ b/aws/rust-runtime/aws-sig-auth/Cargo.toml @@ -7,12 +7,17 @@ license = "Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +sign-eventstream = ["smithy-eventstream"] +default = ["sign-eventstream"] + [dependencies] http = "0.2.2" aws-sigv4 = { path = "../aws-sigv4" } aws-auth = { path = "../aws-auth" } aws-types = { path = "../aws-types" } smithy-http = { path = "../../../rust-runtime/smithy-http" } +smithy-eventstream = { path = "../../../rust-runtime/smithy-eventstream", optional = true } # Trying this out as an experiment. thiserror can be removed and replaced with hand written error # implementations and it is not a breaking change. thiserror = "1" diff --git a/aws/rust-runtime/aws-sig-auth/src/event_stream.rs b/aws/rust-runtime/aws-sig-auth/src/event_stream.rs new file mode 100644 index 0000000000..3e669235e5 --- /dev/null +++ b/aws/rust-runtime/aws-sig-auth/src/event_stream.rs @@ -0,0 +1,129 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +use crate::middleware::Signature; +use aws_auth::Credentials; +use aws_sigv4::event_stream::sign_message; +use aws_sigv4::SigningParams; +use aws_types::region::SigningRegion; +use aws_types::SigningService; +use smithy_eventstream::frame::{Message, SignMessage, SignMessageError}; +use smithy_http::property_bag::PropertyBag; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::time::SystemTime; + +/// Event Stream SigV4 signing implementation. +#[derive(Debug)] +pub struct SigV4Signer { + properties: Arc>, + last_signature: Option, +} + +impl SigV4Signer { + pub fn new(properties: Arc>) -> Self { + Self { + properties, + last_signature: None, + } + } +} + +impl SignMessage for SigV4Signer { + fn sign(&mut self, message: Message) -> Result { + let properties = PropertyAccessor(self.properties.lock().unwrap()); + if self.last_signature.is_none() { + // The Signature property should exist in the property bag for all Event Stream requests. + self.last_signature = Some(properties.expect::().as_ref().into()) + } + + // Every single one of these values would have been retrieved during the initial request, + // so we can safely assume they all exist in the property bag at this point. + let credentials = properties.expect::(); + let region = properties.expect::(); + let signing_service = properties.expect::(); + let time = properties + .get::() + .copied() + .unwrap_or_else(SystemTime::now); + let params = SigningParams { + access_key: credentials.access_key_id(), + secret_key: credentials.secret_access_key(), + security_token: credentials.session_token(), + region: region.as_ref(), + service_name: signing_service.as_ref(), + date_time: time.into(), + settings: (), + }; + + let (signed_message, signature) = + sign_message(&message, self.last_signature.as_ref().unwrap(), ¶ms).into_parts(); + self.last_signature = Some(signature); + + Ok(signed_message) + } +} + +// TODO(EventStream): Make a new type around `Arc>` called `SharedPropertyBag` +// and abstract the mutex away entirely. +struct PropertyAccessor<'a>(MutexGuard<'a, PropertyBag>); + +impl<'a> PropertyAccessor<'a> { + fn get(&self) -> Option<&T> { + self.0.get::() + } + + fn expect(&self) -> &T { + self.get::() + .expect("property should have been inserted into property bag via middleware") + } +} + +#[cfg(test)] +mod tests { + use crate::event_stream::SigV4Signer; + use crate::middleware::Signature; + use aws_auth::Credentials; + use aws_types::region::Region; + use aws_types::region::SigningRegion; + use aws_types::SigningService; + use smithy_eventstream::frame::{HeaderValue, Message, SignMessage}; + use smithy_http::property_bag::PropertyBag; + use std::sync::{Arc, Mutex}; + use std::time::{Duration, UNIX_EPOCH}; + + #[test] + fn sign_message() { + let region = Region::new("us-east-1"); + let mut properties = PropertyBag::new(); + properties.insert(region.clone()); + properties.insert(UNIX_EPOCH + Duration::new(1611160427, 0)); + properties.insert(SigningService::from_static("transcribe")); + properties.insert(Credentials::from_keys("AKIAfoo", "bar", None)); + properties.insert(SigningRegion::from(region)); + properties.insert(Signature::new("initial-signature".into())); + + let mut signer = SigV4Signer::new(Arc::new(Mutex::new(properties))); + let mut signatures = Vec::new(); + for _ in 0..5 { + let signed = signer + .sign(Message::new(&b"identical message"[..])) + .unwrap(); + if let HeaderValue::ByteArray(signature) = signed + .headers() + .iter() + .find(|h| h.name().as_str() == ":chunk-signature") + .unwrap() + .value() + { + signatures.push(signature.clone()); + } else { + panic!("failed to get the :chunk-signature") + } + } + for i in 1..signatures.len() { + assert_ne!(signatures[i - 1], signatures[i]); + } + } +} diff --git a/aws/rust-runtime/aws-sig-auth/src/lib.rs b/aws/rust-runtime/aws-sig-auth/src/lib.rs index 3bcaafb0ad..b0ceb31dd1 100644 --- a/aws/rust-runtime/aws-sig-auth/src/lib.rs +++ b/aws/rust-runtime/aws-sig-auth/src/lib.rs @@ -7,5 +7,8 @@ //! //! In the future, additional signature algorithms can be enabled as Cargo Features. +#[cfg(feature = "sign-eventstream")] +pub mod event_stream; + pub mod middleware; pub mod signer; diff --git a/aws/rust-runtime/aws-sig-auth/src/middleware.rs b/aws/rust-runtime/aws-sig-auth/src/middleware.rs index bdb46cd3fd..b85d29aba5 100644 --- a/aws/rust-runtime/aws-sig-auth/src/middleware.rs +++ b/aws/rust-runtime/aws-sig-auth/src/middleware.rs @@ -24,8 +24,10 @@ impl Signature { pub fn new(signature: String) -> Self { Self(signature) } +} - pub fn as_str(&self) -> &str { +impl AsRef for Signature { + fn as_ref(&self) -> &str { &self.0 } } diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4SigningDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4SigningDecorator.kt index 919fce9d5c..2b254628b8 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4SigningDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4SigningDecorator.kt @@ -13,12 +13,14 @@ import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.model.traits.OptionalAuthTrait +import software.amazon.smithy.rust.codegen.rustlang.CargoDependency import software.amazon.smithy.rust.codegen.rustlang.Writable import software.amazon.smithy.rust.codegen.rustlang.asType import software.amazon.smithy.rust.codegen.rustlang.rust import software.amazon.smithy.rust.codegen.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.rustlang.writable import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig +import software.amazon.smithy.rust.codegen.smithy.RuntimeType import software.amazon.smithy.rust.codegen.smithy.customize.OperationCustomization import software.amazon.smithy.rust.codegen.smithy.customize.OperationSection import software.amazon.smithy.rust.codegen.smithy.customize.RustCodegenDecorator @@ -29,10 +31,12 @@ import software.amazon.smithy.rust.codegen.smithy.letIf import software.amazon.smithy.rust.codegen.util.dq import software.amazon.smithy.rust.codegen.util.expectTrait import software.amazon.smithy.rust.codegen.util.hasTrait +import software.amazon.smithy.rust.codegen.util.isInputEventStream /** * The SigV4SigningDecorator: * - adds a `signing_service()` method to `config` to return the default signing service + * - adds a `new_event_stream_signer()` method to `config` to create an Event Stream SigV4 signer * - sets the `SigningService` during operation construction * - sets a default `OperationSigningConfig` A future enhancement will customize this for specific services that need * different behavior. @@ -48,7 +52,7 @@ class SigV4SigningDecorator : RustCodegenDecorator { baseCustomizations: List ): List { return baseCustomizations.letIf(applies(protocolConfig)) { - it + SigV4SigningConfig(protocolConfig.serviceShape.expectTrait()) + it + SigV4SigningConfig(protocolConfig.runtimeConfig, protocolConfig.serviceShape.expectTrait()) } } @@ -58,25 +62,49 @@ class SigV4SigningDecorator : RustCodegenDecorator { baseCustomizations: List ): List { return baseCustomizations.letIf(applies(protocolConfig)) { - it + SigV4SigningFeature(operation, protocolConfig.runtimeConfig, protocolConfig.serviceShape, protocolConfig.model) + it + SigV4SigningFeature( + protocolConfig.model, + operation, + protocolConfig.runtimeConfig, + protocolConfig.serviceShape, + ) } } } -class SigV4SigningConfig(private val sigV4Trait: SigV4Trait) : ConfigCustomization() { +class SigV4SigningConfig(runtimeConfig: RuntimeConfig, private val sigV4Trait: SigV4Trait) : ConfigCustomization() { + private val codegenScope = arrayOf( + "SigV4Signer" to RuntimeType( + "SigV4Signer", + runtimeConfig.awsRuntimeDependency("aws-sig-auth", listOf("sign-eventstream")), + "aws_sig_auth::event_stream" + ), + "PropertyBag" to RuntimeType( + "PropertyBag", + CargoDependency.SmithyHttp(runtimeConfig), + "smithy_http::property_bag" + ) + ) + override fun section(section: ServiceConfig): Writable { return when (section) { is ServiceConfig.ConfigImpl -> writable { - rust( + rustTemplate( """ /// The signature version 4 service signing name to use in the credential scope when signing requests. /// - /// The signing service may be overidden by the `Endpoint`, or by specifying a custom [`SigningService`](aws_types::SigningService) during - /// operation construction + /// The signing service may be overridden by the `Endpoint`, or by specifying a custom + /// [`SigningService`](aws_types::SigningService) during operation construction pub fn signing_service(&self) -> &'static str { ${sigV4Trait.name.dq()} } - """ + + /// Creates a new Event Stream `SignMessage` implementor. + pub fn new_event_stream_signer(&self, properties: std::sync::Arc>) -> #{SigV4Signer} { + #{SigV4Signer}::new(properties) + } + """, + *codegenScope ) } else -> emptySection @@ -95,10 +123,10 @@ fun disableDoubleEncode(service: ServiceShape) = when { } class SigV4SigningFeature( + private val model: Model, private val operation: OperationShape, runtimeConfig: RuntimeConfig, private val service: ServiceShape, - model: Model ) : OperationCustomization() { private val codegenScope = @@ -111,9 +139,9 @@ class SigV4SigningFeature( is OperationSection.MutateRequest -> writable { rustTemplate( """ - ##[allow(unused_mut)] - let mut signing_config = #{sig_auth}::signer::OperationSigningConfig::default_config(); - """, + ##[allow(unused_mut)] + let mut signing_config = #{sig_auth}::signer::OperationSigningConfig::default_config(); + """, *codegenScope ) if (needsAmzSha256(service)) { @@ -128,6 +156,12 @@ class SigV4SigningFeature( "${section.request}.properties_mut().insert(#{sig_auth}::signer::SignableBody::UnsignedPayload);", *codegenScope ) + } else if (operation.isInputEventStream(model)) { + // TODO(EventStream): Is this actually correct for all Event Stream operations? + rustTemplate( + "${section.request}.properties_mut().insert(#{sig_auth}::signer::SignableBody::Bytes(&[]));", + *codegenScope + ) } // some operations are either unsigned or optionally signed: val authSchemes = serviceIndex.getEffectiveAuthSchemes(service, operation) @@ -140,9 +174,9 @@ class SigV4SigningFeature( } rustTemplate( """ - ${section.request}.properties_mut().insert(signing_config); - ${section.request}.properties_mut().insert(#{aws_types}::SigningService::from_static(${section.config}.signing_service())); - """, + ${section.request}.properties_mut().insert(signing_config); + ${section.request}.properties_mut().insert(#{aws_types}::SigningService::from_static(${section.config}.signing_service())); + """, *codegenScope ) } diff --git a/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/SigV4SigningCustomizationTest.kt b/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/SigV4SigningCustomizationTest.kt index 74be357eed..86f05d6723 100644 --- a/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/SigV4SigningCustomizationTest.kt +++ b/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/SigV4SigningCustomizationTest.kt @@ -7,6 +7,7 @@ package software.amazon.smithy.rustsdk import org.junit.jupiter.api.Test import software.amazon.smithy.aws.traits.auth.SigV4Trait +import software.amazon.smithy.rust.codegen.testutil.TestRuntimeConfig import software.amazon.smithy.rust.codegen.testutil.compileAndTest import software.amazon.smithy.rust.codegen.testutil.stubConfigProject import software.amazon.smithy.rust.codegen.testutil.unitTest @@ -14,13 +15,18 @@ import software.amazon.smithy.rust.codegen.testutil.unitTest internal class SigV4SigningCustomizationTest { @Test fun `generates a valid config`() { - val project = stubConfigProject(SigV4SigningConfig(SigV4Trait.builder().name("test-service").build())) + val project = stubConfigProject( + SigV4SigningConfig( + TestRuntimeConfig, + SigV4Trait.builder().name("test-service").build() + ) + ) project.lib { it.unitTest( """ - let conf = crate::config::Config::builder().build(); - assert_eq!(conf.signing_service(), "test-service"); - """ + let conf = crate::config::Config::builder().build(); + assert_eq!(conf.signing_service(), "test-service"); + """ ) } project.compileAndTest() diff --git a/aws/sdk/examples/transcribestreaming/Cargo.toml b/aws/sdk/examples/transcribestreaming/Cargo.toml new file mode 100644 index 0000000000..42dd8ce2b3 --- /dev/null +++ b/aws/sdk/examples/transcribestreaming/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "transcribestreaming" +version = "0.1.0" +authors = ["John DiSanti "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +aws-auth-providers = { path = "../../build/aws-sdk/aws-auth-providers" } +aws-sdk-transcribestreaming = { package = "aws-sdk-transcribestreaming", path = "../../build/aws-sdk/transcribestreaming" } +aws-types = { path = "../../build/aws-sdk/aws-types" } + +async-stream = "0.3" +bytes = "1" +hound = "3.4" +tokio = { version = "1", features = ["full"] } +tracing-subscriber = "0.2.18" diff --git a/aws/sdk/examples/transcribestreaming/audio/hello-transcribe-8000.wav b/aws/sdk/examples/transcribestreaming/audio/hello-transcribe-8000.wav new file mode 100644 index 0000000000000000000000000000000000000000..3df88f806a44f009b13c987a3941c229ed73b054 GIT binary patch literal 86192 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 literal 0 HcmV?d00001 diff --git a/aws/sdk/examples/transcribestreaming/src/main.rs b/aws/sdk/examples/transcribestreaming/src/main.rs new file mode 100644 index 0000000000..29ca0b7e2a --- /dev/null +++ b/aws/sdk/examples/transcribestreaming/src/main.rs @@ -0,0 +1,66 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +use async_stream::stream; +use aws_sdk_transcribestreaming::model::{AudioEvent, AudioStream, LanguageCode, MediaEncoding}; +use aws_sdk_transcribestreaming::{Blob, Client, Config, Region}; +use bytes::BufMut; +use std::time::Duration; + +const CHUNK_SIZE: usize = 8192; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + + let input_stream = stream! { + let pcm = pcm_data(); + for chunk in pcm.chunks(CHUNK_SIZE) { + // Sleeping isn't necessary, but emphasizes the streaming aspect of this + tokio::time::sleep(Duration::from_millis(100)).await; + yield Ok(AudioStream::AudioEvent(AudioEvent::builder().audio_chunk(Blob::new(chunk)).build())); + } + // Must send an empty chunk at the end + yield Ok(AudioStream::AudioEvent(AudioEvent::builder().audio_chunk(Blob::new(Vec::new())).build())); + }; + + let config = Config::builder() + .region(Region::from_static("us-west-2")) + .build(); + let client = Client::from_conf(config); + + let mut output = client + .start_stream_transcription() + .language_code(LanguageCode::EnGb) + .media_sample_rate_hertz(8000) + .media_encoding(MediaEncoding::Pcm) + .audio_stream(input_stream.into()) + .send() + .await + .unwrap(); + + loop { + match output.transcript_result_stream.recv().await { + Ok(Some(transcription)) => { + println!("Received transcription response:\n{:?}\n", transcription) + } + Ok(None) => break, + Err(err) => println!("Received an error: {:?}", err), + } + } + println!("Done.") +} + +fn pcm_data() -> Vec { + let audio = include_bytes!("../audio/hello-transcribe-8000.wav"); + let reader = hound::WavReader::new(&audio[..]).unwrap(); + let samples_result: hound::Result> = reader.into_samples::().collect(); + + let mut pcm: Vec = Vec::new(); + for sample in samples_result.unwrap() { + pcm.put_i16_le(sample); + } + pcm +} 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 81972bb8d3..7b1d56f134 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 @@ -88,6 +88,9 @@ class InlineDependency( private fun forRustFile(name: String, vararg additionalDependencies: RustDependency) = forRustFile(name, "inlineable", *additionalDependencies) + fun eventStream(runtimeConfig: RuntimeConfig) = + forRustFile("event_stream", CargoDependency.SmithyEventStream(runtimeConfig)) + fun jsonErrors(runtimeConfig: RuntimeConfig) = forRustFile("json_errors", CargoDependency.Http, CargoDependency.SmithyTypes(runtimeConfig)) @@ -121,6 +124,10 @@ data class CargoDependency( private val features: List = listOf() ) : RustDependency(name) { + fun withFeature(feature: String): CargoDependency { + return copy(features = features.toMutableList().apply { add(feature) }) + } + override fun version(): String = when (location) { is CratesIo -> location.version is Local -> "local" @@ -173,12 +180,15 @@ data class CargoDependency( val Md5 = CargoDependency("md5", CratesIo("0.7")) val FastRand = CargoDependency("fastrand", CratesIo("1")) val Http: CargoDependency = CargoDependency("http", CratesIo("0.2")) + val Hyper: CargoDependency = CargoDependency("hyper", CratesIo("0.14")) + val HyperWithStream: CargoDependency = Hyper.withFeature("stream") val Tower: CargoDependency = CargoDependency("tower", CratesIo("0.4"), optional = true) fun SmithyTypes(runtimeConfig: RuntimeConfig) = runtimeConfig.runtimeCrate("types") + fun SmithyClient(runtimeConfig: RuntimeConfig) = runtimeConfig.runtimeCrate("client") + fun SmithyEventStream(runtimeConfig: RuntimeConfig) = runtimeConfig.runtimeCrate("eventstream") fun SmithyHttp(runtimeConfig: RuntimeConfig) = runtimeConfig.runtimeCrate("http") fun SmithyHttpTower(runtimeConfig: RuntimeConfig) = runtimeConfig.runtimeCrate("http-tower") - fun SmithyClient(runtimeConfig: RuntimeConfig) = runtimeConfig.runtimeCrate("client") fun ProtocolTestHelpers(runtimeConfig: RuntimeConfig) = CargoDependency( "protocol-test-helpers", runtimeConfig.runtimeCrateLocation.crateLocation(), scope = DependencyScope.Dev diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/EventStreamSymbolProvider.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/EventStreamSymbolProvider.kt new file mode 100644 index 0000000000..86e9a8bc91 --- /dev/null +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/EventStreamSymbolProvider.kt @@ -0,0 +1,65 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package software.amazon.smithy.rust.codegen.smithy + +import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.MemberShape +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.rust.codegen.rustlang.CargoDependency +import software.amazon.smithy.rust.codegen.rustlang.RustType +import software.amazon.smithy.rust.codegen.rustlang.render +import software.amazon.smithy.rust.codegen.rustlang.stripOuter +import software.amazon.smithy.rust.codegen.smithy.generators.error.errorSymbol +import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticInputTrait +import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticOutputTrait +import software.amazon.smithy.rust.codegen.util.getTrait +import software.amazon.smithy.rust.codegen.util.isEventStream +import software.amazon.smithy.rust.codegen.util.isInputEventStream + +/** + * Wrapping symbol provider to wrap modeled types with the smithy-http Event Stream send/receive types. + */ +class EventStreamSymbolProvider( + private val runtimeConfig: RuntimeConfig, + base: RustSymbolProvider, + private val model: Model +) : WrappingSymbolProvider(base) { + override fun toSymbol(shape: Shape): Symbol { + val initial = super.toSymbol(shape) + + // We only want to wrap with Event Stream types when dealing with member shapes + if (shape is MemberShape && shape.isEventStream(model)) { + // Determine if the member has a container that is a synthetic input or output + val operationShape = model.expectShape(shape.container).let { maybeInputOutput -> + val operationId = maybeInputOutput.getTrait()?.operation + ?: maybeInputOutput.getTrait()?.operation + operationId?.let { model.expectShape(it, OperationShape::class.java) } + } + // If we find an operation shape, then we can wrap the type + if (operationShape != null) { + val error = operationShape.errorSymbol(this).toSymbol() + val errorFmt = error.rustType().render(fullyQualified = true) + val innerFmt = initial.rustType().stripOuter().render(fullyQualified = true) + val outer = when (shape.isInputEventStream(model)) { + true -> "EventStreamInput<$innerFmt>" + else -> "Receiver<$innerFmt, $errorFmt>" + } + val rustType = RustType.Opaque(outer, "smithy_http::event_stream") + return initial.toBuilder() + .name(rustType.name) + .rustType(rustType) + .addReference(error) + .addReference(initial) + .addDependency(CargoDependency.SmithyHttp(runtimeConfig)) + .build() + } + } + + return initial + } +} 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 7e89ceceb0..cf6ba944ab 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 @@ -161,6 +161,18 @@ data class RuntimeType(val name: String?, val dependency: RustDependency?, val n val HttpRequestBuilder = Http("request::Builder") val HttpResponseBuilder = Http("response::Builder") + val Hyper = CargoDependency.Hyper.asType() + + fun eventStreamReceiver(runtimeConfig: RuntimeConfig): RuntimeType = + RuntimeType( + "Receiver", + dependency = CargoDependency.SmithyHttp(runtimeConfig), + "smithy_http::event_stream" + ) + + fun eventStreamInlinables(runtimeConfig: RuntimeConfig): RuntimeType = + forInlineDependency(InlineDependency.eventStream(runtimeConfig)) + fun jsonErrors(runtimeConfig: RuntimeConfig) = forInlineDependency(InlineDependency.jsonErrors(runtimeConfig)) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RustCodegenPlugin.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RustCodegenPlugin.kt index 96bcd029c9..eb93405f73 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RustCodegenPlugin.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RustCodegenPlugin.kt @@ -28,6 +28,7 @@ class RustCodegenPlugin : SmithyBuildPlugin { companion object { fun baseSymbolProvider(model: Model, serviceShape: ServiceShape, symbolVisitorConfig: SymbolVisitorConfig = DefaultConfig) = SymbolVisitor(model, serviceShape = serviceShape, config = symbolVisitorConfig) + .let { EventStreamSymbolProvider(symbolVisitorConfig.runtimeConfig, it, model) } .let { StreamingShapeSymbolProvider(it, model) } .let { BaseSymbolMetadataProvider(it) } .let { StreamingShapeMetadataProvider(it, model) } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/BuilderGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/BuilderGenerator.kt index ebfe51adb9..1ae0bbf3ad 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/BuilderGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/BuilderGenerator.kt @@ -5,6 +5,7 @@ package software.amazon.smithy.rust.codegen.smithy.generators +import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.StructureShape @@ -50,14 +51,15 @@ class OperationBuildError(private val runtimeConfig: RuntimeConfig) { /** setter names will never hit a reserved word and therefore never need escaping */ fun MemberShape.setterName(): String = "set_${this.memberName.toSnakeCase()}" -class BuilderGenerator( - val model: Model, - private val symbolProvider: RustSymbolProvider, - private val shape: StructureShape +open class BuilderGenerator( + protected val model: Model, + protected val symbolProvider: RustSymbolProvider, + protected val shape: StructureShape ) { + protected val runtimeConfig = symbolProvider.config().runtimeConfig private val members: List = shape.allMembers.values.toList() - private val runtimeConfig = symbolProvider.config().runtimeConfig private val structureSymbol = symbolProvider.toSymbol(shape) + fun render(writer: RustWriter) { val symbol = symbolProvider.toSymbol(shape) // TODO: figure out exactly what docs we want on a the builder module @@ -104,6 +106,53 @@ class BuilderGenerator( } } + // TODO(EventStream): [DX] Update builders to take EventInputStream as Into + open fun renderBuilderMember(writer: RustWriter, member: MemberShape, memberName: String, memberSymbol: Symbol) { + // builder members are crate-public to enable using them + // directly in serializers/deserializers + writer.write("pub(crate) $memberName: #T,", memberSymbol) + } + + open fun renderBuilderMemberFn( + writer: RustWriter, + coreType: RustType, + member: MemberShape, + memberName: String, + memberSymbol: Symbol + ) { + fun builderConverter(coreType: RustType) = when (coreType) { + is RustType.String, + is RustType.Box -> "input.into()" + else -> "input" + } + + val signature = when (coreType) { + is RustType.String, + is RustType.Box -> "(mut self, input: impl Into<${coreType.render(true)}>) -> Self" + else -> "(mut self, input: ${coreType.render(true)}) -> Self" + } + writer.documentShape(member, model) + writer.rustBlock("pub fn $memberName$signature") { + write("self.$memberName = Some(${builderConverter(coreType)});") + write("self") + } + } + + open fun renderBuilderMemberSetterFn( + writer: RustWriter, + outerType: RustType, + member: MemberShape, + memberName: String, + memberSymbol: Symbol + ) { + // Render a `set_foo` method. This is useful as a target for code generation, because the argument type + // is the same as the resulting member type, and is always optional. + val inputType = outerType.asOptional() + writer.rustBlock("pub fn ${member.setterName()}(mut self, input: ${inputType.render(true)}) -> Self") { + rust("self.$memberName = input; self") + } + } + private fun renderBuilder(writer: RustWriter) { val builderName = "Builder" @@ -119,18 +168,10 @@ class BuilderGenerator( val memberName = symbolProvider.toMemberName(member) // All fields in the builder are optional val memberSymbol = symbolProvider.toSymbol(member).makeOptional() - // builder members are crate-public to enable using them - // directly in serializers/deserializers - write("pub(crate) $memberName: #T,", memberSymbol) + renderBuilderMember(this, member, memberName, memberSymbol) } } - fun builderConverter(coreType: RustType) = when (coreType) { - is RustType.String, - is RustType.Box -> "input.into()" - else -> "input" - } - writer.rustBlock("impl $builderName") { members.forEach { member -> // All fields in the builder are optional @@ -143,26 +184,10 @@ class BuilderGenerator( when (coreType) { is RustType.Vec -> renderVecHelper(memberName, coreType) is RustType.HashMap -> renderMapHelper(memberName, coreType) - else -> { - val signature = when (coreType) { - is RustType.String, - is RustType.Box -> "(mut self, input: impl Into<${coreType.render(true)}>) -> Self" - else -> "(mut self, input: ${coreType.render(true)}) -> Self" - } - writer.documentShape(member, model) - writer.rustBlock("pub fn $memberName$signature") { - write("self.$memberName = Some(${builderConverter(coreType)});") - write("self") - } - } + else -> renderBuilderMemberFn(this, coreType, member, memberName, memberSymbol) } - // Render a `set_foo` method. This is useful as a target for code generation, because the argument type - // is the same as the resulting member type, and is always optional. - val inputType = outerType.asOptional() - writer.rustBlock("pub fn ${member.setterName()}(mut self, input: ${inputType.render(true)}) -> Self") { - rust("self.$memberName = input; self") - } + renderBuilderMemberSetterFn(this, outerType, member, memberName, memberSymbol) } buildFn(this) } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolGenerator.kt index e7e32e2847..ef7dcc7698 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolGenerator.kt @@ -195,6 +195,7 @@ abstract class HttpProtocolGenerator( ) { withBlock("Ok({", "})") { features.forEach { it.section(OperationSection.MutateInput("self", "_config"))(this) } + rust("let properties = std::sync::Arc::new(std::sync::Mutex::new(smithy_http::property_bag::PropertyBag::new()));") rust("let request = self.request_builder_base()?;") withBlock("let body =", ";") { body("self", shape) @@ -203,7 +204,7 @@ abstract class HttpProtocolGenerator( rust( """ ##[allow(unused_mut)] - let mut request = #T::Request::new(request.map(#T::from)); + let mut request = #T::Request::from_parts(request.map(#T::from), properties); """, operationModule, sdkBody ) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/UnionGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/UnionGenerator.kt index 1337d56eea..269f6c6abc 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/UnionGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/UnionGenerator.kt @@ -9,12 +9,16 @@ import software.amazon.smithy.codegen.core.SymbolProvider import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.UnionShape +import software.amazon.smithy.model.traits.ErrorTrait import software.amazon.smithy.rust.codegen.rustlang.Attribute import software.amazon.smithy.rust.codegen.rustlang.RustWriter import software.amazon.smithy.rust.codegen.rustlang.documentShape import software.amazon.smithy.rust.codegen.rustlang.rust import software.amazon.smithy.rust.codegen.rustlang.rustBlock import software.amazon.smithy.rust.codegen.smithy.expectRustMetadata +import software.amazon.smithy.rust.codegen.smithy.letIf +import software.amazon.smithy.rust.codegen.util.hasTrait +import software.amazon.smithy.rust.codegen.util.isEventStream import software.amazon.smithy.rust.codegen.util.toPascalCase import software.amazon.smithy.rust.codegen.util.toSnakeCase @@ -24,12 +28,17 @@ class UnionGenerator( private val writer: RustWriter, private val shape: UnionShape ) { + private val sortedMembers: List = shape.allMembers.values + .sortedBy { symbolProvider.toMemberName(it) } + .letIf(shape.isEventStream()) { members -> + // Filter out all error union members for Event Stream unions since these get handled as actual SdkErrors + members.filter { member -> !model.expectShape(member.target).hasTrait() } + } fun render() { renderUnion() } - private val sortedMembers: List = shape.allMembers.values.sortedBy { symbolProvider.toMemberName(it) } private fun renderUnion() { val unionSymbol = symbolProvider.toSymbol(shape) val containerMeta = unionSymbol.expectRustMetadata() diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/config/ServiceConfigGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/config/ServiceConfigGenerator.kt index ea79fbdbcb..c59b034d87 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/config/ServiceConfigGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/config/ServiceConfigGenerator.kt @@ -116,12 +116,11 @@ class ServiceConfigGenerator(private val customizations: List) -> std::fmt::Result { - let mut config = f.debug_struct("Config"); - config.finish() - } - - """ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut config = f.debug_struct("Config"); + config.finish() + } + """ ) } @@ -129,7 +128,7 @@ class ServiceConfigGenerator(private val customizations: List Builder { Builder::default() } - """ + """ ) customizations.forEach { it.section(ServiceConfig.ConfigImpl)(this) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/ResponseBindingGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/ResponseBindingGenerator.kt index 2d70ea3264..91606f8010 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/ResponseBindingGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/ResponseBindingGenerator.kt @@ -35,6 +35,8 @@ import software.amazon.smithy.rust.codegen.smithy.generators.ProtocolConfig import software.amazon.smithy.rust.codegen.smithy.makeOptional import software.amazon.smithy.rust.codegen.smithy.protocols.HttpBindingDescriptor import software.amazon.smithy.rust.codegen.smithy.protocols.HttpLocation +import software.amazon.smithy.rust.codegen.smithy.protocols.Protocol +import software.amazon.smithy.rust.codegen.smithy.protocols.parse.EventStreamUnmarshallerGenerator import software.amazon.smithy.rust.codegen.smithy.rustType import software.amazon.smithy.rust.codegen.util.dq import software.amazon.smithy.rust.codegen.util.hasTrait @@ -42,7 +44,11 @@ import software.amazon.smithy.rust.codegen.util.isPrimitive import software.amazon.smithy.rust.codegen.util.isStreaming import software.amazon.smithy.rust.codegen.util.toSnakeCase -class ResponseBindingGenerator(protocolConfig: ProtocolConfig, private val operationShape: OperationShape) { +class ResponseBindingGenerator( + private val protocol: Protocol, + protocolConfig: ProtocolConfig, + private val operationShape: OperationShape +) { private val runtimeConfig = protocolConfig.runtimeConfig private val symbolProvider = protocolConfig.symbolProvider private val model = protocolConfig.model @@ -124,6 +130,7 @@ class ResponseBindingGenerator(protocolConfig: ProtocolConfig, private val opera * Generate a function to deserialize `[binding]` from the response payload */ fun generateDeserializePayloadFn( + operationShape: OperationShape, binding: HttpBindingDescriptor, errorT: RuntimeType, // Deserialize a single structure or union member marked as a payload @@ -142,7 +149,13 @@ class ResponseBindingGenerator(protocolConfig: ProtocolConfig, private val opera outputT, errorT ) { - deserializeStreamingBody(binding) + // Streaming unions are Event Streams and should be handled separately + val target = model.expectShape(binding.member.target) + if (target.isUnionShape) { + bindEventStreamOutput(operationShape, target as UnionShape) + } else { + deserializeStreamingBody(binding) + } } } else { rustWriter.rustBlock("pub fn $fnName(body: &[u8]) -> std::result::Result<#T, #T>", outputT, errorT) { @@ -157,6 +170,27 @@ class ResponseBindingGenerator(protocolConfig: ProtocolConfig, private val opera } } + private fun RustWriter.bindEventStreamOutput(operationShape: OperationShape, target: UnionShape) { + val unmarshallerConstructorFn = EventStreamUnmarshallerGenerator( + protocol, + model, + runtimeConfig, + symbolProvider, + operationShape, + target + ).render() + rustTemplate( + """ + let unmarshaller = #{unmarshallerConstructorFn}(); + let body = std::mem::replace(body, #{SdkBody}::taken()); + Ok(#{Receiver}::new(unmarshaller, body)) + """, + "SdkBody" to RuntimeType.sdkBody(runtimeConfig), + "unmarshallerConstructorFn" to unmarshallerConstructorFn, + "Receiver" to RuntimeType.eventStreamReceiver(runtimeConfig), + ) + } + private fun RustWriter.deserializeStreamingBody(binding: HttpBindingDescriptor) { val member = binding.member val targetShape = model.expectShape(member.target) @@ -164,10 +198,10 @@ class ResponseBindingGenerator(protocolConfig: ProtocolConfig, private val opera rustTemplate( """ // replace the body with an empty body - let body = std::mem::replace(body, #{sdk_body}::taken()); - Ok(#{byte_stream}::new(body)) + let body = std::mem::replace(body, #{SdkBody}::taken()); + Ok(#{ByteStream}::new(body)) """, - "byte_stream" to RuntimeType.byteStream(runtimeConfig), "sdk_body" to RuntimeType.sdkBody(runtimeConfig) + "ByteStream" to RuntimeType.byteStream(runtimeConfig), "SdkBody" to RuntimeType.sdkBody(runtimeConfig) ) } 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 19a4f668ef..8aa4a2a59e 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 @@ -8,7 +8,6 @@ package software.amazon.smithy.rust.codegen.smithy.protocols import software.amazon.smithy.model.Model import software.amazon.smithy.model.pattern.UriPattern import software.amazon.smithy.model.shapes.OperationShape -import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.shapes.ToShapeId import software.amazon.smithy.model.traits.HttpTrait import software.amazon.smithy.model.traits.TimestampFormatTrait @@ -26,7 +25,6 @@ import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.JsonSerial import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.StructuredDataSerializerGenerator import software.amazon.smithy.rust.codegen.smithy.transformers.OperationNormalizer import software.amazon.smithy.rust.codegen.smithy.transformers.RemoveEventStreamOperations -import software.amazon.smithy.rust.codegen.smithy.transformers.StructureModifier import software.amazon.smithy.rust.codegen.util.inputShape import software.amazon.smithy.rust.codegen.util.orNull @@ -47,13 +45,6 @@ class AwsJsonFactory(private val version: AwsJsonVersion) : ProtocolGeneratorFac return HttpBoundProtocolGenerator(protocolConfig, AwsJson(protocolConfig, version)) } - private val shapeIfHasMembers: StructureModifier = { _, shape: StructureShape? -> - when (shape?.members().isNullOrEmpty()) { - true -> null - else -> shape - } - } - override fun transformModel(model: Model): Model { // For AwsJson10, the body matches 1:1 with the input return OperationNormalizer(model).transformModel().let(RemoveEventStreamOperations::transform) 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 b3b5ac3d8e..ffb9841a64 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 @@ -40,6 +40,7 @@ import software.amazon.smithy.rust.codegen.smithy.generators.operationBuildError import software.amazon.smithy.rust.codegen.smithy.generators.setterName import software.amazon.smithy.rust.codegen.smithy.isOptional import software.amazon.smithy.rust.codegen.smithy.protocols.parse.StructuredDataParserGenerator +import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.EventStreamMarshallerGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.StructuredDataSerializerGenerator import software.amazon.smithy.rust.codegen.smithy.transformers.errorMessageMember import software.amazon.smithy.rust.codegen.util.dq @@ -47,6 +48,8 @@ import software.amazon.smithy.rust.codegen.util.expectMember import software.amazon.smithy.rust.codegen.util.hasStreamingMember import software.amazon.smithy.rust.codegen.util.hasTrait import software.amazon.smithy.rust.codegen.util.inputShape +import software.amazon.smithy.rust.codegen.util.isEventStream +import software.amazon.smithy.rust.codegen.util.isInputEventStream import software.amazon.smithy.rust.codegen.util.isStreaming import software.amazon.smithy.rust.codegen.util.outputShape import software.amazon.smithy.rust.codegen.util.toSnakeCase @@ -81,10 +84,12 @@ class HttpBoundProtocolGenerator( "ParseStrict" to RuntimeType.parseStrict(runtimeConfig), "ParseResponse" to RuntimeType.parseResponse(runtimeConfig), "http" to RuntimeType.http, + "hyper" to CargoDependency.HyperWithStream.asType(), "operation" to RuntimeType.operationModule(runtimeConfig), "Bytes" to RuntimeType.Bytes, "SdkBody" to RuntimeType.sdkBody(runtimeConfig), - "BuildError" to runtimeConfig.operationBuildError() + "BuildError" to runtimeConfig.operationBuildError(), + "SmithyHttp" to CargoDependency.SmithyHttp(runtimeConfig).asType() ) override fun RustWriter.body(self: String, operationShape: OperationShape): BodyMetadata { @@ -103,10 +108,50 @@ class HttpBoundProtocolGenerator( BodyMetadata(takesOwnership = false) } else { val member = inputShape.expectMember(payloadMemberName) - serializeViaPayload(member, serializerGenerator) + if (operationShape.isInputEventStream(model)) { + serializeViaEventStream(operationShape, member, serializerGenerator) + } else { + serializeViaPayload(member, serializerGenerator) + } } } + private fun RustWriter.serializeViaEventStream( + operationShape: OperationShape, + memberShape: MemberShape, + serializerGenerator: StructuredDataSerializerGenerator + ): BodyMetadata { + val memberName = symbolProvider.toMemberName(memberShape) + val unionShape = model.expectShape(memberShape.target, UnionShape::class.java) + + val marshallerConstructorFn = EventStreamMarshallerGenerator( + model, + runtimeConfig, + symbolProvider, + unionShape, + serializerGenerator + ).render() + + // TODO(EventStream): [RPC] RPC protocols need to send an initial message with the + // parameters that are not `@eventHeader` or `@eventPayload`. + rustTemplate( + """ + { + let marshaller = #{marshallerConstructorFn}(); + let signer = _config.new_event_stream_signer(properties.clone()); + let adapter: #{SmithyHttp}::event_stream::MessageStreamAdapter<_, #{OperationError}> = + self.$memberName.into_body_stream(marshaller, signer); + let body: #{SdkBody} = #{hyper}::Body::wrap_stream(adapter).into(); + body + } + """, + *codegenScope, + "marshallerConstructorFn" to marshallerConstructorFn, + "OperationError" to operationShape.errorSymbol(symbolProvider) + ) + return BodyMetadata(takesOwnership = true) + } + private fun RustWriter.serializeViaPayload( member: MemberShape, serializerGenerator: StructuredDataSerializerGenerator @@ -175,6 +220,10 @@ class HttpBoundProtocolGenerator( BodyMetadata(takesOwnership = true) } is StructureShape, is UnionShape -> { + check( + !((targetShape as? UnionShape)?.isEventStream() ?: false) + ) { "Event Streams should be handled further up" } + // JSON serialize the structure or union targeted rust( """#T(&$payloadName).map_err(|err|#T::SerializationError(err.into()))?""", @@ -430,7 +479,7 @@ class HttpBoundProtocolGenerator( bindings: List, errorSymbol: RuntimeType, ) { - val httpBindingGenerator = ResponseBindingGenerator(protocolConfig, operationShape) + val httpBindingGenerator = ResponseBindingGenerator(protocol, protocolConfig, operationShape) val structuredDataParser = protocol.structuredDataParser(operationShape) Attribute.AllowUnusedMut.render(this) rust("let mut output = #T::default();", outputShape.builderSymbol(symbolProvider)) @@ -509,6 +558,7 @@ class HttpBoundProtocolGenerator( rust("#T($body).map_err(#T::unhandled)", structuredDataParser.payloadParser(member), errorSymbol) } val deserializer = httpBindingGenerator.generateDeserializePayloadFn( + operationShape, binding, errorSymbol, docHandler = docShapeHandler, diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt new file mode 100644 index 0000000000..75c380405d --- /dev/null +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt @@ -0,0 +1,213 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package software.amazon.smithy.rust.codegen.smithy.protocols.parse + +import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.BlobShape +import software.amazon.smithy.model.shapes.MemberShape +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.StringShape +import software.amazon.smithy.model.shapes.StructureShape +import software.amazon.smithy.model.shapes.UnionShape +import software.amazon.smithy.model.traits.ErrorTrait +import software.amazon.smithy.rust.codegen.rustlang.CargoDependency +import software.amazon.smithy.rust.codegen.rustlang.RustWriter +import software.amazon.smithy.rust.codegen.rustlang.rust +import software.amazon.smithy.rust.codegen.rustlang.rustBlock +import software.amazon.smithy.rust.codegen.rustlang.rustBlockTemplate +import software.amazon.smithy.rust.codegen.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig +import software.amazon.smithy.rust.codegen.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider +import software.amazon.smithy.rust.codegen.smithy.generators.error.errorSymbol +import software.amazon.smithy.rust.codegen.smithy.protocols.Protocol +import software.amazon.smithy.rust.codegen.util.dq +import software.amazon.smithy.rust.codegen.util.hasTrait +import software.amazon.smithy.rust.codegen.util.toPascalCase + +// TODO(EventStream): [TEST] Unit test EventStreamUnmarshallerGenerator +class EventStreamUnmarshallerGenerator( + private val protocol: Protocol, + private val model: Model, + private val runtimeConfig: RuntimeConfig, + private val symbolProvider: RustSymbolProvider, + private val operationShape: OperationShape, + private val unionShape: UnionShape, +) { + private val unionSymbol = symbolProvider.toSymbol(unionShape) + private val operationErrorSymbol = operationShape.errorSymbol(symbolProvider) + private val smithyEventStream = CargoDependency.SmithyEventStream(runtimeConfig) + private val codegenScope = arrayOf( + "UnmarshallMessage" to RuntimeType("UnmarshallMessage", smithyEventStream, "smithy_eventstream::frame"), + "UnmarshalledMessage" to RuntimeType("UnmarshalledMessage", smithyEventStream, "smithy_eventstream::frame"), + "Message" to RuntimeType("Message", smithyEventStream, "smithy_eventstream::frame"), + "Header" to RuntimeType("Header", smithyEventStream, "smithy_eventstream::frame"), + "HeaderValue" to RuntimeType("HeaderValue", smithyEventStream, "smithy_eventstream::frame"), + "Error" to RuntimeType("Error", smithyEventStream, "smithy_eventstream::error"), + "Inlineables" to RuntimeType.eventStreamInlinables(runtimeConfig), + "SmithyError" to RuntimeType("Error", CargoDependency.SmithyTypes(runtimeConfig), "smithy_types") + ) + + fun render(): RuntimeType { + val unmarshallerType = unionShape.eventStreamUnmarshallerType() + return RuntimeType.forInlineFun("${unmarshallerType.name}::new", "event_stream_serde") { inlineWriter -> + inlineWriter.renderUnmarshaller(unmarshallerType, unionSymbol) + } + } + + private fun RustWriter.renderUnmarshaller(unmarshallerType: RuntimeType, unionSymbol: Symbol) { + rust( + """ + ##[non_exhaustive] + ##[derive(Debug)] + pub struct ${unmarshallerType.name}; + + impl ${unmarshallerType.name} { + pub fn new() -> Self { + ${unmarshallerType.name} + } + } + """ + ) + + rustBlockTemplate( + "impl #{UnmarshallMessage} for ${unmarshallerType.name}", + *codegenScope + ) { + rust("type Output = #T;", unionSymbol) + rust("type Error = #T;", operationErrorSymbol) + + rustBlockTemplate( + """ + fn unmarshall( + &self, + message: #{Message} + ) -> std::result::Result<#{UnmarshalledMessage}, #{Error}> + """, + *codegenScope + ) { + rustBlockTemplate( + """ + let response_headers = #{Inlineables}::parse_response_headers(&message)?; + match response_headers.message_type.as_str() + """, + *codegenScope + ) { + rustBlock("\"event\" => ") { + renderUnmarshallEvent() + } + rustBlock("\"exception\" => ") { + renderUnmarshallError() + } + rustBlock("value => ") { + rustTemplate( + "return Err(#{Error}::Unmarshalling(format!(\"unrecognized :message-type: {}\", value)));", + *codegenScope + ) + } + } + } + } + } + + private fun RustWriter.renderUnmarshallEvent() { + rustBlock("match response_headers.smithy_type.as_str()") { + for (member in unionShape.members()) { + val target = model.expectShape(member.target) + if (!target.hasTrait()) { + rustBlock("${member.memberName.dq()} => ") { + renderUnmarshallUnionMember(member, target) + } + } + } + rustBlock("smithy_type => ") { + // TODO: Handle this better once unions support unknown variants + rustTemplate( + "return Err(#{Error}::Unmarshalling(format!(\"unrecognized :event-type: {}\", smithy_type)));", + *codegenScope + ) + } + } + } + + private fun RustWriter.renderUnmarshallUnionMember(member: MemberShape, target: Shape) { + rustTemplate( + "return Ok(#{UnmarshalledMessage}::Event(#{Output}::${member.memberName.toPascalCase()}(", + "Output" to unionSymbol, + *codegenScope + ) + // TODO(EventStream): [RPC] Don't blow up on an initial-message that's not part of the union (:event-type will be "initial-request" or "initial-response") + // TODO(EventStream): [RPC] Incorporate initial-message into original output (:event-type will be "initial-request" or "initial-response") + when (target) { + is BlobShape -> { + rust("unimplemented!(\"TODO(EventStream): Implement blob unmarshalling\")") + } + is StringShape -> { + rust("unimplemented!(\"TODO(EventStream): Implement string unmarshalling\")") + } + is UnionShape, is StructureShape -> { + // TODO(EventStream): Check :content-type against expected content-type, error if unexpected + val parser = protocol.structuredDataParser(operationShape).payloadParser(member) + rustTemplate( + """ + #{parser}(&message.payload()[..]) + .map_err(|err| { + #{Error}::Unmarshalling(format!("failed to unmarshall ${member.memberName}: {}", err)) + })? + """, + "parser" to parser, + *codegenScope + ) + } + } + rust(")))") + } + + private fun RustWriter.renderUnmarshallError() { + rustBlock("match response_headers.smithy_type.as_str()") { + for (member in unionShape.members()) { + val target = model.expectShape(member.target) + if (target.hasTrait() && target is StructureShape) { + rustBlock("${member.memberName.dq()} => ") { + val parser = protocol.structuredDataParser(operationShape).errorParser(target) + if (parser != null) { + rust("let builder = #T::builder();", symbolProvider.toSymbol(target)) + rustTemplate( + """ + let builder = #{parser}(&message.payload()[..], builder) + .map_err(|err| { + #{Error}::Unmarshalling(format!("failed to unmarshall ${member.memberName}: {}", err)) + })?; + return Ok(#{UnmarshalledMessage}::Error( + #{OpError}::new( + #{OpError}Kind::${member.memberName.toPascalCase()}(builder.build()), + #{SmithyError}::builder().build(), + ) + )) + """, + "OpError" to operationErrorSymbol, + "parser" to parser, + *codegenScope + ) + } + } + } + } + rust("_ => {}") + } + // TODO(EventStream): Generic error parsing; will need to refactor `parseGenericError` to + // operate on bodies rather than responses. This should be easy for all but restJson, + // which pulls the error type out of a header. + rust("unimplemented!(\"event stream generic error parsing\")") + } + + private fun UnionShape.eventStreamUnmarshallerType(): RuntimeType { + val symbol = symbolProvider.toSymbol(this) + return RuntimeType("${symbol.name.toPascalCase()}Unmarshaller", null, "crate::event_stream_serde") + } +} diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/EventStreamMarshallerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/EventStreamMarshallerGenerator.kt new file mode 100644 index 0000000000..38c7669a08 --- /dev/null +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/EventStreamMarshallerGenerator.kt @@ -0,0 +1,161 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package software.amazon.smithy.rust.codegen.smithy.protocols.serialize + +import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.BlobShape +import software.amazon.smithy.model.shapes.MemberShape +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.StringShape +import software.amazon.smithy.model.shapes.StructureShape +import software.amazon.smithy.model.shapes.UnionShape +import software.amazon.smithy.model.traits.EventHeaderTrait +import software.amazon.smithy.model.traits.EventPayloadTrait +import software.amazon.smithy.rust.codegen.rustlang.CargoDependency +import software.amazon.smithy.rust.codegen.rustlang.RustWriter +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.rustBlockTemplate +import software.amazon.smithy.rust.codegen.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig +import software.amazon.smithy.rust.codegen.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider +import software.amazon.smithy.rust.codegen.smithy.rustType +import software.amazon.smithy.rust.codegen.util.dq +import software.amazon.smithy.rust.codegen.util.hasTrait +import software.amazon.smithy.rust.codegen.util.toPascalCase + +// TODO(EventStream): [TEST] Unit test EventStreamMarshallerGenerator +class EventStreamMarshallerGenerator( + private val model: Model, + runtimeConfig: RuntimeConfig, + private val symbolProvider: RustSymbolProvider, + private val unionShape: UnionShape, + private val serializerGenerator: StructuredDataSerializerGenerator, +) { + private val smithyEventStream = CargoDependency.SmithyEventStream(runtimeConfig) + private val codegenScope = arrayOf( + "MarshallMessage" to RuntimeType("MarshallMessage", smithyEventStream, "smithy_eventstream::frame"), + "Message" to RuntimeType("Message", smithyEventStream, "smithy_eventstream::frame"), + "Header" to RuntimeType("Header", smithyEventStream, "smithy_eventstream::frame"), + "HeaderValue" to RuntimeType("HeaderValue", smithyEventStream, "smithy_eventstream::frame"), + "Error" to RuntimeType("Error", smithyEventStream, "smithy_eventstream::error"), + ) + + fun render(): RuntimeType { + val marshallerType = unionShape.eventStreamMarshallerType() + val unionSymbol = symbolProvider.toSymbol(unionShape) + + return RuntimeType.forInlineFun("${marshallerType.name}::new", "event_stream_serde") { inlineWriter -> + inlineWriter.renderMarshaller(marshallerType, unionSymbol) + } + } + + private fun RustWriter.renderMarshaller(marshallerType: RuntimeType, unionSymbol: Symbol) { + rust( + """ + ##[non_exhaustive] + ##[derive(Debug)] + pub struct ${marshallerType.name}; + + impl ${marshallerType.name} { + pub fn new() -> Self { + ${marshallerType.name} + } + } + """ + ) + + rustBlockTemplate( + "impl #{MarshallMessage} for ${marshallerType.name}", + *codegenScope + ) { + rust("type Input = ${unionSymbol.rustType().render(fullyQualified = true)};") + + rustBlockTemplate( + "fn marshall(&self, input: Self::Input) -> std::result::Result<#{Message}, #{Error}>", + *codegenScope + ) { + rust("let mut headers = Vec::new();") + addStringHeader(":message-type", "\"event\".into()") + rustBlock("let payload = match input") { + for (member in unionShape.members()) { + val eventType = member.memberName // must be the original name, not the Rust-safe name + rustBlock("Self::Input::${member.memberName.toPascalCase()}(inner) => ") { + addStringHeader(":event-type", "${eventType.dq()}.into()") + val target = model.expectShape(member.target, StructureShape::class.java) + serializeEvent(target) + } + } + } + rustTemplate("; Ok(#{Message}::new_from_parts(headers, payload))", *codegenScope) + } + } + } + + private fun RustWriter.serializeEvent(struct: StructureShape) { + for (member in struct.members()) { + val memberName = symbolProvider.toMemberName(member) + val target = model.expectShape(member.target) + if (member.hasTrait()) { + serializeUnionMember(memberName, member, target) + } else if (member.hasTrait()) { + TODO("TODO(EventStream): Implement @eventHeader trait") + } else { + throw IllegalStateException("Event Stream members must be a header or payload") + } + } + } + + private fun RustWriter.serializeUnionMember(memberName: String, member: MemberShape, target: Shape) { + if (target is BlobShape || target is StringShape) { + data class PayloadContext(val conversionFn: String, val contentType: String) + val ctx = when (target) { + is BlobShape -> PayloadContext("into_inner", "application/octet-stream") + is StringShape -> PayloadContext("into_bytes", "text/plain") + else -> throw IllegalStateException("unreachable") + } + addStringHeader(":content-type", "${ctx.contentType.dq()}.into()") + if (member.isOptional) { + rust( + """ + if let Some(inner_payload) = inner.$memberName { + inner_payload.${ctx.conversionFn}() + } else { + Vec::new() + } + """ + ) + } else { + rust("inner.$memberName.${ctx.conversionFn}()") + } + } else { + // TODO(EventStream): Select content-type based on protocol + addStringHeader(":content-type", "\"TODO\".into()") + + val serializerFn = serializerGenerator.payloadSerializer(member) + rustTemplate( + """ + #{serializerFn}(&inner.$memberName) + .map_err(|err| #{Error}::Marshalling(format!("{}", err)))? + """, + "serializerFn" to serializerFn, + *codegenScope + ) + } + } + + private fun RustWriter.addStringHeader(name: String, valueExpr: String) { + rustTemplate("headers.push(#{Header}::new(${name.dq()}, #{HeaderValue}::String($valueExpr)));", *codegenScope) + } + + private fun UnionShape.eventStreamMarshallerType(): RuntimeType { + val symbol = symbolProvider.toSymbol(this) + return RuntimeType("${symbol.name.toPascalCase()}Marshaller", null, "crate::event_stream_serde") + } +} diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGenerator.kt index 2d7f05f558..cbf3468703 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGenerator.kt @@ -141,7 +141,7 @@ class JsonSerializerGenerator( val target = model.expectShape(member.target, StructureShape::class.java) return RuntimeType.forInlineFun(fnName, "operation_ser") { writer -> writer.rustBlockTemplate( - "pub fn $fnName(input: &#{target}) -> Result<#{SdkBody}, #{Error}>", + "pub fn $fnName(input: &#{target}) -> std::result::Result, #{Error}>", *codegenScope, "target" to symbolProvider.toSymbol(target) ) { @@ -149,7 +149,7 @@ class JsonSerializerGenerator( rustTemplate("let mut object = #{JsonObjectWriter}::new(&mut out);", *codegenScope) serializeStructure(StructContext("object", "input", target)) rust("object.finish();") - rustTemplate("Ok(#{SdkBody}::from(out))", *codegenScope) + rustTemplate("Ok(out.into_bytes())", *codegenScope) } } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/QuerySerializerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/QuerySerializerGenerator.kt index 5d64ba3ce0..3128a6bcaa 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/QuerySerializerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/QuerySerializerGenerator.kt @@ -101,6 +101,8 @@ abstract class QuerySerializerGenerator(protocolConfig: ProtocolConfig) : Struct } override fun payloadSerializer(member: MemberShape): RuntimeType { + // TODO(EventStream): [RPC] The query will need to be rendered to the initial message, + // so this needs to be implemented TODO("The $protocolName protocol doesn't support http payload traits") } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/StructuredDataSerializerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/StructuredDataSerializerGenerator.kt index dcd716a6ce..88e024e4f6 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/StructuredDataSerializerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/StructuredDataSerializerGenerator.kt @@ -11,30 +11,30 @@ import software.amazon.smithy.rust.codegen.smithy.RuntimeType interface StructuredDataSerializerGenerator { /** - * Generate a parse function for a given targeted as a payload. - * Entry point for payload-based parsing. - * Roughly: + * Generate a serializer for a request payload. Expected signature: * ```rust + * fn serialize_some_payload(input: &PayloadSmithyType) -> Result, Error> { + * ... + * } * ``` */ fun payloadSerializer(member: MemberShape): RuntimeType - /** Generate a serializer for operation input - * Because only a subset of fields of the operation may be impacted by the document, a builder is passed - * through: - * + /** + * Generate a serializer for an operation input. * ```rust - * fn parse_some_operation(inp: &[u8], builder: my_operation::Builder) -> Result { - * ... + * fn serialize_some_operation(input: &SomeSmithyType) -> Result { + * ... * } * ``` */ fun operationSerializer(operationShape: OperationShape): RuntimeType? /** + * Generate a serializer for a document. * ```rust - * fn parse_document(inp: &[u8]) -> Result { - * ... + * fn serialize_document(input: &Document) -> Result { + * ... * } * ``` */ diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGenerator.kt index 469c8e824f..20eec6fd09 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGenerator.kt @@ -121,7 +121,7 @@ class XmlBindingTraitSerializerGenerator( let mut writer = #{XmlWriter}::new(&mut out); ##[allow(unused_mut)] let mut root = writer.start_el(${operationXmlName.dq()})${inputShape.xmlNamespace().apply()}; - """, + """, *codegenScope ) serializeStructure(inputShape, xmlMembers, Ctx.Element("root", "&input")) @@ -140,10 +140,9 @@ class XmlBindingTraitSerializerGenerator( val target = model.expectShape(member.target, StructureShape::class.java) return RuntimeType.forInlineFun(fnName, "xml_ser") { val t = symbolProvider.toSymbol(member).rustType().stripOuter().render(true) - it.rustBlock( - "pub fn $fnName(input: &$t) -> Result<#T, String>", - - RuntimeType.sdkBody(runtimeConfig), + it.rustBlockTemplate( + "pub fn $fnName(input: &$t) -> std::result::Result, String>", + *codegenScope ) { rust("let mut out = String::new();") // create a scope for writer. This ensure that writer has been dropped before returning the @@ -156,7 +155,7 @@ class XmlBindingTraitSerializerGenerator( let mut root = writer.start_el(${xmlIndex.payloadShapeName(member).dq()})${ target.xmlNamespace().apply() }; - """, + """, *codegenScope ) serializeStructure( @@ -165,7 +164,7 @@ class XmlBindingTraitSerializerGenerator( Ctx.Element("root", "&input") ) } - rustTemplate("Ok(#{SdkBody}::from(out))", *codegenScope) + rustTemplate("Ok(out.into_bytes())", *codegenScope) } } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/RemoveEventStreamOperations.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/RemoveEventStreamOperations.kt index 815a73e60a..08ef69afc9 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/RemoveEventStreamOperations.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/RemoveEventStreamOperations.kt @@ -16,20 +16,34 @@ import java.util.logging.Logger /** Transformer to REMOVE operations that use EventStreaming until event streaming is supported */ object RemoveEventStreamOperations { private val logger = Logger.getLogger(javaClass.name) - fun transform(model: Model): Model = ModelTransformer.create().filterShapes(model) { parentShape -> - if (parentShape !is OperationShape) { - true - } else { - val ioShapes = listOfNotNull(parentShape.output.orNull(), parentShape.input.orNull()).map { model.expectShape(it, StructureShape::class.java) } - val hasEventStream = ioShapes.any { ioShape -> - val streamingMember = ioShape.findStreamingMember(model)?.let { model.expectShape(it.target) } - streamingMember?.isUnionShape ?: false - } - // If a streaming member has a union trait, it is an event stream. Event Streams are not currently supported - // by the SDK, so if we generate this API it won't work. - (!hasEventStream).also { - if (!it) { - logger.info("Removed $parentShape from model because it targets an event stream") + + private fun eventStreamEnabled(): Boolean = + System.getenv()["SMITHYRS_EXPERIMENTAL_EVENTSTREAM"] == "1" + + fun transform(model: Model): Model { + if (eventStreamEnabled()) { + return model + } + return ModelTransformer.create().filterShapes(model) { parentShape -> + if (parentShape !is OperationShape) { + true + } else { + val ioShapes = listOfNotNull(parentShape.output.orNull(), parentShape.input.orNull()).map { + model.expectShape( + it, + StructureShape::class.java + ) + } + val hasEventStream = ioShapes.any { ioShape -> + val streamingMember = ioShape.findStreamingMember(model)?.let { model.expectShape(it.target) } + streamingMember?.isUnionShape ?: false + } + // If a streaming member has a union trait, it is an event stream. Event Streams are not currently supported + // by the SDK, so if we generate this API it won't work. + (!hasEventStream).also { + if (!it) { + logger.info("Removed $parentShape from model because it targets an event stream") + } } } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/util/Smithy.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/util/Smithy.kt index 4391e6e7aa..ad572c3612 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/util/Smithy.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/util/Smithy.kt @@ -17,6 +17,7 @@ import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.model.traits.StreamingTrait import software.amazon.smithy.model.traits.Trait +import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticInputTrait inline fun Model.lookup(shapeId: String): T { return this.expectShape(ShapeId.from(shapeId), T::class.java) @@ -42,6 +43,31 @@ fun StructureShape.hasStreamingMember(model: Model) = this.findStreamingMember(m fun UnionShape.hasStreamingMember(model: Model) = this.findMemberWithTrait(model) != null fun MemberShape.isStreaming(model: Model) = this.getMemberTrait(model, StreamingTrait::class.java).isPresent +fun UnionShape.isEventStream(): Boolean { + return hasTrait(StreamingTrait::class.java) +} +fun MemberShape.isEventStream(model: Model): Boolean { + return (model.expectShape(target) as? UnionShape)?.isEventStream() ?: false +} +fun MemberShape.isInputEventStream(model: Model): Boolean { + return isEventStream(model) && model.expectShape(container).hasTrait() +} +fun MemberShape.isOutputEventStream(model: Model): Boolean { + return isEventStream(model) && model.expectShape(container).hasTrait() +} +private fun Shape.hasEventStreamMember(model: Model): Boolean { + return members().any { it.isEventStream(model) } +} +fun OperationShape.isInputEventStream(model: Model): Boolean { + return input.map { id -> model.expectShape(id).hasEventStreamMember(model) }.orElse(false) +} +fun OperationShape.isOutputEventStream(model: Model): Boolean { + return output.map { id -> model.expectShape(id).hasEventStreamMember(model) }.orElse(false) +} +fun OperationShape.isEventStream(model: Model): Boolean { + return isInputEventStream(model) || isOutputEventStream(model) +} + /* * Returns the member of this structure targeted with streaming trait (if it exists). * diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/UnionGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/UnionGeneratorTest.kt index 904e922f1a..0e70b6cabd 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/UnionGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/UnionGeneratorTest.kt @@ -6,6 +6,7 @@ package software.amazon.smithy.rust.codegen.generators import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotContain import org.junit.jupiter.api.Test import software.amazon.smithy.codegen.core.SymbolProvider import software.amazon.smithy.rust.codegen.rustlang.RustWriter @@ -77,6 +78,29 @@ class UnionGeneratorTest { ) } + @Test + fun `filter out errors for Event Stream unions`() { + val writer = generateUnion( + """ + @error("client") structure BadRequestException { } + @error("client") structure ConflictException { } + structure NormalMessage { } + + @streaming + union MyUnion { + BadRequestException: BadRequestException, + ConflictException: ConflictException, + NormalMessage: NormalMessage, + } + """ + ) + + val code = writer.toString() + code shouldNotContain "BadRequestException" + code shouldNotContain "ConflictException" + code shouldContain "NormalMessage" + } + private fun generateUnion(modelSmithy: String, unionName: String = "MyUnion"): RustWriter { val model = "namespace test\n$modelSmithy".asSmithyModel() val provider: SymbolProvider = testSymbolProvider(model) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/ResponseBindingGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/ResponseBindingGeneratorTest.kt index ac52121204..d455bd774d 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/ResponseBindingGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/ResponseBindingGeneratorTest.kt @@ -16,6 +16,7 @@ import software.amazon.smithy.rust.codegen.smithy.generators.ProtocolConfig import software.amazon.smithy.rust.codegen.smithy.generators.http.ResponseBindingGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.HttpLocation import software.amazon.smithy.rust.codegen.smithy.protocols.HttpTraitHttpBindingResolver +import software.amazon.smithy.rust.codegen.smithy.protocols.RestJson import software.amazon.smithy.rust.codegen.smithy.transformers.OperationNormalizer import software.amazon.smithy.rust.codegen.testutil.TestWorkspace import software.amazon.smithy.rust.codegen.testutil.asSmithyModel @@ -78,7 +79,9 @@ class ResponseBindingGeneratorTest { .filter { it.location == HttpLocation.HEADER } bindings.forEach { binding -> val runtimeType = ResponseBindingGenerator( - testProtocolConfig, operationShape + RestJson(testProtocolConfig), + testProtocolConfig, + operationShape ).generateDeserializeHeaderFn(binding) // little hack to force these functions to be generated rust("// use #T;", runtimeType) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/EventStreamSymbolProviderTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/EventStreamSymbolProviderTest.kt new file mode 100644 index 0000000000..a72a66024b --- /dev/null +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/EventStreamSymbolProviderTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package software.amazon.smithy.rust.codegen.smithy + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test +import software.amazon.smithy.model.shapes.MemberShape +import software.amazon.smithy.model.shapes.ServiceShape +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.rust.codegen.rustlang.RustType +import software.amazon.smithy.rust.codegen.smithy.transformers.OperationNormalizer +import software.amazon.smithy.rust.codegen.testutil.TestRuntimeConfig +import software.amazon.smithy.rust.codegen.testutil.asSmithyModel + +class EventStreamSymbolProviderTest { + @Test + fun `it should adjust types for operations with event streams`() { + // Transform the model so that it has synthetic inputs/outputs + val model = OperationNormalizer( + """ + namespace test + + structure Something { stuff: Blob } + + @streaming + union SomeStream { + Something: Something, + } + + structure TestInput { inputStream: SomeStream } + structure TestOutput { outputStream: SomeStream } + operation TestOperation { + input: TestInput, + output: TestOutput, + } + service TestService { version: "123", operations: [TestOperation] } + """.asSmithyModel() + ).transformModel() + + val service = model.expectShape(ShapeId.from("test#TestService")) as ServiceShape + val provider = EventStreamSymbolProvider(TestRuntimeConfig, SymbolVisitor(model, service, DefaultConfig), model) + + // Look up the synthetic input/output rather than the original input/output + val inputStream = model.expectShape(ShapeId.from("test#TestOperationInput\$inputStream")) as MemberShape + val outputStream = model.expectShape(ShapeId.from("test#TestOperationOutput\$outputStream")) as MemberShape + + val inputType = provider.toSymbol(inputStream).rustType() + val outputType = provider.toSymbol(outputStream).rustType() + + inputType shouldBe RustType.Opaque("EventStreamInput", "smithy_http::event_stream") + outputType shouldBe RustType.Opaque("Receiver", "smithy_http::event_stream") + } + + @Test + fun `it should leave alone types for operations without event streams`() { + val model = OperationNormalizer( + """ + namespace test + + structure Something { stuff: Blob } + + union NotStreaming { + Something: Something, + } + + structure TestInput { inputStream: NotStreaming } + structure TestOutput { outputStream: NotStreaming } + operation TestOperation { + input: TestInput, + output: TestOutput, + } + service TestService { version: "123", operations: [TestOperation] } + """.asSmithyModel() + ).transformModel() + + val service = model.expectShape(ShapeId.from("test#TestService")) as ServiceShape + val provider = EventStreamSymbolProvider(TestRuntimeConfig, SymbolVisitor(model, service, DefaultConfig), model) + + // Look up the synthetic input/output rather than the original input/output + val inputStream = model.expectShape(ShapeId.from("test#TestOperationInput\$inputStream")) as MemberShape + val outputStream = model.expectShape(ShapeId.from("test#TestOperationOutput\$outputStream")) as MemberShape + + val inputType = provider.toSymbol(inputStream).rustType() + val outputType = provider.toSymbol(outputStream).rustType() + + inputType shouldBe RustType.Option(RustType.Opaque("NotStreaming", "crate::model")) + outputType shouldBe RustType.Option(RustType.Opaque("NotStreaming", "crate::model")) + } +} diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGeneratorTest.kt index 15186706de..4575d02463 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGeneratorTest.kt @@ -124,7 +124,7 @@ internal class XmlBindingTraitSerializerGeneratorTest { .build() ).build().unwrap(); let serialized = ${writer.format(operationParser)}(&inp.payload.unwrap()).unwrap(); - let output = std::str::from_utf8(serialized.bytes().unwrap()).unwrap(); + let output = std::str::from_utf8(serialized).unwrap(); assert_eq!(output, "hello!"); """ ) diff --git a/rust-runtime/inlineable/Cargo.toml b/rust-runtime/inlineable/Cargo.toml index 5464f6769f..833b4a6d16 100644 --- a/rust-runtime/inlineable/Cargo.toml +++ b/rust-runtime/inlineable/Cargo.toml @@ -11,8 +11,9 @@ are to allow this crate to be compilable and testable in isolation, no client co [dependencies] "bytes" = "1" "http" = "0.2.1" -"smithy-types" = { version = "0.0.1", path = "../smithy-types" } -"smithy-http" = { version = "0.0.1", path = "../smithy-http" } +"smithy-eventstream" = { path = "../smithy-eventstream" } +"smithy-types" = { path = "../smithy-types" } +"smithy-http" = { path = "../smithy-http" } "smithy-json" = { path = "../smithy-json" } "smithy-query" = { path = "../smithy-query" } "smithy-xml" = { path = "../smithy-xml" } diff --git a/rust-runtime/inlineable/src/event_stream.rs b/rust-runtime/inlineable/src/event_stream.rs new file mode 100644 index 0000000000..a6003f648a --- /dev/null +++ b/rust-runtime/inlineable/src/event_stream.rs @@ -0,0 +1,154 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +use smithy_eventstream::error::Error; +use smithy_eventstream::frame::{Header, Message}; +use smithy_eventstream::str_bytes::StrBytes; + +pub struct ResponseHeaders<'a> { + pub content_type: &'a StrBytes, + pub message_type: &'a StrBytes, + pub smithy_type: &'a StrBytes, +} + +fn expect_header_str_value<'a>( + header: Option<&'a Header>, + name: &str, +) -> Result<&'a StrBytes, Error> { + match header { + Some(header) => Ok(header.value().as_string().map_err(|value| { + Error::Unmarshalling(format!( + "expected response {} header to be string, received {:?}", + name, value + )) + })?), + None => Err(Error::Unmarshalling(format!( + "expected response to include {} header, but it was missing", + name + ))), + } +} + +pub fn parse_response_headers(message: &Message) -> Result { + let (mut content_type, mut message_type, mut event_type, mut exception_type) = + (None, None, None, None); + for header in message.headers() { + match header.name().as_str() { + ":content-type" => content_type = Some(header), + ":message-type" => message_type = Some(header), + ":event-type" => event_type = Some(header), + ":exception-type" => exception_type = Some(header), + _ => {} + } + } + let message_type = expect_header_str_value(message_type, ":message-type")?; + Ok(ResponseHeaders { + content_type: expect_header_str_value(content_type, ":content-type")?, + message_type, + smithy_type: if message_type.as_str() == "event" { + expect_header_str_value(event_type, ":event-type")? + } else if message_type.as_str() == "exception" { + expect_header_str_value(exception_type, ":exception-type")? + } else { + return Err(Error::Unmarshalling(format!( + "unrecognized `:message-type`: {}", + message_type.as_str() + ))); + }, + }) +} + +#[cfg(test)] +mod tests { + use crate::event_stream::parse_response_headers; + use smithy_eventstream::frame::{Header, HeaderValue, Message}; + + #[test] + fn normal_message() { + let message = Message::new(&b"test"[..]) + .add_header(Header::new( + ":event-type", + HeaderValue::String("Foo".into()), + )) + .add_header(Header::new( + ":content-type", + HeaderValue::String("application/json".into()), + )) + .add_header(Header::new( + ":message-type", + HeaderValue::String("event".into()), + )); + let parsed = parse_response_headers(&message).unwrap(); + assert_eq!("Foo", parsed.smithy_type.as_str()); + assert_eq!("application/json", parsed.content_type.as_str()); + assert_eq!("event", parsed.message_type.as_str()); + } + + #[test] + fn error_message() { + let message = Message::new(&b"test"[..]) + .add_header(Header::new( + ":exception-type", + HeaderValue::String("BadRequestException".into()), + )) + .add_header(Header::new( + ":content-type", + HeaderValue::String("application/json".into()), + )) + .add_header(Header::new( + ":message-type", + HeaderValue::String("exception".into()), + )); + let parsed = parse_response_headers(&message).unwrap(); + assert_eq!("BadRequestException", parsed.smithy_type.as_str()); + assert_eq!("application/json", parsed.content_type.as_str()); + assert_eq!("exception", parsed.message_type.as_str()); + } + + #[test] + fn missing_exception_type() { + let message = Message::new(&b"test"[..]) + .add_header(Header::new( + ":content-type", + HeaderValue::String("application/json".into()), + )) + .add_header(Header::new( + ":message-type", + HeaderValue::String("exception".into()), + )); + let error = parse_response_headers(&message).err().unwrap().to_string(); + assert_eq!("failed to unmarshall message: expected response to include :exception-type header, but it was missing", error); + } + + #[test] + fn missing_event_type() { + let message = Message::new(&b"test"[..]) + .add_header(Header::new( + ":content-type", + HeaderValue::String("application/json".into()), + )) + .add_header(Header::new( + ":message-type", + HeaderValue::String("event".into()), + )); + let error = parse_response_headers(&message).err().unwrap().to_string(); + assert_eq!("failed to unmarshall message: expected response to include :event-type header, but it was missing", error); + } + + #[test] + fn missing_content_type() { + let message = Message::new(&b"test"[..]) + .add_header(Header::new( + ":event-type", + HeaderValue::String("Foo".into()), + )) + .add_header(Header::new( + ":message-type", + HeaderValue::String("event".into()), + )); + let error = parse_response_headers(&message).err().unwrap().to_string(); + assert_eq!("failed to unmarshall message: expected response to include :content-type header, but it was missing", error); + } +} diff --git a/rust-runtime/inlineable/src/lib.rs b/rust-runtime/inlineable/src/lib.rs index 5e2818082d..d225bb0e97 100644 --- a/rust-runtime/inlineable/src/lib.rs +++ b/rust-runtime/inlineable/src/lib.rs @@ -5,6 +5,8 @@ #[allow(dead_code)] mod ec2_query_errors; +#[allow(unused)] +mod event_stream; #[allow(dead_code)] mod idempotency_token; #[allow(dead_code)] diff --git a/rust-runtime/smithy-eventstream/src/error.rs b/rust-runtime/smithy-eventstream/src/error.rs index b0f311dbc5..a561b2f4bf 100644 --- a/rust-runtime/smithy-eventstream/src/error.rs +++ b/rust-runtime/smithy-eventstream/src/error.rs @@ -23,6 +23,8 @@ pub enum Error { PayloadTooLong, PreludeChecksumMismatch(u32, u32), TimestampValueTooLarge(Instant), + Marshalling(String), + Unmarshalling(String), } impl StdError for Error {} @@ -56,6 +58,8 @@ impl fmt::Display for Error { "timestamp value {:?} is too large to fit into an i64", time ), + Marshalling(error) => write!(f, "failed to marshall message: {}", error), + Unmarshalling(error) => write!(f, "failed to unmarshall message: {}", error), } } } diff --git a/rust-runtime/smithy-eventstream/src/frame.rs b/rust-runtime/smithy-eventstream/src/frame.rs index 5d02ea5c49..e9dede4a04 100644 --- a/rust-runtime/smithy-eventstream/src/frame.rs +++ b/rust-runtime/smithy-eventstream/src/frame.rs @@ -12,6 +12,7 @@ use crate::str_bytes::StrBytes; use bytes::{Buf, BufMut, Bytes}; use std::convert::{TryFrom, TryInto}; use std::error::Error as StdError; +use std::fmt; use std::mem::size_of; const PRELUDE_LENGTH_BYTES: u32 = 3 * size_of::() as u32; @@ -23,24 +24,35 @@ const MIN_HEADER_LEN: usize = 2; pub type SignMessageError = Box; /// Signs an Event Stream message. -pub trait SignMessage { +pub trait SignMessage: fmt::Debug { fn sign(&mut self, message: Message) -> Result; } /// Converts a Smithy modeled Event Stream type into a [`Message`](Message). -pub trait MarshallMessage { +pub trait MarshallMessage: fmt::Debug { /// Smithy modeled input type to convert from. type Input; fn marshall(&self, input: Self::Input) -> Result; } +/// A successfully unmarshalled message that is either an `Event` or an `Error`. +pub enum UnmarshalledMessage { + Event(T), + Error(E), +} + /// Converts an Event Stream [`Message`](Message) into a Smithy modeled type. -pub trait UnmarshallMessage { +pub trait UnmarshallMessage: fmt::Debug { /// Smithy modeled type to convert into. type Output; + /// Smithy modeled error to convert into. + type Error; - fn unmarshall(&self, message: Message) -> Result; + fn unmarshall( + &self, + message: Message, + ) -> Result, Error>; } mod value { @@ -78,6 +90,71 @@ mod value { Uuid(u128), } + impl HeaderValue { + pub fn as_bool(&self) -> Result { + match self { + HeaderValue::Bool(value) => Ok(*value), + _ => Err(self), + } + } + + pub fn as_byte(&self) -> Result { + match self { + HeaderValue::Byte(value) => Ok(*value), + _ => Err(self), + } + } + + pub fn as_int16(&self) -> Result { + match self { + HeaderValue::Int16(value) => Ok(*value), + _ => Err(self), + } + } + + pub fn as_int32(&self) -> Result { + match self { + HeaderValue::Int32(value) => Ok(*value), + _ => Err(self), + } + } + + pub fn as_int64(&self) -> Result { + match self { + HeaderValue::Int64(value) => Ok(*value), + _ => Err(self), + } + } + + pub fn as_byte_array(&self) -> Result<&Bytes, &Self> { + match self { + HeaderValue::ByteArray(value) => Ok(value), + _ => Err(self), + } + } + + pub fn as_string(&self) -> Result<&StrBytes, &Self> { + match self { + HeaderValue::String(value) => Ok(value), + _ => Err(self), + } + } + + pub fn as_timestamp(&self) -> Result { + match self { + HeaderValue::Timestamp(value) => Ok(*value), + _ => Err(self), + } + } + + pub fn as_uuid(&self) -> Result { + match self { + HeaderValue::Uuid(value) => Ok(*value), + _ => Err(self), + } + } + } + macro_rules! read_value { ($buf:ident, $typ:ident, $size_typ:ident, $read_fn:ident) => { if $buf.remaining() >= size_of::<$size_typ>() { @@ -289,6 +366,14 @@ impl Message { } } + /// Creates a message with the given `headers` and `payload`. + pub fn new_from_parts(headers: Vec
, payload: impl Into) -> Self { + Self { + headers, + payload: payload.into(), + } + } + /// Adds a header to the message. pub fn add_header(mut self, header: Header) -> Self { self.headers.push(header); @@ -609,7 +694,7 @@ pub enum DecodedFrame { /// Streaming decoder for decoding a [`Message`] from a stream. #[non_exhaustive] -#[derive(Default)] +#[derive(Default, Debug)] pub struct MessageFrameDecoder { prelude: [u8; PRELUDE_LENGTH_BYTES_USIZE], prelude_read: bool, diff --git a/rust-runtime/smithy-http/src/event_stream.rs b/rust-runtime/smithy-http/src/event_stream.rs index 137534badb..4be9cf58c6 100644 --- a/rust-runtime/smithy-http/src/event_stream.rs +++ b/rust-runtime/smithy-http/src/event_stream.rs @@ -14,12 +14,49 @@ use hyper::body::HttpBody; use pin_project::pin_project; use smithy_eventstream::frame::{ DecodedFrame, MarshallMessage, MessageFrameDecoder, SignMessage, UnmarshallMessage, + UnmarshalledMessage, }; use std::error::Error as StdError; +use std::fmt; use std::marker::PhantomData; use std::pin::Pin; use std::task::{Context, Poll}; +pub type BoxError = Box; + +/// Input type for Event Streams. +pub struct EventStreamInput { + input_stream: Pin> + Send>>, +} + +impl fmt::Debug for EventStreamInput { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "EventStreamInput(Box)") + } +} + +impl EventStreamInput { + #[doc(hidden)] + pub fn into_body_stream( + self, + marshaller: impl MarshallMessage + Send + Sync + 'static, + signer: impl SignMessage + Send + Sync + 'static, + ) -> MessageStreamAdapter { + MessageStreamAdapter::new(marshaller, signer, self.input_stream) + } +} + +impl From for EventStreamInput +where + S: Stream> + Send + 'static, +{ + fn from(stream: S) -> Self { + EventStreamInput { + input_stream: Box::pin(stream), + } + } +} + /// Adapts a `Stream` to a signed `Stream` by using the provided /// message marshaller and signer implementations. /// @@ -30,24 +67,32 @@ pub struct MessageStreamAdapter { marshaller: Box + Send + Sync>, signer: Box, #[pin] - stream: Pin> + Send + Sync>>, + stream: Pin> + Send>>, + _phantom: PhantomData, } -impl MessageStreamAdapter { +impl MessageStreamAdapter +where + E: StdError + Send + Sync + 'static, +{ pub fn new( marshaller: impl MarshallMessage + Send + Sync + 'static, signer: impl SignMessage + Send + Sync + 'static, - stream: impl Stream> + Send + Sync + 'static, + stream: Pin> + Send>>, ) -> Self { MessageStreamAdapter { marshaller: Box::new(marshaller), signer: Box::new(signer), - stream: Box::pin(stream), + stream, + _phantom: Default::default(), } } } -impl Stream for MessageStreamAdapter { +impl Stream for MessageStreamAdapter +where + E: StdError + Send + Sync + 'static, +{ type Item = Result>; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { @@ -56,7 +101,7 @@ impl Stream for MessageStreamAdapter { if let Some(message_result) = message_option { let message_result = - message_result.map_err(|err| SdkError::ConstructionFailure(Box::new(err))); + message_result.map_err(|err| SdkError::ConstructionFailure(err)); let message = this .marshaller .marshall(message_result?) @@ -80,17 +125,21 @@ impl Stream for MessageStreamAdapter { - unmarshaller: Box>, +#[derive(Debug)] +pub struct Receiver { + unmarshaller: Box>, decoder: MessageFrameDecoder, buffer: SegmentedBuf, body: SdkBody, _phantom: PhantomData, } -impl Receiver { +impl Receiver { /// Creates a new `Receiver` with the given message unmarshaller and SDK body. - pub fn new(unmarshaller: impl UnmarshallMessage + 'static, body: SdkBody) -> Self { + pub fn new( + unmarshaller: impl UnmarshallMessage + 'static, + body: SdkBody, + ) -> Self { Receiver { unmarshaller: Box::new(unmarshaller), decoder: MessageFrameDecoder::new(), @@ -119,11 +168,16 @@ impl Receiver { .decode_frame(&mut self.buffer) .map_err(|err| SdkError::DispatchFailure(Box::new(err)))? { - return Ok(Some( - self.unmarshaller - .unmarshall(message) - .map_err(|err| SdkError::DispatchFailure(Box::new(err)))?, - )); + return match self + .unmarshaller + .unmarshall(message) + .map_err(|err| SdkError::DispatchFailure(Box::new(err)))? + { + UnmarshalledMessage::Event(event) => Ok(Some(event)), + UnmarshalledMessage::Error(err) => { + Err(SdkError::ServiceError { err, raw: None }) + } + }; } } Ok(None) @@ -134,7 +188,7 @@ impl Receiver { mod tests { use super::{MarshallMessage, Receiver, UnmarshallMessage}; use crate::body::SdkBody; - use crate::event_stream::MessageStreamAdapter; + use crate::event_stream::{EventStreamInput, MessageStreamAdapter}; use crate::result::SdkError; use async_stream::stream; use bytes::Bytes; @@ -142,7 +196,9 @@ mod tests { use futures_util::stream::StreamExt; use hyper::body::Body; use smithy_eventstream::error::Error as EventStreamError; - use smithy_eventstream::frame::{Header, HeaderValue, Message, SignMessage, SignMessageError}; + use smithy_eventstream::frame::{ + Header, HeaderValue, Message, SignMessage, SignMessageError, UnmarshalledMessage, + }; use std::error::Error as StdError; use std::io::{Error as IOError, ErrorKind}; @@ -164,25 +220,31 @@ mod tests { impl StdError for FakeError {} #[derive(Debug, Eq, PartialEq)] - struct UnmarshalledMessage(String); + struct TestMessage(String); + #[derive(Debug)] struct Marshaller; impl MarshallMessage for Marshaller { - type Input = UnmarshalledMessage; + type Input = TestMessage; fn marshall(&self, input: Self::Input) -> Result { Ok(Message::new(input.0.as_bytes().to_vec())) } } + #[derive(Debug)] struct Unmarshaller; impl UnmarshallMessage for Unmarshaller { - type Output = UnmarshalledMessage; + type Output = TestMessage; + type Error = EventStreamError; - fn unmarshall(&self, message: Message) -> Result { - Ok(UnmarshalledMessage( + fn unmarshall( + &self, + message: Message, + ) -> Result, EventStreamError> { + Ok(UnmarshalledMessage::Event(TestMessage( std::str::from_utf8(&message.payload()[..]).unwrap().into(), - )) + ))) } } @@ -192,14 +254,13 @@ mod tests { vec![Ok(encode_message("one")), Ok(encode_message("two"))]; let chunk_stream = futures_util::stream::iter(chunks); let body = SdkBody::from(Body::wrap_stream(chunk_stream)); - let mut receiver = - Receiver::::new(Unmarshaller, body); + let mut receiver = Receiver::::new(Unmarshaller, body); assert_eq!( - UnmarshalledMessage("one".into()), + TestMessage("one".into()), receiver.recv().await.unwrap().unwrap() ); assert_eq!( - UnmarshalledMessage("two".into()), + TestMessage("two".into()), receiver.recv().await.unwrap().unwrap() ); } @@ -212,10 +273,9 @@ mod tests { ]; let chunk_stream = futures_util::stream::iter(chunks); let body = SdkBody::from(Body::wrap_stream(chunk_stream)); - let mut receiver = - Receiver::::new(Unmarshaller, body); + let mut receiver = Receiver::::new(Unmarshaller, body); assert_eq!( - UnmarshalledMessage("one".into()), + TestMessage("one".into()), receiver.recv().await.unwrap().unwrap() ); assert!(matches!( @@ -234,10 +294,9 @@ mod tests { ]; let chunk_stream = futures_util::stream::iter(chunks); let body = SdkBody::from(Body::wrap_stream(chunk_stream)); - let mut receiver = - Receiver::::new(Unmarshaller, body); + let mut receiver = Receiver::::new(Unmarshaller, body); assert_eq!( - UnmarshalledMessage("one".into()), + TestMessage("one".into()), receiver.recv().await.unwrap().unwrap() ); assert!(matches!( @@ -246,6 +305,16 @@ mod tests { )); } + #[derive(Debug)] + struct TestServiceError; + impl std::fmt::Display for TestServiceError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "TestServiceError") + } + } + impl StdError for TestServiceError {} + + #[derive(Debug)] struct TestSigner; impl SignMessage for TestSigner { fn sign(&mut self, message: Message) -> Result { @@ -267,12 +336,15 @@ mod tests { #[tokio::test] async fn message_stream_adapter_success() { let stream = stream! { - yield Ok(UnmarshalledMessage("test".into())); + yield Ok(TestMessage("test".into())); }; let mut adapter = - check_compatible_with_hyper_wrap_stream( - MessageStreamAdapter::<_, EventStreamError>::new(Marshaller, TestSigner, stream), - ); + check_compatible_with_hyper_wrap_stream(MessageStreamAdapter::< + TestMessage, + TestServiceError, + >::new( + Marshaller, TestSigner, Box::pin(stream) + )); let mut sent_bytes = adapter.next().await.unwrap().unwrap(); let sent = Message::read_from(&mut sent_bytes).unwrap(); @@ -285,12 +357,15 @@ mod tests { #[tokio::test] async fn message_stream_adapter_construction_failure() { let stream = stream! { - yield Err(EventStreamError::InvalidMessageLength); + yield Err(EventStreamError::InvalidMessageLength.into()); }; let mut adapter = - check_compatible_with_hyper_wrap_stream( - MessageStreamAdapter::::new(Marshaller, TestSigner, stream), - ); + check_compatible_with_hyper_wrap_stream(MessageStreamAdapter::< + TestMessage, + TestServiceError, + >::new( + Marshaller, TestSigner, Box::pin(stream) + )); let result = adapter.next().await.unwrap(); assert!(result.is_err()); @@ -299,4 +374,18 @@ mod tests { SdkError::ConstructionFailure(_) )); } + + // Verify the developer experience for this compiles + #[allow(unused)] + fn event_stream_input_ergonomics() { + fn check(input: impl Into>) { + let _: EventStreamInput = input.into(); + } + check(stream! { + yield Ok(TestMessage("test".into())); + }); + check(stream! { + yield Err(EventStreamError::InvalidMessageLength.into()); + }); + } } diff --git a/rust-runtime/smithy-http/src/middleware.rs b/rust-runtime/smithy-http/src/middleware.rs index f94c6971fe..fedd306402 100644 --- a/rust-runtime/smithy-http/src/middleware.rs +++ b/rust-runtime/smithy-http/src/middleware.rs @@ -139,6 +139,9 @@ fn sdk_result( ) -> Result, SdkError> { match parsed { Ok(parsed) => Ok(SdkSuccess { raw, parsed }), - Err(err) => Err(SdkError::ServiceError { raw, err }), + Err(err) => Err(SdkError::ServiceError { + raw: Some(raw), + err, + }), } } diff --git a/rust-runtime/smithy-http/src/operation.rs b/rust-runtime/smithy-http/src/operation.rs index 76a8926173..be49033b6c 100644 --- a/rust-runtime/smithy-http/src/operation.rs +++ b/rust-runtime/smithy-http/src/operation.rs @@ -159,6 +159,11 @@ impl Request { } } + /// Creates a new operation `Request` from its parts. + pub fn from_parts(inner: http::Request, properties: Arc>) -> Self { + Request { inner, properties } + } + /// Allows modification of the HTTP request and associated properties with a fallible closure. pub fn augment( self, diff --git a/rust-runtime/smithy-http/src/result.rs b/rust-runtime/smithy-http/src/result.rs index 5cda96d517..1940d79c57 100644 --- a/rust-runtime/smithy-http/src/result.rs +++ b/rust-runtime/smithy-http/src/result.rs @@ -35,7 +35,10 @@ pub enum SdkError { }, /// An error response was received from the service - ServiceError { err: E, raw: operation::Response }, + ServiceError { + err: E, + raw: Option, + }, } impl Display for SdkError From 30ea2746b36a1e0c43490996869911a712d6e301 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Tue, 17 Aug 2021 14:28:55 -0700 Subject: [PATCH 02/17] Make the raw response in SdkError generic --- aws/rust-runtime/aws-http/src/lib.rs | 28 ++++++++----------- .../parse/EventStreamUnmarshallerGenerator.kt | 2 +- rust-runtime/smithy-eventstream/src/frame.rs | 2 +- rust-runtime/smithy-http/src/event_stream.rs | 10 +++---- rust-runtime/smithy-http/src/middleware.rs | 5 +--- rust-runtime/smithy-http/src/result.rs | 9 ++---- 6 files changed, 23 insertions(+), 33 deletions(-) diff --git a/aws/rust-runtime/aws-http/src/lib.rs b/aws/rust-runtime/aws-http/src/lib.rs index 3c5ad304f7..6546b7dd67 100644 --- a/aws/rust-runtime/aws-http/src/lib.rs +++ b/aws/rust-runtime/aws-http/src/lib.rs @@ -63,16 +63,14 @@ where Err(SdkError::ServiceError { err, raw }) => (err, raw), Err(_) => return RetryKind::NotRetryable, }; - if let Some(response) = &response { - if let Some(retry_after_delay) = response - .http() - .headers() - .get("x-amz-retry-after") - .and_then(|header| header.to_str().ok()) - .and_then(|header| header.parse::().ok()) - { - return RetryKind::Explicit(Duration::from_millis(retry_after_delay)); - } + if let Some(retry_after_delay) = response + .http() + .headers() + .get("x-amz-retry-after") + .and_then(|header| header.to_str().ok()) + .and_then(|header| header.parse::().ok()) + { + return RetryKind::Explicit(Duration::from_millis(retry_after_delay)); } if let Some(kind) = err.retryable_error_kind() { return RetryKind::Error(kind); @@ -85,11 +83,9 @@ where return RetryKind::Error(ErrorKind::TransientError); } }; - if let Some(status) = response.as_ref().map(|resp| resp.http().status().as_u16()) { - if TRANSIENT_ERROR_STATUS_CODES.contains(&status) { - return RetryKind::Error(ErrorKind::TransientError); - }; - } + if TRANSIENT_ERROR_STATUS_CODES.contains(&response.http().status().as_u16()) { + return RetryKind::Error(ErrorKind::TransientError); + }; // TODO: is IDPCommunicationError modeled yet? RetryKind::NotRetryable } @@ -137,7 +133,7 @@ mod test { ) -> Result, SdkError> { Err(SdkError::ServiceError { err, - raw: Some(operation::Response::new(raw.map(|b| SdkBody::from(b)))), + raw: operation::Response::new(raw.map(|b| SdkBody::from(b))), }) } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt index 75c380405d..da73a75dcd 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt @@ -86,7 +86,7 @@ class EventStreamUnmarshallerGenerator( """ fn unmarshall( &self, - message: #{Message} + message: &#{Message} ) -> std::result::Result<#{UnmarshalledMessage}, #{Error}> """, *codegenScope diff --git a/rust-runtime/smithy-eventstream/src/frame.rs b/rust-runtime/smithy-eventstream/src/frame.rs index e9dede4a04..600fbd944f 100644 --- a/rust-runtime/smithy-eventstream/src/frame.rs +++ b/rust-runtime/smithy-eventstream/src/frame.rs @@ -51,7 +51,7 @@ pub trait UnmarshallMessage: fmt::Debug { fn unmarshall( &self, - message: Message, + message: &Message, ) -> Result, Error>; } diff --git a/rust-runtime/smithy-http/src/event_stream.rs b/rust-runtime/smithy-http/src/event_stream.rs index 4be9cf58c6..b144c33c13 100644 --- a/rust-runtime/smithy-http/src/event_stream.rs +++ b/rust-runtime/smithy-http/src/event_stream.rs @@ -13,7 +13,7 @@ use futures_core::Stream; use hyper::body::HttpBody; use pin_project::pin_project; use smithy_eventstream::frame::{ - DecodedFrame, MarshallMessage, MessageFrameDecoder, SignMessage, UnmarshallMessage, + DecodedFrame, MarshallMessage, Message, MessageFrameDecoder, SignMessage, UnmarshallMessage, UnmarshalledMessage, }; use std::error::Error as StdError; @@ -153,7 +153,7 @@ impl Receiver { /// it returns an `Ok(None)`. If there is a transport layer error, it will return /// `Err(SdkError::DispatchFailure)`. Service-modeled errors will be a part of the returned /// messages. - pub async fn recv(&mut self) -> Result, SdkError> { + pub async fn recv(&mut self) -> Result, SdkError> { let next_chunk = self .body .data() @@ -170,12 +170,12 @@ impl Receiver { { return match self .unmarshaller - .unmarshall(message) + .unmarshall(&message) .map_err(|err| SdkError::DispatchFailure(Box::new(err)))? { UnmarshalledMessage::Event(event) => Ok(Some(event)), UnmarshalledMessage::Error(err) => { - Err(SdkError::ServiceError { err, raw: None }) + Err(SdkError::ServiceError { err, raw: message }) } }; } @@ -240,7 +240,7 @@ mod tests { fn unmarshall( &self, - message: Message, + message: &Message, ) -> Result, EventStreamError> { Ok(UnmarshalledMessage::Event(TestMessage( std::str::from_utf8(&message.payload()[..]).unwrap().into(), diff --git a/rust-runtime/smithy-http/src/middleware.rs b/rust-runtime/smithy-http/src/middleware.rs index fedd306402..f94c6971fe 100644 --- a/rust-runtime/smithy-http/src/middleware.rs +++ b/rust-runtime/smithy-http/src/middleware.rs @@ -139,9 +139,6 @@ fn sdk_result( ) -> Result, SdkError> { match parsed { Ok(parsed) => Ok(SdkSuccess { raw, parsed }), - Err(err) => Err(SdkError::ServiceError { - raw: Some(raw), - err, - }), + Err(err) => Err(SdkError::ServiceError { raw, err }), } } diff --git a/rust-runtime/smithy-http/src/result.rs b/rust-runtime/smithy-http/src/result.rs index 1940d79c57..9ade7aefe2 100644 --- a/rust-runtime/smithy-http/src/result.rs +++ b/rust-runtime/smithy-http/src/result.rs @@ -19,7 +19,7 @@ pub struct SdkSuccess { /// Failed SDK Result #[derive(Debug)] -pub enum SdkError { +pub enum SdkError { /// The request failed during construction. It was not dispatched over the network. ConstructionFailure(BoxError), @@ -35,13 +35,10 @@ pub enum SdkError { }, /// An error response was received from the service - ServiceError { - err: E, - raw: Option, - }, + ServiceError { err: E, raw: R }, } -impl Display for SdkError +impl Display for SdkError where E: Error, { From 4d5c6c2706c067c3aa37d93115e7edeeffae936a Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Tue, 17 Aug 2021 14:33:45 -0700 Subject: [PATCH 03/17] Fix XmlBindingTraitSerializerGeneratorTest --- .../rust/codegen/smithy/generators/BuilderGenerator.kt | 10 +++++----- .../XmlBindingTraitSerializerGeneratorTest.kt | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/BuilderGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/BuilderGenerator.kt index 1ae0bbf3ad..62cdec6baa 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/BuilderGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/BuilderGenerator.kt @@ -51,12 +51,12 @@ class OperationBuildError(private val runtimeConfig: RuntimeConfig) { /** setter names will never hit a reserved word and therefore never need escaping */ fun MemberShape.setterName(): String = "set_${this.memberName.toSnakeCase()}" -open class BuilderGenerator( - protected val model: Model, - protected val symbolProvider: RustSymbolProvider, - protected val shape: StructureShape +class BuilderGenerator( + private val model: Model, + private val symbolProvider: RustSymbolProvider, + private val shape: StructureShape ) { - protected val runtimeConfig = symbolProvider.config().runtimeConfig + private val runtimeConfig = symbolProvider.config().runtimeConfig private val members: List = shape.allMembers.values.toList() private val structureSymbol = symbolProvider.toSymbol(shape) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGeneratorTest.kt index 4575d02463..63f585f546 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGeneratorTest.kt @@ -124,7 +124,7 @@ internal class XmlBindingTraitSerializerGeneratorTest { .build() ).build().unwrap(); let serialized = ${writer.format(operationParser)}(&inp.payload.unwrap()).unwrap(); - let output = std::str::from_utf8(serialized).unwrap(); + let output = std::str::from_utf8(&serialized).unwrap(); assert_eq!(output, "hello!"); """ ) From 4ce410b8bfb29c08866a8166acb6b86c5c69ac5c Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Tue, 17 Aug 2021 14:59:18 -0700 Subject: [PATCH 04/17] Make the build aware of the SMITHYRS_EXPERIMENTAL_EVENTSTREAM switch --- aws/sdk/build.gradle.kts | 2 ++ codegen-test/build.gradle.kts | 2 ++ .../codegen/smithy/transformers/RemoveEventStreamOperations.kt | 1 + 3 files changed, 5 insertions(+) diff --git a/aws/sdk/build.gradle.kts b/aws/sdk/build.gradle.kts index 35b57def7e..cc04d0c1c0 100644 --- a/aws/sdk/build.gradle.kts +++ b/aws/sdk/build.gradle.kts @@ -235,6 +235,8 @@ task("generateSmithyBuild") { projectDir.resolve("smithy-build.json").writeText(generateSmithyBuild(awsServices)) } inputs.property("servicelist", awsServices.sortedBy { it.module }.toString()) + // TODO(EventStream): Remove this when removing SMITHYRS_EXPERIMENTAL_EVENTSTREAM + inputs.property("_eventStreamCacheInvalidation", System.getenv("SMITHYRS_EXPERIMENTAL_EVENTSTREAM")) inputs.dir(projectDir.resolve("aws-models")) outputs.file(projectDir.resolve("smithy-build.json")) } diff --git a/codegen-test/build.gradle.kts b/codegen-test/build.gradle.kts index 91909f8ca7..fdda54b953 100644 --- a/codegen-test/build.gradle.kts +++ b/codegen-test/build.gradle.kts @@ -106,6 +106,8 @@ task("generateSmithyBuild") { doFirst { projectDir.resolve("smithy-build.json").writeText(generateSmithyBuild(CodegenTests)) } + // TODO(EventStream): Remove this when removing SMITHYRS_EXPERIMENTAL_EVENTSTREAM + inputs.property("_eventStreamCacheInvalidation", System.getenv("SMITHYRS_EXPERIMENTAL_EVENTSTREAM")) } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/RemoveEventStreamOperations.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/RemoveEventStreamOperations.kt index 08ef69afc9..d48907500c 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/RemoveEventStreamOperations.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/RemoveEventStreamOperations.kt @@ -13,6 +13,7 @@ import software.amazon.smithy.rust.codegen.util.findStreamingMember import software.amazon.smithy.rust.codegen.util.orNull import java.util.logging.Logger +// TODO(EventStream): Remove this class once the Event Stream implementation is stable /** Transformer to REMOVE operations that use EventStreaming until event streaming is supported */ object RemoveEventStreamOperations { private val logger = Logger.getLogger(javaClass.name) From ef47f2c567f2a10a280961f7d42ad919011dcb9f Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Tue, 17 Aug 2021 15:03:50 -0700 Subject: [PATCH 05/17] Fix SigV4SigningCustomizationTest --- .../amazon/smithy/rustsdk/SigV4SigningCustomizationTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/SigV4SigningCustomizationTest.kt b/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/SigV4SigningCustomizationTest.kt index 86f05d6723..bcdba1dd6c 100644 --- a/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/SigV4SigningCustomizationTest.kt +++ b/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/SigV4SigningCustomizationTest.kt @@ -7,7 +7,6 @@ package software.amazon.smithy.rustsdk import org.junit.jupiter.api.Test import software.amazon.smithy.aws.traits.auth.SigV4Trait -import software.amazon.smithy.rust.codegen.testutil.TestRuntimeConfig import software.amazon.smithy.rust.codegen.testutil.compileAndTest import software.amazon.smithy.rust.codegen.testutil.stubConfigProject import software.amazon.smithy.rust.codegen.testutil.unitTest @@ -17,7 +16,7 @@ internal class SigV4SigningCustomizationTest { fun `generates a valid config`() { val project = stubConfigProject( SigV4SigningConfig( - TestRuntimeConfig, + AwsTestRuntimeConfig, SigV4Trait.builder().name("test-service").build() ) ) From 035ee1e79c8cf97a7eadea5972d6b6ecc1fb593b Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Tue, 17 Aug 2021 15:04:53 -0700 Subject: [PATCH 06/17] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab88f20a2a..80ad448ec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ vNext (Month Day, Year) **New this week** +- (When complete) Add Event Stream support (#653, #xyz) - (When complete) Add profile file provider for region (#594, #xyz) - Add experimental `dvr` module to smithy-client. This will enable easier testing of HTTP traffic. (#640) - Add profile file credential provider implementation. This implementation currently does not support credential sources From 7d2ab92a8e3bf056acfad2151a28a0971f870fb5 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Tue, 17 Aug 2021 15:31:40 -0700 Subject: [PATCH 07/17] Fix build when SMITHYRS_EXPERIMENTAL_EVENTSTREAM is not set --- aws/sdk/build.gradle.kts | 2 +- codegen-test/build.gradle.kts | 2 +- .../rust/codegen/smithy/generators/BuilderGenerator.kt | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/aws/sdk/build.gradle.kts b/aws/sdk/build.gradle.kts index cc04d0c1c0..8eaf48a3e1 100644 --- a/aws/sdk/build.gradle.kts +++ b/aws/sdk/build.gradle.kts @@ -236,7 +236,7 @@ task("generateSmithyBuild") { } inputs.property("servicelist", awsServices.sortedBy { it.module }.toString()) // TODO(EventStream): Remove this when removing SMITHYRS_EXPERIMENTAL_EVENTSTREAM - inputs.property("_eventStreamCacheInvalidation", System.getenv("SMITHYRS_EXPERIMENTAL_EVENTSTREAM")) + inputs.property("_eventStreamCacheInvalidation", System.getenv("SMITHYRS_EXPERIMENTAL_EVENTSTREAM") ?: "0") inputs.dir(projectDir.resolve("aws-models")) outputs.file(projectDir.resolve("smithy-build.json")) } diff --git a/codegen-test/build.gradle.kts b/codegen-test/build.gradle.kts index fdda54b953..e2464050d3 100644 --- a/codegen-test/build.gradle.kts +++ b/codegen-test/build.gradle.kts @@ -107,7 +107,7 @@ task("generateSmithyBuild") { projectDir.resolve("smithy-build.json").writeText(generateSmithyBuild(CodegenTests)) } // TODO(EventStream): Remove this when removing SMITHYRS_EXPERIMENTAL_EVENTSTREAM - inputs.property("_eventStreamCacheInvalidation", System.getenv("SMITHYRS_EXPERIMENTAL_EVENTSTREAM")) + inputs.property("_eventStreamCacheInvalidation", System.getenv("SMITHYRS_EXPERIMENTAL_EVENTSTREAM") ?: "0") } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/BuilderGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/BuilderGenerator.kt index 62cdec6baa..21d23c4bb8 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/BuilderGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/BuilderGenerator.kt @@ -107,13 +107,13 @@ class BuilderGenerator( } // TODO(EventStream): [DX] Update builders to take EventInputStream as Into - open fun renderBuilderMember(writer: RustWriter, member: MemberShape, memberName: String, memberSymbol: Symbol) { + private fun renderBuilderMember(writer: RustWriter, member: MemberShape, memberName: String, memberSymbol: Symbol) { // builder members are crate-public to enable using them // directly in serializers/deserializers writer.write("pub(crate) $memberName: #T,", memberSymbol) } - open fun renderBuilderMemberFn( + private fun renderBuilderMemberFn( writer: RustWriter, coreType: RustType, member: MemberShape, @@ -138,7 +138,7 @@ class BuilderGenerator( } } - open fun renderBuilderMemberSetterFn( + private fun renderBuilderMemberSetterFn( writer: RustWriter, outerType: RustType, member: MemberShape, From 7733d477c33437bef112a10b70ea5ad9ea706b6a Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Tue, 17 Aug 2021 19:09:24 -0700 Subject: [PATCH 08/17] Add initial unit test for EventStreamUnmarshallerGenerator --- .../parse/EventStreamUnmarshallerGenerator.kt | 96 ++++++--- .../smithy/protocols/EventStreamTestTools.kt | 192 ++++++++++++++++++ .../EventStreamUnmarshallerGeneratorTest.kt | 77 +++++++ rust-runtime/smithy-eventstream/src/frame.rs | 1 + 4 files changed, 337 insertions(+), 29 deletions(-) create mode 100644 codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/EventStreamTestTools.kt create mode 100644 codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGeneratorTest.kt diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt index da73a75dcd..1dbc318381 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt @@ -15,12 +15,15 @@ import software.amazon.smithy.model.shapes.StringShape import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.model.traits.ErrorTrait +import software.amazon.smithy.model.traits.EventHeaderTrait +import software.amazon.smithy.model.traits.EventPayloadTrait import software.amazon.smithy.rust.codegen.rustlang.CargoDependency import software.amazon.smithy.rust.codegen.rustlang.RustWriter import software.amazon.smithy.rust.codegen.rustlang.rust import software.amazon.smithy.rust.codegen.rustlang.rustBlock import software.amazon.smithy.rust.codegen.rustlang.rustBlockTemplate import software.amazon.smithy.rust.codegen.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.rustlang.withBlock import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig import software.amazon.smithy.rust.codegen.smithy.RuntimeType import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider @@ -30,11 +33,10 @@ import software.amazon.smithy.rust.codegen.util.dq import software.amazon.smithy.rust.codegen.util.hasTrait import software.amazon.smithy.rust.codegen.util.toPascalCase -// TODO(EventStream): [TEST] Unit test EventStreamUnmarshallerGenerator class EventStreamUnmarshallerGenerator( private val protocol: Protocol, private val model: Model, - private val runtimeConfig: RuntimeConfig, + runtimeConfig: RuntimeConfig, private val symbolProvider: RustSymbolProvider, private val operationShape: OperationShape, private val unionShape: UnionShape, @@ -43,14 +45,15 @@ class EventStreamUnmarshallerGenerator( private val operationErrorSymbol = operationShape.errorSymbol(symbolProvider) private val smithyEventStream = CargoDependency.SmithyEventStream(runtimeConfig) private val codegenScope = arrayOf( - "UnmarshallMessage" to RuntimeType("UnmarshallMessage", smithyEventStream, "smithy_eventstream::frame"), - "UnmarshalledMessage" to RuntimeType("UnmarshalledMessage", smithyEventStream, "smithy_eventstream::frame"), - "Message" to RuntimeType("Message", smithyEventStream, "smithy_eventstream::frame"), + "Blob" to RuntimeType("Blob", CargoDependency.SmithyTypes(runtimeConfig), "smithy_types"), + "Error" to RuntimeType("Error", smithyEventStream, "smithy_eventstream::error"), "Header" to RuntimeType("Header", smithyEventStream, "smithy_eventstream::frame"), "HeaderValue" to RuntimeType("HeaderValue", smithyEventStream, "smithy_eventstream::frame"), - "Error" to RuntimeType("Error", smithyEventStream, "smithy_eventstream::error"), "Inlineables" to RuntimeType.eventStreamInlinables(runtimeConfig), - "SmithyError" to RuntimeType("Error", CargoDependency.SmithyTypes(runtimeConfig), "smithy_types") + "Message" to RuntimeType("Message", smithyEventStream, "smithy_eventstream::frame"), + "SmithyError" to RuntimeType("Error", CargoDependency.SmithyTypes(runtimeConfig), "smithy_types"), + "UnmarshallMessage" to RuntimeType("UnmarshallMessage", smithyEventStream, "smithy_eventstream::frame"), + "UnmarshalledMessage" to RuntimeType("UnmarshalledMessage", smithyEventStream, "smithy_eventstream::frame"), ) fun render(): RuntimeType { @@ -118,7 +121,7 @@ class EventStreamUnmarshallerGenerator( private fun RustWriter.renderUnmarshallEvent() { rustBlock("match response_headers.smithy_type.as_str()") { for (member in unionShape.members()) { - val target = model.expectShape(member.target) + val target = model.expectShape(member.target, StructureShape::class.java) if (!target.hasTrait()) { rustBlock("${member.memberName.dq()} => ") { renderUnmarshallUnionMember(member, target) @@ -135,37 +138,72 @@ class EventStreamUnmarshallerGenerator( } } - private fun RustWriter.renderUnmarshallUnionMember(member: MemberShape, target: Shape) { - rustTemplate( - "return Ok(#{UnmarshalledMessage}::Event(#{Output}::${member.memberName.toPascalCase()}(", - "Output" to unionSymbol, - *codegenScope - ) + private fun RustWriter.renderUnmarshallUnionMember(unionMember: MemberShape, unionStruct: StructureShape) { + val unionMemberName = unionMember.memberName.toPascalCase() + val payloadOnly = + unionStruct.members().none { it.hasTrait() || it.hasTrait() } + if (payloadOnly) { + withBlock("let parsed = ", ";") { + renderParseProtocolPayload(unionMember) + } + rustTemplate( + "return Ok(#{UnmarshalledMessage}::Event(#{Output}::$unionMemberName(parsed)));", + "Output" to unionSymbol, + *codegenScope + ) + } else { + rust("let builder = #T::builder();", symbolProvider.toSymbol(unionStruct)) + for (member in unionStruct.members()) { + val target = model.expectShape(member.target) + if (member.hasTrait()) { + // TODO(EventStream): Add `@eventHeader` unmarshalling support + } else if (member.hasTrait()) { + renderUnmarshallEventPayload(member, target) + } + } + rustTemplate( + "return Ok(#{UnmarshalledMessage}::Event(#{Output}::$unionMemberName(builder.build())));", + "Output" to unionSymbol, + *codegenScope + ) + } + } + + private fun RustWriter.renderUnmarshallEventPayload(member: MemberShape, target: Shape) { // TODO(EventStream): [RPC] Don't blow up on an initial-message that's not part of the union (:event-type will be "initial-request" or "initial-response") // TODO(EventStream): [RPC] Incorporate initial-message into original output (:event-type will be "initial-request" or "initial-response") + val memberName = symbolProvider.toMemberName(member) when (target) { is BlobShape -> { - rust("unimplemented!(\"TODO(EventStream): Implement blob unmarshalling\")") + withBlock("let builder = builder.$memberName(", ");") { + rustTemplate("#{Blob}::new(message.payload().as_ref())", *codegenScope) + } } is StringShape -> { - rust("unimplemented!(\"TODO(EventStream): Implement string unmarshalling\")") + rust("// TODO(EventStream): Implement string unmarshalling") } is UnionShape, is StructureShape -> { - // TODO(EventStream): Check :content-type against expected content-type, error if unexpected - val parser = protocol.structuredDataParser(operationShape).payloadParser(member) - rustTemplate( - """ - #{parser}(&message.payload()[..]) - .map_err(|err| { - #{Error}::Unmarshalling(format!("failed to unmarshall ${member.memberName}: {}", err)) - })? - """, - "parser" to parser, - *codegenScope - ) + withBlock("let builder = builder.$memberName(", ");") { + renderParseProtocolPayload(member) + } } } - rust(")))") + } + + private fun RustWriter.renderParseProtocolPayload(member: MemberShape) { + // TODO(EventStream): Check :content-type against expected content-type, error if unexpected + val parser = protocol.structuredDataParser(operationShape).payloadParser(member) + val memberName = member.memberName.toPascalCase() + rustTemplate( + """ + #{parser}(&message.payload()[..]) + .map_err(|err| { + #{Error}::Unmarshalling(format!("failed to unmarshall $memberName: {}", err)) + })? + """, + "parser" to parser, + *codegenScope + ) } private fun RustWriter.renderUnmarshallError() { diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/EventStreamTestTools.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/EventStreamTestTools.kt new file mode 100644 index 0000000000..2fc3a295ee --- /dev/null +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/EventStreamTestTools.kt @@ -0,0 +1,192 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package software.amazon.smithy.rust.codegen.smithy.protocols + +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.ArgumentsProvider +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.ServiceShape +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.model.shapes.StructureShape +import software.amazon.smithy.model.shapes.UnionShape +import software.amazon.smithy.model.traits.ErrorTrait +import software.amazon.smithy.rust.codegen.rustlang.RustModule +import software.amazon.smithy.rust.codegen.rustlang.RustWriter +import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider +import software.amazon.smithy.rust.codegen.smithy.generators.BuilderGenerator +import software.amazon.smithy.rust.codegen.smithy.generators.ProtocolConfig +import software.amazon.smithy.rust.codegen.smithy.generators.StructureGenerator +import software.amazon.smithy.rust.codegen.smithy.generators.UnionGenerator +import software.amazon.smithy.rust.codegen.smithy.generators.error.CombinedErrorGenerator +import software.amazon.smithy.rust.codegen.smithy.generators.implBlock +import software.amazon.smithy.rust.codegen.smithy.transformers.OperationNormalizer +import software.amazon.smithy.rust.codegen.testutil.TestWorkspace +import software.amazon.smithy.rust.codegen.testutil.TestWriterDelegator +import software.amazon.smithy.rust.codegen.testutil.asSmithyModel +import software.amazon.smithy.rust.codegen.testutil.renderWithModelBuilder +import software.amazon.smithy.rust.codegen.testutil.testSymbolProvider +import software.amazon.smithy.rust.codegen.util.hasTrait +import software.amazon.smithy.rust.codegen.util.lookup +import software.amazon.smithy.rust.codegen.util.outputShape +import java.util.stream.Stream + +private fun fillInBaseModel( + protocolName: String, + extraServiceAnnotations: String = "", +): String = """ + namespace test + + use aws.protocols#$protocolName + + union TestUnion { + Foo: String, + Bar: Integer, + } + structure TestStruct { + someString: String, + someInt: Integer, + } + + @error("client") + structure SomeError { + Message: String, + } + + structure MessageWithBlob { @eventPayload data: Blob } + structure MessageWithString { @eventPayload data: String } + structure MessageWithStruct { @eventPayload someStruct: TestStruct } + structure MessageWithUnion { @eventPayload someUnion: TestUnion } + structure MessageWithHeaders { + @eventHeader blob: Blob, + @eventHeader boolean: Boolean, + @eventHeader byte: Byte, + @eventHeader int: Integer, + @eventHeader long: Long, + @eventHeader short: Short, + @eventHeader string: String, + @eventHeader timestamp: Timestamp, + } + structure MessageWithHeaderAndPayload { + @eventHeader header: String, + @eventPayload payload: Blob, + } + structure MessageWithNoHeaderPayloadTraits { + someInt: Integer, + someString: String, + } + + @streaming + union TestStream { + MessageWithBlob: MessageWithBlob, + MessageWithString: MessageWithString, + MessageWithStruct: MessageWithStruct, + MessageWithUnion: MessageWithUnion, + MessageWithHeaders: MessageWithHeaders, + MessageWithNoHeaderPayloadTraits: MessageWithNoHeaderPayloadTraits, + SomeError: SomeError, + } + structure TestStreamInputOutput { @required value: TestStream } + operation TestStreamOp { + input: TestStreamInputOutput, + output: TestStreamInputOutput, + errors: [SomeError], + } + $extraServiceAnnotations + @$protocolName + service TestService { version: "123", operations: [TestStreamOp] } +""" + +object EventStreamTestModels { + fun restJson1(): Model = fillInBaseModel("restJson1").asSmithyModel() + fun restXml(): Model = fillInBaseModel("restXml").asSmithyModel() + fun awsJson11(): Model = fillInBaseModel("awsJson1_1").asSmithyModel() + fun awsQuery(): Model = fillInBaseModel("awsQuery", "@xmlNamespace(uri: \"https://example.com\")").asSmithyModel() + fun ec2Query(): Model = fillInBaseModel("ec2Query", "@xmlNamespace(uri: \"https://example.com\")").asSmithyModel() + + data class TestCase( + val protocolShapeId: String, + val model: Model, + val protocolBuilder: (ProtocolConfig) -> Protocol, + ) + + class ModelArgumentsProvider : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream = + Stream.of( + Arguments.of(TestCase("aws.protocols#restJson1", restJson1()) { RestJson(it) }), + Arguments.of(TestCase("aws.protocols#restXml", restXml()) { RestXml(it) }), + Arguments.of(TestCase("aws.protocols#awsJson1_1", awsJson11()) { AwsJson(it, AwsJsonVersion.Json11) }), + Arguments.of(TestCase("aws.protocols#awsQuery", awsQuery()) { AwsQueryProtocol(it) }), + Arguments.of(TestCase("aws.protocols#ec2Query", ec2Query()) { Ec2QueryProtocol(it) }), + ) + } +} + +data class TestEventStreamProject( + val model: Model, + val serviceShape: ServiceShape, + val operationShape: OperationShape, + val streamShape: UnionShape, + val symbolProvider: RustSymbolProvider, + val project: TestWriterDelegator, +) + +object EventStreamTestTools { + fun generateTestProject(model: Model): TestEventStreamProject { + val model = OperationNormalizer(model).transformModel() + val serviceShape = model.expectShape(ShapeId.from("test#TestService")) as ServiceShape + val operationShape = model.expectShape(ShapeId.from("test#TestStreamOp")) as OperationShape + val unionShape = model.expectShape(ShapeId.from("test#TestStream")) as UnionShape + + val symbolProvider = testSymbolProvider(model) + val project = TestWorkspace.testProject(symbolProvider) + project.withModule(RustModule.default("error", public = true)) { + CombinedErrorGenerator(model, symbolProvider, operationShape).render(it) + for (shape in model.shapes().filter { shape -> shape.isStructureShape && shape.hasTrait() }) { + StructureGenerator(model, symbolProvider, it, shape as StructureShape).render() + val builderGen = BuilderGenerator(model, symbolProvider, shape) + builderGen.render(it) + it.implBlock(shape, symbolProvider) { + builderGen.renderConvenienceMethod(this) + } + } + } + project.withModule(RustModule.default("model", public = true)) { + val inputOutput = model.lookup("test#TestStreamInputOutput") + recursivelyGenerateModels(model, symbolProvider, inputOutput, it) + } + project.withModule(RustModule.default("output", public = true)) { + operationShape.outputShape(model).renderWithModelBuilder(model, symbolProvider, it) + } + println("file:///${project.baseDir}/src/error.rs") + println("file:///${project.baseDir}/src/event_stream.rs") + println("file:///${project.baseDir}/src/event_stream_serde.rs") + println("file:///${project.baseDir}/src/lib.rs") + println("file:///${project.baseDir}/src/model.rs") + return TestEventStreamProject(model, serviceShape, operationShape, unionShape, symbolProvider, project) + } + + private fun recursivelyGenerateModels( + model: Model, + symbolProvider: RustSymbolProvider, + shape: Shape, + writer: RustWriter + ) { + for (member in shape.members()) { + val target = model.expectShape(member.target) + if (target is StructureShape || target is UnionShape) { + if (target is StructureShape) { + target.renderWithModelBuilder(model, symbolProvider, writer) + } else if (target is UnionShape) { + UnionGenerator(model, symbolProvider, writer, target).render() + } + recursivelyGenerateModels(model, symbolProvider, target, writer) + } + } + } +} diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGeneratorTest.kt new file mode 100644 index 0000000000..8b0eb2fe22 --- /dev/null +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGeneratorTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package software.amazon.smithy.rust.codegen.smithy.protocols.parse + +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ArgumentsSource +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.rust.codegen.smithy.generators.ProtocolConfig +import software.amazon.smithy.rust.codegen.smithy.protocols.EventStreamTestModels +import software.amazon.smithy.rust.codegen.smithy.protocols.EventStreamTestTools +import software.amazon.smithy.rust.codegen.testutil.TestRuntimeConfig +import software.amazon.smithy.rust.codegen.testutil.compileAndTest +import software.amazon.smithy.rust.codegen.testutil.unitTest + +class EventStreamUnmarshallerGeneratorTest { + @ParameterizedTest + @ArgumentsSource(EventStreamTestModels.ModelArgumentsProvider::class) + fun test(testCase: EventStreamTestModels.TestCase) { + val test = EventStreamTestTools.generateTestProject(testCase.model) + + val protocolConfig = ProtocolConfig( + test.model, + test.symbolProvider, + TestRuntimeConfig, + test.serviceShape, + ShapeId.from(testCase.protocolShapeId), + "test" + ) + val protocol = testCase.protocolBuilder(protocolConfig) + val generator = EventStreamUnmarshallerGenerator( + protocol, + test.model, + TestRuntimeConfig, + test.symbolProvider, + test.operationShape, + test.streamShape + ) + + test.project.lib { writer -> + // TODO(EventStream): Add test for MessageWithString + // TODO(EventStream): Add test for MessageWithStruct + // TODO(EventStream): Add test for MessageWithUnion + // TODO(EventStream): Add test for MessageWithHeaders + // TODO(EventStream): Add test for MessageWithHeaderAndPayload + // TODO(EventStream): Add test for MessageWithNoHeaderPayloadTraits + writer.unitTest( + """ + use smithy_eventstream::frame::{Header, HeaderValue, Message, UnmarshallMessage, UnmarshalledMessage}; + use smithy_types::Blob; + use crate::model::*; + + let message = Message::new(&b"hello, world!"[..]) + .add_header(Header::new(":message-type", HeaderValue::String("event".into()))) + .add_header(Header::new(":event-type", HeaderValue::String("MessageWithBlob".into()))) + .add_header(Header::new(":content-type", HeaderValue::String("application/octet-stream".into()))); + let unmarshaller = ${writer.format(generator.render())}(); + let result = unmarshaller.unmarshall(&message).unwrap(); + if let UnmarshalledMessage::Event(event) = result { + assert_eq!( + TestStream::MessageWithBlob( + MessageWithBlob::builder().data(Blob::new(&b"hello, world!"[..])).build() + ), + event + ); + } else { + panic!("Expected event, got error: {:?}", result); + } + """, + "message_with_blob", + ) + } + test.project.compileAndTest() + } +} diff --git a/rust-runtime/smithy-eventstream/src/frame.rs b/rust-runtime/smithy-eventstream/src/frame.rs index 600fbd944f..9bf90355a2 100644 --- a/rust-runtime/smithy-eventstream/src/frame.rs +++ b/rust-runtime/smithy-eventstream/src/frame.rs @@ -37,6 +37,7 @@ pub trait MarshallMessage: fmt::Debug { } /// A successfully unmarshalled message that is either an `Event` or an `Error`. +#[derive(Debug)] pub enum UnmarshalledMessage { Event(T), Error(E), From 982687e3ebadb7f27dc73edc415c3fb7adde50cb Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Wed, 18 Aug 2021 10:06:24 -0700 Subject: [PATCH 09/17] Add event header unmarshalling support --- .../parse/EventStreamUnmarshallerGenerator.kt | 75 +++++-- .../smithy/protocols/EventStreamTestTools.kt | 94 ++++++++- .../EventStreamUnmarshallerGeneratorTest.kt | 186 +++++++++++++++--- rust-runtime/inlineable/src/event_stream.rs | 27 ++- rust-runtime/smithy-eventstream/src/frame.rs | 10 +- 5 files changed, 337 insertions(+), 55 deletions(-) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt index 1dbc318381..bdd4422f3c 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt @@ -8,11 +8,16 @@ package software.amazon.smithy.rust.codegen.smithy.protocols.parse import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.BlobShape +import software.amazon.smithy.model.shapes.BooleanShape +import software.amazon.smithy.model.shapes.ByteShape +import software.amazon.smithy.model.shapes.IntegerShape +import software.amazon.smithy.model.shapes.LongShape import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.OperationShape -import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.ShortShape import software.amazon.smithy.model.shapes.StringShape import software.amazon.smithy.model.shapes.StructureShape +import software.amazon.smithy.model.shapes.TimestampShape import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.model.traits.ErrorTrait import software.amazon.smithy.model.traits.EventHeaderTrait @@ -152,13 +157,22 @@ class EventStreamUnmarshallerGenerator( *codegenScope ) } else { - rust("let builder = #T::builder();", symbolProvider.toSymbol(unionStruct)) - for (member in unionStruct.members()) { - val target = model.expectShape(member.target) - if (member.hasTrait()) { - // TODO(EventStream): Add `@eventHeader` unmarshalling support - } else if (member.hasTrait()) { - renderUnmarshallEventPayload(member, target) + rust("let mut builder = #T::builder();", symbolProvider.toSymbol(unionStruct)) + val payloadMember = unionStruct.members().firstOrNull { it.hasTrait() } + if (payloadMember != null) { + renderUnmarshallEventPayload(payloadMember) + } + val headerMembers = unionStruct.members().filter { it.hasTrait() } + if (headerMembers.isNotEmpty()) { + rustBlock("for header in message.headers()") { + rustBlock("match header.name().as_str()") { + for (member in headerMembers) { + rustBlock("${member.memberName.dq()} => ") { + renderUnmarshallEventHeader(member) + } + } + rust("_ => {}") + } } } rustTemplate( @@ -169,21 +183,42 @@ class EventStreamUnmarshallerGenerator( } } - private fun RustWriter.renderUnmarshallEventPayload(member: MemberShape, target: Shape) { + private fun RustWriter.renderUnmarshallEventHeader(member: MemberShape) { + val memberName = symbolProvider.toMemberName(member) + withBlock("builder = builder.$memberName(", ");") { + when (val target = model.expectShape(member.target)) { + is BooleanShape -> rustTemplate("#{Inlineables}::expect_bool(header)?", *codegenScope) + is ByteShape -> rustTemplate("#{Inlineables}::expect_byte(header)?", *codegenScope) + is ShortShape -> rustTemplate("#{Inlineables}::expect_int16(header)?", *codegenScope) + is IntegerShape -> rustTemplate("#{Inlineables}::expect_int32(header)?", *codegenScope) + is LongShape -> rustTemplate("#{Inlineables}::expect_int64(header)?", *codegenScope) + is BlobShape -> rustTemplate("#{Inlineables}::expect_byte_array(header)?", *codegenScope) + is StringShape -> rustTemplate("#{Inlineables}::expect_string(header)?", *codegenScope) + is TimestampShape -> rustTemplate("#{Inlineables}::expect_timestamp(header)?", *codegenScope) + else -> throw IllegalStateException("unsupported event stream header shape type: $target") + } + } + } + + private fun RustWriter.renderUnmarshallEventPayload(member: MemberShape) { // TODO(EventStream): [RPC] Don't blow up on an initial-message that's not part of the union (:event-type will be "initial-request" or "initial-response") // TODO(EventStream): [RPC] Incorporate initial-message into original output (:event-type will be "initial-request" or "initial-response") val memberName = symbolProvider.toMemberName(member) - when (target) { - is BlobShape -> { - withBlock("let builder = builder.$memberName(", ");") { + withBlock("builder = builder.$memberName(", ");") { + when (model.expectShape(member.target)) { + is BlobShape -> { rustTemplate("#{Blob}::new(message.payload().as_ref())", *codegenScope) } - } - is StringShape -> { - rust("// TODO(EventStream): Implement string unmarshalling") - } - is UnionShape, is StructureShape -> { - withBlock("let builder = builder.$memberName(", ");") { + is StringShape -> { + rustTemplate( + """ + std::str::from_utf8(message.payload()) + .map_err(|_| #{Error}::Unmarshalling("message payload is not valid UTF-8".into()))? + """, + *codegenScope + ) + } + is UnionShape, is StructureShape -> { renderParseProtocolPayload(member) } } @@ -214,10 +249,10 @@ class EventStreamUnmarshallerGenerator( rustBlock("${member.memberName.dq()} => ") { val parser = protocol.structuredDataParser(operationShape).errorParser(target) if (parser != null) { - rust("let builder = #T::builder();", symbolProvider.toSymbol(target)) + rust("let mut builder = #T::builder();", symbolProvider.toSymbol(target)) rustTemplate( """ - let builder = #{parser}(&message.payload()[..], builder) + builder = #{parser}(&message.payload()[..], builder) .map_err(|err| { #{Error}::Unmarshalling(format!("failed to unmarshall ${member.memberName}: {}", err)) })?; diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/EventStreamTestTools.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/EventStreamTestTools.kt index 2fc3a295ee..ba93470caf 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/EventStreamTestTools.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/EventStreamTestTools.kt @@ -88,6 +88,7 @@ private fun fillInBaseModel( MessageWithStruct: MessageWithStruct, MessageWithUnion: MessageWithUnion, MessageWithHeaders: MessageWithHeaders, + MessageWithHeaderAndPayload: MessageWithHeaderAndPayload, MessageWithNoHeaderPayloadTraits: MessageWithNoHeaderPayloadTraits, SomeError: SomeError, } @@ -112,17 +113,98 @@ object EventStreamTestModels { data class TestCase( val protocolShapeId: String, val model: Model, + val contentType: String, + val validTestStruct: String, + val validMessageWithNoHeaderPayloadTraits: String, + val validTestUnion: String, val protocolBuilder: (ProtocolConfig) -> Protocol, - ) + ) { + override fun toString(): String = protocolShapeId + } class ModelArgumentsProvider : ArgumentsProvider { override fun provideArguments(context: ExtensionContext?): Stream = Stream.of( - Arguments.of(TestCase("aws.protocols#restJson1", restJson1()) { RestJson(it) }), - Arguments.of(TestCase("aws.protocols#restXml", restXml()) { RestXml(it) }), - Arguments.of(TestCase("aws.protocols#awsJson1_1", awsJson11()) { AwsJson(it, AwsJsonVersion.Json11) }), - Arguments.of(TestCase("aws.protocols#awsQuery", awsQuery()) { AwsQueryProtocol(it) }), - Arguments.of(TestCase("aws.protocols#ec2Query", ec2Query()) { Ec2QueryProtocol(it) }), + Arguments.of( + TestCase( + protocolShapeId = "aws.protocols#restJson1", + model = restJson1(), + contentType = "application/json", + validTestStruct = """{"someString":"hello","someInt":5}""", + validMessageWithNoHeaderPayloadTraits = """{"someString":"hello","someInt":5}""", + validTestUnion = """{"Foo":"hello"}""", + ) { RestJson(it) } + ), + Arguments.of( + TestCase( + protocolShapeId = "aws.protocols#awsJson1_1", + model = awsJson11(), + contentType = "application/x-amz-json-1.1", + validTestStruct = """{"someString":"hello","someInt":5}""", + validMessageWithNoHeaderPayloadTraits = """{"someString":"hello","someInt":5}""", + validTestUnion = """{"Foo":"hello"}""", + ) { AwsJson(it, AwsJsonVersion.Json11) } + ), + Arguments.of( + TestCase( + protocolShapeId = "aws.protocols#restXml", + model = restXml(), + contentType = "text/xml", + validTestStruct = """ + + hello + 5 + + """.trimIndent(), + validMessageWithNoHeaderPayloadTraits = """ + + hello + 5 + + """.trimIndent(), + validTestUnion = "hello" + ) { RestXml(it) } + ), + Arguments.of( + TestCase( + protocolShapeId = "aws.protocols#awsQuery", + model = awsQuery(), + contentType = "application/x-www-form-urlencoded", + validTestStruct = """ + + hello + 5 + + """.trimIndent(), + validMessageWithNoHeaderPayloadTraits = """ + + hello + 5 + + """.trimIndent(), + validTestUnion = "hello" + ) { AwsQueryProtocol(it) } + ), + Arguments.of( + TestCase( + protocolShapeId = "aws.protocols#ec2Query", + model = ec2Query(), + contentType = "application/x-www-form-urlencoded", + validTestStruct = """ + + hello + 5 + + """.trimIndent(), + validMessageWithNoHeaderPayloadTraits = """ + + hello + 5 + + """.trimIndent(), + validTestUnion = "hello" + ) { Ec2QueryProtocol(it) } + ), ) } } diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGeneratorTest.kt index 8b0eb2fe22..db05f055df 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGeneratorTest.kt @@ -8,6 +8,7 @@ package software.amazon.smithy.rust.codegen.smithy.protocols.parse import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ArgumentsSource import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.rust.codegen.rustlang.rust import software.amazon.smithy.rust.codegen.smithy.generators.ProtocolConfig import software.amazon.smithy.rust.codegen.smithy.protocols.EventStreamTestModels import software.amazon.smithy.rust.codegen.smithy.protocols.EventStreamTestTools @@ -40,37 +41,176 @@ class EventStreamUnmarshallerGeneratorTest { ) test.project.lib { writer -> - // TODO(EventStream): Add test for MessageWithString - // TODO(EventStream): Add test for MessageWithStruct - // TODO(EventStream): Add test for MessageWithUnion - // TODO(EventStream): Add test for MessageWithHeaders - // TODO(EventStream): Add test for MessageWithHeaderAndPayload - // TODO(EventStream): Add test for MessageWithNoHeaderPayloadTraits - writer.unitTest( + // TODO(EventStream): Add test for bad content type + // TODO(EventStream): Add test for modeled error parsing + // TODO(EventStream): Add test for generic error parsing + writer.rust( """ use smithy_eventstream::frame::{Header, HeaderValue, Message, UnmarshallMessage, UnmarshalledMessage}; - use smithy_types::Blob; + use smithy_types::{Blob, Instant}; use crate::model::*; - let message = Message::new(&b"hello, world!"[..]) - .add_header(Header::new(":message-type", HeaderValue::String("event".into()))) - .add_header(Header::new(":event-type", HeaderValue::String("MessageWithBlob".into()))) - .add_header(Header::new(":content-type", HeaderValue::String("application/octet-stream".into()))); - let unmarshaller = ${writer.format(generator.render())}(); - let result = unmarshaller.unmarshall(&message).unwrap(); - if let UnmarshalledMessage::Event(event) = result { - assert_eq!( - TestStream::MessageWithBlob( - MessageWithBlob::builder().data(Blob::new(&b"hello, world!"[..])).build() - ), - event - ); - } else { - panic!("Expected event, got error: {:?}", result); + fn msg( + message_type: &'static str, + event_type: &'static str, + content_type: &'static str, + payload: &'static [u8], + ) -> Message { + Message::new(payload) + .add_header(Header::new(":message-type", HeaderValue::String(message_type.into()))) + .add_header(Header::new(":event-type", HeaderValue::String(event_type.into()))) + .add_header(Header::new(":content-type", HeaderValue::String(content_type.into()))) + } + fn expect_event(unmarshalled: UnmarshalledMessage) -> T { + match unmarshalled { + UnmarshalledMessage::Event(event) => event, + _ => panic!("expected event, got: {:?}", unmarshalled), + } } + """ + ) + + writer.unitTest( + """ + let message = msg("event", "MessageWithBlob", "application/octet-stream", b"hello, world!"); + let result = ${writer.format(generator.render())}().unmarshall(&message); + assert!(result.is_ok(), "expected ok, got: {:?}", result); + assert_eq!( + TestStream::MessageWithBlob( + MessageWithBlob::builder().data(Blob::new(&b"hello, world!"[..])).build() + ), + expect_event(result.unwrap()) + ); """, "message_with_blob", ) + + writer.unitTest( + """ + let message = msg("event", "MessageWithString", "application/octet-stream", b"hello, world!"); + let result = ${writer.format(generator.render())}().unmarshall(&message); + assert!(result.is_ok(), "expected ok, got: {:?}", result); + assert_eq!( + TestStream::MessageWithString(MessageWithString::builder().data("hello, world!").build()), + expect_event(result.unwrap()) + ); + """, + "message_with_string", + ) + + writer.unitTest( + """ + let message = msg( + "event", + "MessageWithStruct", + "${testCase.contentType}", + br#"${testCase.validTestStruct}"# + ); + let result = ${writer.format(generator.render())}().unmarshall(&message); + assert!(result.is_ok(), "expected ok, got: {:?}", result); + assert_eq!( + TestStream::MessageWithStruct(MessageWithStruct::builder().some_struct( + TestStruct::builder() + .some_string("hello") + .some_int(5) + .build() + ).build()), + expect_event(result.unwrap()) + ); + """, + "message_with_struct", + ) + + writer.unitTest( + """ + let message = msg( + "event", + "MessageWithUnion", + "${testCase.contentType}", + br#"${testCase.validTestUnion}"# + ); + let result = ${writer.format(generator.render())}().unmarshall(&message); + assert!(result.is_ok(), "expected ok, got: {:?}", result); + assert_eq!( + TestStream::MessageWithUnion(MessageWithUnion::builder().some_union( + TestUnion::Foo("hello".into()) + ).build()), + expect_event(result.unwrap()) + ); + """, + "message_with_union", + ) + + writer.unitTest( + """ + let message = msg("event", "MessageWithHeaders", "application/octet-stream", b"") + .add_header(Header::new("blob", HeaderValue::ByteArray((&b"test"[..]).into()))) + .add_header(Header::new("boolean", HeaderValue::Bool(true))) + .add_header(Header::new("byte", HeaderValue::Byte(55i8))) + .add_header(Header::new("int", HeaderValue::Int32(100_000i32))) + .add_header(Header::new("long", HeaderValue::Int64(9_000_000_000i64))) + .add_header(Header::new("short", HeaderValue::Int16(16_000i16))) + .add_header(Header::new("string", HeaderValue::String("test".into()))) + .add_header(Header::new("timestamp", HeaderValue::Timestamp(Instant::from_epoch_seconds(5)))); + let result = ${writer.format(generator.render())}().unmarshall(&message); + assert!(result.is_ok(), "expected ok, got: {:?}", result); + assert_eq!( + TestStream::MessageWithHeaders(MessageWithHeaders::builder() + .blob(Blob::new(&b"test"[..])) + .boolean(true) + .byte(55i8) + .int(100_000i32) + .long(9_000_000_000i64) + .short(16_000i16) + .string("test") + .timestamp(Instant::from_epoch_seconds(5)) + .build() + ), + expect_event(result.unwrap()) + ); + """, + "message_with_headers", + ) + + writer.unitTest( + """ + let message = msg("event", "MessageWithHeaderAndPayload", "application/octet-stream", b"payload") + .add_header(Header::new("header", HeaderValue::String("header".into()))); + let result = ${writer.format(generator.render())}().unmarshall(&message); + assert!(result.is_ok(), "expected ok, got: {:?}", result); + assert_eq!( + TestStream::MessageWithHeaderAndPayload(MessageWithHeaderAndPayload::builder() + .header("header") + .payload(Blob::new(&b"payload"[..])) + .build() + ), + expect_event(result.unwrap()) + ); + """, + "message_with_header_and_payload", + ) + + writer.unitTest( + """ + let message = msg( + "event", + "MessageWithNoHeaderPayloadTraits", + "${testCase.contentType}", + br#"${testCase.validMessageWithNoHeaderPayloadTraits}"# + ); + let result = ${writer.format(generator.render())}().unmarshall(&message); + assert!(result.is_ok(), "expected ok, got: {:?}", result); + assert_eq!( + TestStream::MessageWithNoHeaderPayloadTraits(MessageWithNoHeaderPayloadTraits::builder() + .some_int(5) + .some_string("hello") + .build() + ), + expect_event(result.unwrap()) + ); + """, + "message_with_no_header_payload_traits", + ) } test.project.compileAndTest() } diff --git a/rust-runtime/inlineable/src/event_stream.rs b/rust-runtime/inlineable/src/event_stream.rs index a6003f648a..f2bd964d42 100644 --- a/rust-runtime/inlineable/src/event_stream.rs +++ b/rust-runtime/inlineable/src/event_stream.rs @@ -4,8 +4,9 @@ */ use smithy_eventstream::error::Error; -use smithy_eventstream::frame::{Header, Message}; +use smithy_eventstream::frame::{Header, HeaderValue, Message}; use smithy_eventstream::str_bytes::StrBytes; +use smithy_types::{Blob, Instant}; pub struct ResponseHeaders<'a> { pub content_type: &'a StrBytes, @@ -13,6 +14,30 @@ pub struct ResponseHeaders<'a> { pub smithy_type: &'a StrBytes, } +macro_rules! expect_shape_fn { + (fn $fn_name:ident[$val_typ:ident] -> $result_typ:ident { $val_name:ident -> $val_expr:expr }) => { + pub fn $fn_name(header: &Header) -> Result<$result_typ, Error> { + match header.value() { + HeaderValue::$val_typ($val_name) => Ok($val_expr), + _ => Err(Error::Unmarshalling(format!( + "expected '{}' header value to be {}", + header.name().as_str(), + stringify!($val_typ) + ))), + } + } + }; +} + +expect_shape_fn!(fn expect_bool[Bool] -> bool { value -> *value }); +expect_shape_fn!(fn expect_byte[Byte] -> i8 { value -> *value }); +expect_shape_fn!(fn expect_int16[Int16] -> i16 { value -> *value }); +expect_shape_fn!(fn expect_int32[Int32] -> i32 { value -> *value }); +expect_shape_fn!(fn expect_int64[Int64] -> i64 { value -> *value }); +expect_shape_fn!(fn expect_byte_array[ByteArray] -> Blob { bytes -> Blob::new(bytes.as_ref()) }); +expect_shape_fn!(fn expect_string[String] -> String { value -> value.as_str().into() }); +expect_shape_fn!(fn expect_timestamp[Timestamp] -> Instant { value -> *value }); + fn expect_header_str_value<'a>( header: Option<&'a Header>, name: &str, diff --git a/rust-runtime/smithy-eventstream/src/frame.rs b/rust-runtime/smithy-eventstream/src/frame.rs index 9bf90355a2..82697a8f6b 100644 --- a/rust-runtime/smithy-eventstream/src/frame.rs +++ b/rust-runtime/smithy-eventstream/src/frame.rs @@ -81,7 +81,7 @@ mod value { #[derive(Clone, Debug, PartialEq)] pub enum HeaderValue { Bool(bool), - Byte(u8), + Byte(i8), Int16(i16), Int32(i32), Int64(i64), @@ -99,7 +99,7 @@ mod value { } } - pub fn as_byte(&self) -> Result { + pub fn as_byte(&self) -> Result { match self { HeaderValue::Byte(value) => Ok(*value), _ => Err(self), @@ -172,7 +172,7 @@ mod value { match value_type { TYPE_TRUE => Ok(HeaderValue::Bool(true)), TYPE_FALSE => Ok(HeaderValue::Bool(false)), - TYPE_BYTE => read_value!(buffer, Byte, u8, get_u8), + TYPE_BYTE => read_value!(buffer, Byte, i8, get_i8), TYPE_INT16 => read_value!(buffer, Int16, i16, get_i16), TYPE_INT32 => read_value!(buffer, Int32, i32, get_i32), TYPE_INT64 => read_value!(buffer, Int64, i64, get_i64), @@ -215,7 +215,7 @@ mod value { Bool(val) => buffer.put_u8(if *val { TYPE_TRUE } else { TYPE_FALSE }), Byte(val) => { buffer.put_u8(TYPE_BYTE); - buffer.put_u8(*val); + buffer.put_i8(*val); } Int16(val) => { buffer.put_u8(TYPE_INT16); @@ -262,7 +262,7 @@ mod value { Ok(match value_type { TYPE_TRUE => HeaderValue::Bool(true), TYPE_FALSE => HeaderValue::Bool(false), - TYPE_BYTE => HeaderValue::Byte(u8::arbitrary(unstruct)?), + TYPE_BYTE => HeaderValue::Byte(i8::arbitrary(unstruct)?), TYPE_INT16 => HeaderValue::Int16(i16::arbitrary(unstruct)?), TYPE_INT32 => HeaderValue::Int32(i32::arbitrary(unstruct)?), TYPE_INT64 => HeaderValue::Int64(i64::arbitrary(unstruct)?), From a18429c93e6140803ed2108b6a8ee7d52bfe1d19 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Wed, 18 Aug 2021 14:08:57 -0700 Subject: [PATCH 10/17] Don't pull in event stream dependencies by default --- aws/rust-runtime/aws-sig-auth/Cargo.toml | 4 +- aws/rust-runtime/aws-sigv4/Cargo.toml | 2 +- .../smithy/rustsdk/AwsRuntimeDependency.kt | 2 +- .../rustsdk/IntegrationTestDependencies.kt | 7 ++-- .../rust/codegen/rustlang/CargoDependency.kt | 7 +++- .../rust/codegen/smithy/CodegenDelegator.kt | 27 ++++++++++++- .../smithy/EventStreamSymbolProvider.kt | 2 +- .../generators/HttpProtocolTestGenerator.kt | 2 +- .../codegen/smithy/CodegenDelegatorTest.kt | 38 +++++++++++++++++++ rust-runtime/smithy-http/Cargo.toml | 2 +- 10 files changed, 79 insertions(+), 14 deletions(-) create mode 100644 codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenDelegatorTest.kt diff --git a/aws/rust-runtime/aws-sig-auth/Cargo.toml b/aws/rust-runtime/aws-sig-auth/Cargo.toml index 208206a065..03893d4a5f 100644 --- a/aws/rust-runtime/aws-sig-auth/Cargo.toml +++ b/aws/rust-runtime/aws-sig-auth/Cargo.toml @@ -8,8 +8,8 @@ license = "Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -sign-eventstream = ["smithy-eventstream"] -default = ["sign-eventstream"] +sign-eventstream = ["smithy-eventstream", "aws-sigv4/sign-eventstream"] +default = [] [dependencies] http = "0.2.2" diff --git a/aws/rust-runtime/aws-sigv4/Cargo.toml b/aws/rust-runtime/aws-sigv4/Cargo.toml index f14630c821..feaaa01b6f 100644 --- a/aws/rust-runtime/aws-sigv4/Cargo.toml +++ b/aws/rust-runtime/aws-sigv4/Cargo.toml @@ -10,7 +10,7 @@ description = "AWS SigV4 signer" [features] sign-http = ["http", "http-body", "percent-encoding", "form_urlencoded"] sign-eventstream = ["smithy-eventstream"] -default = ["sign-http", "sign-eventstream"] +default = ["sign-http"] [dependencies] chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsRuntimeDependency.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsRuntimeDependency.kt index 4a2c40c229..d0104c8159 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsRuntimeDependency.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsRuntimeDependency.kt @@ -32,5 +32,5 @@ object AwsRuntimeType { val S3Errors by lazy { RuntimeType.forInlineDependency(InlineAwsDependency.forRustFile("s3_errors")) } } -fun RuntimeConfig.awsRuntimeDependency(name: String, features: List = listOf()): CargoDependency = +fun RuntimeConfig.awsRuntimeDependency(name: String, features: Set = setOf()): CargoDependency = CargoDependency(name, awsRoot().crateLocation(), features = features) diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/IntegrationTestDependencies.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/IntegrationTestDependencies.kt index cb03b3078e..3fc606086e 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/IntegrationTestDependencies.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/IntegrationTestDependencies.kt @@ -49,7 +49,8 @@ class IntegrationTestDependencies( override fun section(section: LibRsSection) = when (section) { LibRsSection.Body -> writable { if (hasTests) { - val smithyClient = CargoDependency.SmithyClient(runtimeConfig).copy(features = listOf("test-util"), scope = DependencyScope.Dev) + val smithyClient = CargoDependency.SmithyClient(runtimeConfig) + .copy(features = setOf("test-util"), scope = DependencyScope.Dev) addDependency(smithyClient) addDependency(SerdeJson) addDependency(Tokio) @@ -63,5 +64,5 @@ class IntegrationTestDependencies( } val Criterion = CargoDependency("criterion", CratesIo("0.3"), scope = DependencyScope.Dev) -val SerdeJson = CargoDependency("serde_json", CratesIo("1"), features = emptyList(), scope = DependencyScope.Dev) -val Tokio = CargoDependency("tokio", CratesIo("1"), features = listOf("macros", "test-util"), scope = DependencyScope.Dev) +val SerdeJson = CargoDependency("serde_json", CratesIo("1"), features = emptySet(), scope = DependencyScope.Dev) +val Tokio = CargoDependency("tokio", CratesIo("1"), features = setOf("macros", "test-util"), scope = DependencyScope.Dev) 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 7b1d56f134..3d7c79821c 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 @@ -121,13 +121,16 @@ data class CargoDependency( private val location: DependencyLocation, val scope: DependencyScope = DependencyScope.Compile, val optional: Boolean = false, - private val features: List = listOf() + val features: Set = emptySet() ) : RustDependency(name) { fun withFeature(feature: String): CargoDependency { - return copy(features = features.toMutableList().apply { add(feature) }) + return copy(features = features.toMutableSet().apply { add(feature) }) } + fun canMergeWith(other: CargoDependency): Boolean = + name == other.name && location == other.location && scope == other.scope + override fun version(): String = when (location) { is CratesIo -> location.version is Local -> "local" diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenDelegator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenDelegator.kt index 9918a9f4d1..e56253be02 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenDelegator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenDelegator.kt @@ -101,8 +101,10 @@ fun CodegenWriterDelegator.finalize( this.useFileWriter("src/lib.rs", "crate::lib") { writer -> LibRsGenerator(settings.moduleDescription, modules, libRsCustomizations).render(writer) } - val cargoDependencies = - this.dependencies.map { RustDependency.fromSymbolDependency(it) }.filterIsInstance().distinct() + val cargoDependencies = mergeDependencyFeatures( + this.dependencies.map { RustDependency.fromSymbolDependency(it) } + .filterIsInstance().distinct() + ) this.useFileWriter("Cargo.toml") { val cargoToml = CargoTomlGenerator( settings, @@ -114,3 +116,24 @@ fun CodegenWriterDelegator.finalize( } flushWriters() } + +internal fun mergeDependencyFeatures(cargoDependencies: List): List { + val dependencies = cargoDependencies.toMutableList() + dependencies.sortBy { it.name } + + var index = 1 + while (index < dependencies.size) { + val first = dependencies[index - 1] + val second = dependencies[index] + if (first.canMergeWith(second)) { + dependencies[index - 1] = first.copy( + features = first.features + second.features, + optional = first.optional && second.optional + ) + dependencies.removeAt(index) + } else { + index += 1 + } + } + return dependencies +} diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/EventStreamSymbolProvider.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/EventStreamSymbolProvider.kt index 86e9a8bc91..9d4497f0f6 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/EventStreamSymbolProvider.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/EventStreamSymbolProvider.kt @@ -55,7 +55,7 @@ class EventStreamSymbolProvider( .rustType(rustType) .addReference(error) .addReference(initial) - .addDependency(CargoDependency.SmithyHttp(runtimeConfig)) + .addDependency(CargoDependency.SmithyHttp(runtimeConfig).withFeature("event-stream")) .build() } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolTestGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolTestGenerator.kt index 06e2b27e93..dbadc8eb8e 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolTestGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolTestGenerator.kt @@ -146,7 +146,7 @@ class HttpProtocolTestGenerator( val Tokio = CargoDependency( "tokio", CratesIo("1"), - features = listOf("macros", "test-util", "rt"), + features = setOf("macros", "test-util", "rt"), scope = DependencyScope.Dev ) testModuleWriter.addDependency(Tokio) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenDelegatorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenDelegatorTest.kt new file mode 100644 index 0000000000..6db34c5e7f --- /dev/null +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenDelegatorTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package software.amazon.smithy.rust.codegen.smithy + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test +import software.amazon.smithy.rust.codegen.rustlang.CargoDependency +import software.amazon.smithy.rust.codegen.rustlang.CratesIo +import software.amazon.smithy.rust.codegen.rustlang.DependencyScope.Compile + +class CodegenDelegatorTest { + @Test + fun testMergeDependencyFeatures() { + val merged = mergeDependencyFeatures( + listOf( + CargoDependency("A", CratesIo("1"), Compile, optional = false, features = setOf()), + CargoDependency("A", CratesIo("1"), Compile, optional = false, features = setOf("f1")), + CargoDependency("A", CratesIo("1"), Compile, optional = false, features = setOf("f2")), + CargoDependency("A", CratesIo("1"), Compile, optional = false, features = setOf("f1", "f2")), + + CargoDependency("B", CratesIo("2"), Compile, optional = false, features = setOf()), + CargoDependency("B", CratesIo("2"), Compile, optional = true, features = setOf()), + + CargoDependency("C", CratesIo("3"), Compile, optional = true, features = setOf()), + CargoDependency("C", CratesIo("3"), Compile, optional = true, features = setOf()), + ).shuffled() + ) + + merged shouldBe setOf( + CargoDependency("A", CratesIo("1"), Compile, optional = false, features = setOf("f1", "f2")), + CargoDependency("B", CratesIo("2"), Compile, optional = false, features = setOf()), + CargoDependency("C", CratesIo("3"), Compile, optional = true, features = setOf()), + ) + } +} diff --git a/rust-runtime/smithy-http/Cargo.toml b/rust-runtime/smithy-http/Cargo.toml index 752bc022a5..2c17afe1dd 100644 --- a/rust-runtime/smithy-http/Cargo.toml +++ b/rust-runtime/smithy-http/Cargo.toml @@ -8,7 +8,7 @@ license = "Apache-2.0" [features] bytestream-util = ["tokio/fs", "tokio-util/io"] event-stream = ["smithy-eventstream"] -default = ["bytestream-util", "event-stream"] +default = ["bytestream-util"] [dependencies] smithy-types = { path = "../smithy-types" } From f8b7954e1f8da754889cb929bd973e5563bb6088 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Wed, 18 Aug 2021 14:35:53 -0700 Subject: [PATCH 11/17] Only add event stream signer to config for services that need it --- .../smithy/rustsdk/SigV4SigningDecorator.kt | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4SigningDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4SigningDecorator.kt index 2b254628b8..c251045842 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4SigningDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4SigningDecorator.kt @@ -31,6 +31,7 @@ import software.amazon.smithy.rust.codegen.smithy.letIf import software.amazon.smithy.rust.codegen.util.dq import software.amazon.smithy.rust.codegen.util.expectTrait import software.amazon.smithy.rust.codegen.util.hasTrait +import software.amazon.smithy.rust.codegen.util.isEventStream import software.amazon.smithy.rust.codegen.util.isInputEventStream /** @@ -51,8 +52,17 @@ class SigV4SigningDecorator : RustCodegenDecorator { protocolConfig: ProtocolConfig, baseCustomizations: List ): List { - return baseCustomizations.letIf(applies(protocolConfig)) { - it + SigV4SigningConfig(protocolConfig.runtimeConfig, protocolConfig.serviceShape.expectTrait()) + return baseCustomizations.letIf(applies(protocolConfig)) { customizations -> + val serviceHasEventStream = protocolConfig.serviceShape.operations + .any { id -> + protocolConfig.model.expectShape(id, OperationShape::class.java) + .isEventStream(protocolConfig.model) + } + customizations + SigV4SigningConfig( + protocolConfig.runtimeConfig, + serviceHasEventStream, + protocolConfig.serviceShape.expectTrait() + ) } } @@ -72,11 +82,15 @@ class SigV4SigningDecorator : RustCodegenDecorator { } } -class SigV4SigningConfig(runtimeConfig: RuntimeConfig, private val sigV4Trait: SigV4Trait) : ConfigCustomization() { +class SigV4SigningConfig( + runtimeConfig: RuntimeConfig, + private val serviceHasEventStream: Boolean, + private val sigV4Trait: SigV4Trait +) : ConfigCustomization() { private val codegenScope = arrayOf( "SigV4Signer" to RuntimeType( "SigV4Signer", - runtimeConfig.awsRuntimeDependency("aws-sig-auth", listOf("sign-eventstream")), + runtimeConfig.awsRuntimeDependency("aws-sig-auth", setOf("sign-eventstream")), "aws_sig_auth::event_stream" ), "PropertyBag" to RuntimeType( @@ -98,14 +112,23 @@ class SigV4SigningConfig(runtimeConfig: RuntimeConfig, private val sigV4Trait: S pub fn signing_service(&self) -> &'static str { ${sigV4Trait.name.dq()} } - - /// Creates a new Event Stream `SignMessage` implementor. - pub fn new_event_stream_signer(&self, properties: std::sync::Arc>) -> #{SigV4Signer} { - #{SigV4Signer}::new(properties) - } """, *codegenScope ) + if (serviceHasEventStream) { + rustTemplate( + """ + /// Creates a new Event Stream `SignMessage` implementor. + pub fn new_event_stream_signer( + &self, + properties: std::sync::Arc> + ) -> #{SigV4Signer} { + #{SigV4Signer}::new(properties) + } + """, + *codegenScope + ) + } } else -> emptySection } From 16aeadee1e885cd22bf8282b66375b9a61acde65 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Wed, 18 Aug 2021 14:52:08 -0700 Subject: [PATCH 12/17] Move event stream inlineables into smithy-eventstream --- .../rust/codegen/smithy/RuntimeTypes.kt | 3 --- .../smithy/generators/BuilderGenerator.kt | 2 +- .../http/ResponseBindingGenerator.kt | 4 ++-- .../parse/EventStreamUnmarshallerGenerator.kt | 20 ++++++++--------- rust-runtime/inlineable/Cargo.toml | 1 - rust-runtime/smithy-eventstream/src/lib.rs | 1 + .../src/smithy.rs} | 22 +++++++++---------- 7 files changed, 25 insertions(+), 28 deletions(-) rename rust-runtime/{inlineable/src/event_stream.rs => smithy-eventstream/src/smithy.rs} (96%) 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 cf6ba944ab..5276ee0254 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 @@ -170,9 +170,6 @@ data class RuntimeType(val name: String?, val dependency: RustDependency?, val n "smithy_http::event_stream" ) - fun eventStreamInlinables(runtimeConfig: RuntimeConfig): RuntimeType = - forInlineDependency(InlineDependency.eventStream(runtimeConfig)) - fun jsonErrors(runtimeConfig: RuntimeConfig) = forInlineDependency(InlineDependency.jsonErrors(runtimeConfig)) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/BuilderGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/BuilderGenerator.kt index 21d23c4bb8..94ef6e12de 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/BuilderGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/BuilderGenerator.kt @@ -106,7 +106,7 @@ class BuilderGenerator( } } - // TODO(EventStream): [DX] Update builders to take EventInputStream as Into + // TODO(EventStream): [DX] Consider updating builders to take EventInputStream as Into private fun renderBuilderMember(writer: RustWriter, member: MemberShape, memberName: String, memberSymbol: Symbol) { // builder members are crate-public to enable using them // directly in serializers/deserializers diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/ResponseBindingGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/ResponseBindingGenerator.kt index 91606f8010..9521dc1317 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/ResponseBindingGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/ResponseBindingGenerator.kt @@ -151,8 +151,8 @@ class ResponseBindingGenerator( ) { // Streaming unions are Event Streams and should be handled separately val target = model.expectShape(binding.member.target) - if (target.isUnionShape) { - bindEventStreamOutput(operationShape, target as UnionShape) + if (target is UnionShape) { + bindEventStreamOutput(operationShape, target) } else { deserializeStreamingBody(binding) } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt index bdd4422f3c..9944fd69ae 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt @@ -54,7 +54,7 @@ class EventStreamUnmarshallerGenerator( "Error" to RuntimeType("Error", smithyEventStream, "smithy_eventstream::error"), "Header" to RuntimeType("Header", smithyEventStream, "smithy_eventstream::frame"), "HeaderValue" to RuntimeType("HeaderValue", smithyEventStream, "smithy_eventstream::frame"), - "Inlineables" to RuntimeType.eventStreamInlinables(runtimeConfig), + "ExpectFns" to RuntimeType("smithy", smithyEventStream, "smithy_eventstream"), "Message" to RuntimeType("Message", smithyEventStream, "smithy_eventstream::frame"), "SmithyError" to RuntimeType("Error", CargoDependency.SmithyTypes(runtimeConfig), "smithy_types"), "UnmarshallMessage" to RuntimeType("UnmarshallMessage", smithyEventStream, "smithy_eventstream::frame"), @@ -101,7 +101,7 @@ class EventStreamUnmarshallerGenerator( ) { rustBlockTemplate( """ - let response_headers = #{Inlineables}::parse_response_headers(&message)?; + let response_headers = #{ExpectFns}::parse_response_headers(&message)?; match response_headers.message_type.as_str() """, *codegenScope @@ -187,14 +187,14 @@ class EventStreamUnmarshallerGenerator( val memberName = symbolProvider.toMemberName(member) withBlock("builder = builder.$memberName(", ");") { when (val target = model.expectShape(member.target)) { - is BooleanShape -> rustTemplate("#{Inlineables}::expect_bool(header)?", *codegenScope) - is ByteShape -> rustTemplate("#{Inlineables}::expect_byte(header)?", *codegenScope) - is ShortShape -> rustTemplate("#{Inlineables}::expect_int16(header)?", *codegenScope) - is IntegerShape -> rustTemplate("#{Inlineables}::expect_int32(header)?", *codegenScope) - is LongShape -> rustTemplate("#{Inlineables}::expect_int64(header)?", *codegenScope) - is BlobShape -> rustTemplate("#{Inlineables}::expect_byte_array(header)?", *codegenScope) - is StringShape -> rustTemplate("#{Inlineables}::expect_string(header)?", *codegenScope) - is TimestampShape -> rustTemplate("#{Inlineables}::expect_timestamp(header)?", *codegenScope) + is BooleanShape -> rustTemplate("#{ExpectFns}::expect_bool(header)?", *codegenScope) + is ByteShape -> rustTemplate("#{ExpectFns}::expect_byte(header)?", *codegenScope) + is ShortShape -> rustTemplate("#{ExpectFns}::expect_int16(header)?", *codegenScope) + is IntegerShape -> rustTemplate("#{ExpectFns}::expect_int32(header)?", *codegenScope) + is LongShape -> rustTemplate("#{ExpectFns}::expect_int64(header)?", *codegenScope) + is BlobShape -> rustTemplate("#{ExpectFns}::expect_byte_array(header)?", *codegenScope) + is StringShape -> rustTemplate("#{ExpectFns}::expect_string(header)?", *codegenScope) + is TimestampShape -> rustTemplate("#{ExpectFns}::expect_timestamp(header)?", *codegenScope) else -> throw IllegalStateException("unsupported event stream header shape type: $target") } } diff --git a/rust-runtime/inlineable/Cargo.toml b/rust-runtime/inlineable/Cargo.toml index 833b4a6d16..c2823e826d 100644 --- a/rust-runtime/inlineable/Cargo.toml +++ b/rust-runtime/inlineable/Cargo.toml @@ -11,7 +11,6 @@ are to allow this crate to be compilable and testable in isolation, no client co [dependencies] "bytes" = "1" "http" = "0.2.1" -"smithy-eventstream" = { path = "../smithy-eventstream" } "smithy-types" = { path = "../smithy-types" } "smithy-http" = { path = "../smithy-http" } "smithy-json" = { path = "../smithy-json" } diff --git a/rust-runtime/smithy-eventstream/src/lib.rs b/rust-runtime/smithy-eventstream/src/lib.rs index 96bb7fdd0e..7cf7417d2d 100644 --- a/rust-runtime/smithy-eventstream/src/lib.rs +++ b/rust-runtime/smithy-eventstream/src/lib.rs @@ -8,4 +8,5 @@ mod buf; pub mod error; pub mod frame; +pub mod smithy; pub mod str_bytes; diff --git a/rust-runtime/inlineable/src/event_stream.rs b/rust-runtime/smithy-eventstream/src/smithy.rs similarity index 96% rename from rust-runtime/inlineable/src/event_stream.rs rename to rust-runtime/smithy-eventstream/src/smithy.rs index f2bd964d42..72e83f1fdf 100644 --- a/rust-runtime/inlineable/src/event_stream.rs +++ b/rust-runtime/smithy-eventstream/src/smithy.rs @@ -3,17 +3,11 @@ * SPDX-License-Identifier: Apache-2.0. */ -use smithy_eventstream::error::Error; -use smithy_eventstream::frame::{Header, HeaderValue, Message}; -use smithy_eventstream::str_bytes::StrBytes; +use crate::error::Error; +use crate::frame::{Header, HeaderValue, Message}; +use crate::str_bytes::StrBytes; use smithy_types::{Blob, Instant}; -pub struct ResponseHeaders<'a> { - pub content_type: &'a StrBytes, - pub message_type: &'a StrBytes, - pub smithy_type: &'a StrBytes, -} - macro_rules! expect_shape_fn { (fn $fn_name:ident[$val_typ:ident] -> $result_typ:ident { $val_name:ident -> $val_expr:expr }) => { pub fn $fn_name(header: &Header) -> Result<$result_typ, Error> { @@ -38,6 +32,12 @@ expect_shape_fn!(fn expect_byte_array[ByteArray] -> Blob { bytes -> Blob::new(by expect_shape_fn!(fn expect_string[String] -> String { value -> value.as_str().into() }); expect_shape_fn!(fn expect_timestamp[Timestamp] -> Instant { value -> *value }); +pub struct ResponseHeaders<'a> { + pub content_type: &'a StrBytes, + pub message_type: &'a StrBytes, + pub smithy_type: &'a StrBytes, +} + fn expect_header_str_value<'a>( header: Option<&'a Header>, name: &str, @@ -87,8 +87,8 @@ pub fn parse_response_headers(message: &Message) -> Result Date: Wed, 18 Aug 2021 15:18:32 -0700 Subject: [PATCH 13/17] Fix some clippy lints --- .../smithy/customizations/AllowClippyLintsGenerator.kt | 5 ++++- .../smithy/protocols/HttpBoundProtocolGenerator.kt | 2 +- .../parse/EventStreamUnmarshallerGenerator.kt | 4 ++-- .../amazon/smithy/rust/codegen/testutil/Rust.kt | 10 +++++++++- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customizations/AllowClippyLintsGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customizations/AllowClippyLintsGenerator.kt index bb97c75ced..c1bdd101e2 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customizations/AllowClippyLintsGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customizations/AllowClippyLintsGenerator.kt @@ -27,7 +27,10 @@ val ClippyAllowLints = listOf( "should_implement_trait", // protocol tests use silly names like `baz`, don't flag that - "blacklisted_name" + "blacklisted_name", + + // Forcing use of `vec![]` can make codegen harder in some cases + "vec_init_then_push", ) class AllowClippyLints : LibRsCustomization() { 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 ffb9841a64..fdc8dc7d29 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 @@ -513,7 +513,7 @@ class HttpBoundProtocolGenerator( } val err = if (StructureGenerator.fallibleBuilder(outputShape, symbolProvider)) { - ".map_err(|s|${format(errorSymbol)}::unhandled(s))?" + ".map_err(${format(errorSymbol)}::unhandled)?" } else "" rust("output.build()$err") } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt index 9944fd69ae..6b0c30b3c4 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt @@ -152,7 +152,7 @@ class EventStreamUnmarshallerGenerator( renderParseProtocolPayload(unionMember) } rustTemplate( - "return Ok(#{UnmarshalledMessage}::Event(#{Output}::$unionMemberName(parsed)));", + "Ok(#{UnmarshalledMessage}::Event(#{Output}::$unionMemberName(parsed)))", "Output" to unionSymbol, *codegenScope ) @@ -176,7 +176,7 @@ class EventStreamUnmarshallerGenerator( } } rustTemplate( - "return Ok(#{UnmarshalledMessage}::Event(#{Output}::$unionMemberName(builder.build())));", + "Ok(#{UnmarshalledMessage}::Event(#{Output}::$unionMemberName(builder.build())))", "Output" to unionSymbol, *codegenScope ) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/Rust.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/Rust.kt index 34793a3354..36764a8fe0 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/Rust.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/Rust.kt @@ -157,7 +157,12 @@ class TestWriterDelegator(fileManifest: FileManifest, symbolProvider: RustSymbol val baseDir: Path = fileManifest.baseDir } -fun TestWriterDelegator.compileAndTest() { +/** + * Setting `runClippy` to true can be helpful when debugging clippy failures, but + * should generally be set to false to avoid invalidating the Cargo cache between + * every unit test run. + */ +fun TestWriterDelegator.compileAndTest(runClippy: Boolean = false) { val stubModel = """ namespace fake service Fake { @@ -183,6 +188,9 @@ fun TestWriterDelegator.compileAndTest() { // cargo fmt errors are useless, ignore } "cargo test".runCommand(baseDir, mapOf("RUSTFLAGS" to "-A dead_code")) + if (runClippy) { + "cargo clippy".runCommand(baseDir) + } } // TODO: unify these test helpers a bit From 7045bccc7ff3fdd362dd602142140fbe44b1775d Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Wed, 18 Aug 2021 16:56:23 -0700 Subject: [PATCH 14/17] Transform event stream unions --- .../rustsdk/SigV4SigningCustomizationTest.kt | 1 + .../rust/codegen/smithy/CodegenVisitor.kt | 6 ++ .../smithy/generators/UnionGenerator.kt | 11 +--- .../rust/codegen/smithy/protocols/AwsJson.kt | 7 +-- .../rust/codegen/smithy/protocols/AwsQuery.kt | 6 +- .../rust/codegen/smithy/protocols/Ec2Query.kt | 6 +- .../rust/codegen/smithy/protocols/RestJson.kt | 6 +- .../rust/codegen/smithy/protocols/RestXml.kt | 6 +- .../parse/EventStreamUnmarshallerGenerator.kt | 22 ++++--- .../traits/SyntheticEventStreamUnionTrait.kt | 19 ++++++ .../transformers/EventStreamNormalizer.kt | 42 +++++++++++++ .../transformers/OperationNormalizer.kt | 23 +++---- .../codegen/generators/UnionGeneratorTest.kt | 24 ------- .../http/RequestBindingGeneratorTest.kt | 2 +- .../http/ResponseBindingGeneratorTest.kt | 2 +- .../smithy/EventStreamSymbolProviderTest.kt | 8 +-- .../StreamingShapeSymbolProviderTest.kt | 4 +- .../HttpProtocolTestGeneratorTest.kt | 6 +- .../smithy/protocols/EventStreamTestTools.kt | 3 +- .../parse/AwsQueryParserGeneratorTest.kt | 2 +- .../parse/Ec2QueryParserGeneratorTest.kt | 2 +- .../parse/JsonParserGeneratorTest.kt | 2 +- .../XmlBindingTraitParserGeneratorTest.kt | 2 +- .../AwsQuerySerializerGeneratorTest.kt | 2 +- .../Ec2QuerySerializerGeneratorTest.kt | 2 +- .../serialize/JsonSerializerGeneratorTest.kt | 2 +- .../XmlBindingTraitSerializerGeneratorTest.kt | 2 +- .../transformers/EventStreamNormalizerTest.kt | 63 +++++++++++++++++++ .../transformers/OperationNormalizerTest.kt | 9 +-- rust-runtime/inlineable/src/lib.rs | 2 - 30 files changed, 180 insertions(+), 114 deletions(-) create mode 100644 codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/traits/SyntheticEventStreamUnionTrait.kt create mode 100644 codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/EventStreamNormalizer.kt create mode 100644 codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/EventStreamNormalizerTest.kt diff --git a/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/SigV4SigningCustomizationTest.kt b/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/SigV4SigningCustomizationTest.kt index bcdba1dd6c..e6b2e039f8 100644 --- a/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/SigV4SigningCustomizationTest.kt +++ b/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/SigV4SigningCustomizationTest.kt @@ -17,6 +17,7 @@ internal class SigV4SigningCustomizationTest { val project = stubConfigProject( SigV4SigningConfig( AwsTestRuntimeConfig, + true, SigV4Trait.builder().name("test-service").build() ) ) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenVisitor.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenVisitor.kt index fa89bab71f..c23cd6ba2a 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenVisitor.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenVisitor.kt @@ -28,7 +28,10 @@ import software.amazon.smithy.rust.codegen.smithy.generators.implBlock import software.amazon.smithy.rust.codegen.smithy.protocols.ProtocolLoader import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticInputTrait import software.amazon.smithy.rust.codegen.smithy.transformers.AddErrorMessage +import software.amazon.smithy.rust.codegen.smithy.transformers.EventStreamNormalizer +import software.amazon.smithy.rust.codegen.smithy.transformers.OperationNormalizer import software.amazon.smithy.rust.codegen.smithy.transformers.RecursiveShapeBoxer +import software.amazon.smithy.rust.codegen.smithy.transformers.RemoveEventStreamOperations import software.amazon.smithy.rust.codegen.util.CommandFailed import software.amazon.smithy.rust.codegen.util.getTrait import software.amazon.smithy.rust.codegen.util.hasTrait @@ -78,6 +81,9 @@ class CodegenVisitor(context: PluginContext, private val codegenDecorator: RustC private fun baselineTransform(model: Model) = model.let(RecursiveShapeBoxer::transform) .letIf(settings.codegenConfig.addMessageToErrors, AddErrorMessage::transform) + .let(OperationNormalizer::transform) + .let(RemoveEventStreamOperations::transform) + .let(EventStreamNormalizer::transform) fun execute() { logger.info("generating Rust client...") diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/UnionGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/UnionGenerator.kt index 269f6c6abc..052bde5cfe 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/UnionGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/UnionGenerator.kt @@ -9,16 +9,12 @@ import software.amazon.smithy.codegen.core.SymbolProvider import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.UnionShape -import software.amazon.smithy.model.traits.ErrorTrait import software.amazon.smithy.rust.codegen.rustlang.Attribute import software.amazon.smithy.rust.codegen.rustlang.RustWriter import software.amazon.smithy.rust.codegen.rustlang.documentShape import software.amazon.smithy.rust.codegen.rustlang.rust import software.amazon.smithy.rust.codegen.rustlang.rustBlock import software.amazon.smithy.rust.codegen.smithy.expectRustMetadata -import software.amazon.smithy.rust.codegen.smithy.letIf -import software.amazon.smithy.rust.codegen.util.hasTrait -import software.amazon.smithy.rust.codegen.util.isEventStream import software.amazon.smithy.rust.codegen.util.toPascalCase import software.amazon.smithy.rust.codegen.util.toSnakeCase @@ -28,12 +24,7 @@ class UnionGenerator( private val writer: RustWriter, private val shape: UnionShape ) { - private val sortedMembers: List = shape.allMembers.values - .sortedBy { symbolProvider.toMemberName(it) } - .letIf(shape.isEventStream()) { members -> - // Filter out all error union members for Event Stream unions since these get handled as actual SdkErrors - members.filter { member -> !model.expectShape(member.target).hasTrait() } - } + private val sortedMembers: List = shape.allMembers.values.sortedBy { symbolProvider.toMemberName(it) } fun render() { renderUnion() 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 8aa4a2a59e..919418eaf8 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 @@ -23,8 +23,6 @@ import software.amazon.smithy.rust.codegen.smithy.protocols.parse.JsonParserGene import software.amazon.smithy.rust.codegen.smithy.protocols.parse.StructuredDataParserGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.JsonSerializerGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.StructuredDataSerializerGenerator -import software.amazon.smithy.rust.codegen.smithy.transformers.OperationNormalizer -import software.amazon.smithy.rust.codegen.smithy.transformers.RemoveEventStreamOperations import software.amazon.smithy.rust.codegen.util.inputShape import software.amazon.smithy.rust.codegen.util.orNull @@ -45,10 +43,7 @@ class AwsJsonFactory(private val version: AwsJsonVersion) : ProtocolGeneratorFac return HttpBoundProtocolGenerator(protocolConfig, AwsJson(protocolConfig, version)) } - override fun transformModel(model: Model): Model { - // For AwsJson10, the body matches 1:1 with the input - return OperationNormalizer(model).transformModel().let(RemoveEventStreamOperations::transform) - } + override fun transformModel(model: Model): Model = model override fun support(): ProtocolSupport = ProtocolSupport( requestSerialization = true, 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 d15c9bd578..0cd45ecfdf 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 @@ -24,17 +24,13 @@ import software.amazon.smithy.rust.codegen.smithy.protocols.parse.AwsQueryParser import software.amazon.smithy.rust.codegen.smithy.protocols.parse.StructuredDataParserGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.AwsQuerySerializerGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.StructuredDataSerializerGenerator -import software.amazon.smithy.rust.codegen.smithy.transformers.OperationNormalizer -import software.amazon.smithy.rust.codegen.smithy.transformers.RemoveEventStreamOperations import software.amazon.smithy.rust.codegen.util.getTrait class AwsQueryFactory : ProtocolGeneratorFactory { override fun buildProtocolGenerator(protocolConfig: ProtocolConfig): HttpBoundProtocolGenerator = HttpBoundProtocolGenerator(protocolConfig, AwsQueryProtocol(protocolConfig)) - override fun transformModel(model: Model): Model { - return OperationNormalizer(model).transformModel().let(RemoveEventStreamOperations::transform) - } + override fun transformModel(model: Model): Model = model override fun support(): ProtocolSupport { return ProtocolSupport( 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 f7cc2764ce..bcb79a5b0a 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 @@ -22,16 +22,12 @@ import software.amazon.smithy.rust.codegen.smithy.protocols.parse.Ec2QueryParser import software.amazon.smithy.rust.codegen.smithy.protocols.parse.StructuredDataParserGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.Ec2QuerySerializerGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.StructuredDataSerializerGenerator -import software.amazon.smithy.rust.codegen.smithy.transformers.OperationNormalizer -import software.amazon.smithy.rust.codegen.smithy.transformers.RemoveEventStreamOperations class Ec2QueryFactory : ProtocolGeneratorFactory { override fun buildProtocolGenerator(protocolConfig: ProtocolConfig): HttpBoundProtocolGenerator = HttpBoundProtocolGenerator(protocolConfig, Ec2QueryProtocol(protocolConfig)) - override fun transformModel(model: Model): Model { - return OperationNormalizer(model).transformModel().let(RemoveEventStreamOperations::transform) - } + override fun transformModel(model: Model): Model = model override fun support(): ProtocolSupport { return ProtocolSupport( 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 b21c510fe5..f576844e68 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 @@ -20,17 +20,13 @@ import software.amazon.smithy.rust.codegen.smithy.protocols.parse.JsonParserGene import software.amazon.smithy.rust.codegen.smithy.protocols.parse.StructuredDataParserGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.JsonSerializerGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.StructuredDataSerializerGenerator -import software.amazon.smithy.rust.codegen.smithy.transformers.OperationNormalizer -import software.amazon.smithy.rust.codegen.smithy.transformers.RemoveEventStreamOperations class RestJsonFactory : ProtocolGeneratorFactory { override fun buildProtocolGenerator( protocolConfig: ProtocolConfig ): HttpBoundProtocolGenerator = HttpBoundProtocolGenerator(protocolConfig, RestJson(protocolConfig)) - override fun transformModel(model: Model): Model { - return OperationNormalizer(model).transformModel().let(RemoveEventStreamOperations::transform) - } + override fun transformModel(model: Model): Model = model override fun support(): ProtocolSupport { return ProtocolSupport( 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 f002fb330d..0e4f451dbc 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 @@ -21,8 +21,6 @@ import software.amazon.smithy.rust.codegen.smithy.protocols.parse.RestXmlParserG import software.amazon.smithy.rust.codegen.smithy.protocols.parse.StructuredDataParserGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.StructuredDataSerializerGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.XmlBindingTraitSerializerGenerator -import software.amazon.smithy.rust.codegen.smithy.transformers.OperationNormalizer -import software.amazon.smithy.rust.codegen.smithy.transformers.RemoveEventStreamOperations import software.amazon.smithy.rust.codegen.util.expectTrait class RestXmlFactory(private val generator: (ProtocolConfig) -> Protocol = { RestXml(it) }) : @@ -33,9 +31,7 @@ class RestXmlFactory(private val generator: (ProtocolConfig) -> Protocol = { Res return HttpBoundProtocolGenerator(protocolConfig, generator(protocolConfig)) } - override fun transformModel(model: Model): Model { - return OperationNormalizer(model).transformModel().let(RemoveEventStreamOperations::transform) - } + override fun transformModel(model: Model): Model = model override fun support(): ProtocolSupport { return ProtocolSupport( diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt index 6b0c30b3c4..f5b68ff2a0 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt @@ -19,7 +19,6 @@ import software.amazon.smithy.model.shapes.StringShape import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.shapes.TimestampShape import software.amazon.smithy.model.shapes.UnionShape -import software.amazon.smithy.model.traits.ErrorTrait import software.amazon.smithy.model.traits.EventHeaderTrait import software.amazon.smithy.model.traits.EventPayloadTrait import software.amazon.smithy.rust.codegen.rustlang.CargoDependency @@ -34,7 +33,9 @@ import software.amazon.smithy.rust.codegen.smithy.RuntimeType import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider import software.amazon.smithy.rust.codegen.smithy.generators.error.errorSymbol import software.amazon.smithy.rust.codegen.smithy.protocols.Protocol +import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticEventStreamUnionTrait import software.amazon.smithy.rust.codegen.util.dq +import software.amazon.smithy.rust.codegen.util.expectTrait import software.amazon.smithy.rust.codegen.util.hasTrait import software.amazon.smithy.rust.codegen.util.toPascalCase @@ -127,10 +128,8 @@ class EventStreamUnmarshallerGenerator( rustBlock("match response_headers.smithy_type.as_str()") { for (member in unionShape.members()) { val target = model.expectShape(member.target, StructureShape::class.java) - if (!target.hasTrait()) { - rustBlock("${member.memberName.dq()} => ") { - renderUnmarshallUnionMember(member, target) - } + rustBlock("${member.memberName.dq()} => ") { + renderUnmarshallUnionMember(member, target) } } rustBlock("smithy_type => ") { @@ -242,14 +241,17 @@ class EventStreamUnmarshallerGenerator( } private fun RustWriter.renderUnmarshallError() { - rustBlock("match response_headers.smithy_type.as_str()") { - for (member in unionShape.members()) { - val target = model.expectShape(member.target) - if (target.hasTrait() && target is StructureShape) { + val syntheticUnion = unionShape.expectTrait() + if (syntheticUnion.errorMembers.isNotEmpty()) { + rustBlock("match response_headers.smithy_type.as_str()") { + for (member in syntheticUnion.errorMembers) { + val target = model.expectShape(member.target, StructureShape::class.java) rustBlock("${member.memberName.dq()} => ") { val parser = protocol.structuredDataParser(operationShape).errorParser(target) if (parser != null) { rust("let mut builder = #T::builder();", symbolProvider.toSymbol(target)) + // TODO(EventStream): Errors on the operation can be disjoint with errors in the union, + // so we need to generate a new top-level Error type for each event stream union. rustTemplate( """ builder = #{parser}(&message.payload()[..], builder) @@ -270,8 +272,8 @@ class EventStreamUnmarshallerGenerator( } } } + rust("_ => {}") } - rust("_ => {}") } // TODO(EventStream): Generic error parsing; will need to refactor `parseGenericError` to // operate on bodies rather than responses. This should be easy for all but restJson, diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/traits/SyntheticEventStreamUnionTrait.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/traits/SyntheticEventStreamUnionTrait.kt new file mode 100644 index 0000000000..f2ed927f07 --- /dev/null +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/traits/SyntheticEventStreamUnionTrait.kt @@ -0,0 +1,19 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package software.amazon.smithy.rust.codegen.smithy.traits + +import software.amazon.smithy.model.node.Node +import software.amazon.smithy.model.shapes.MemberShape +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.model.traits.AnnotationTrait + +class SyntheticEventStreamUnionTrait( + val errorMembers: List, +) : AnnotationTrait(ID, Node.objectNode()) { + companion object { + val ID = ShapeId.from("smithy.api.internal#syntheticEventStreamUnion") + } +} diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/EventStreamNormalizer.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/EventStreamNormalizer.kt new file mode 100644 index 0000000000..b435f118b5 --- /dev/null +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/EventStreamNormalizer.kt @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package software.amazon.smithy.rust.codegen.smithy.transformers + +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.UnionShape +import software.amazon.smithy.model.traits.ErrorTrait +import software.amazon.smithy.model.transform.ModelTransformer +import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticEventStreamUnionTrait +import software.amazon.smithy.rust.codegen.util.hasTrait +import software.amazon.smithy.rust.codegen.util.isEventStream + +/** + * Generates synthetic unions to replace the modeled unions for Event Stream types. + * This allows us to strip out all the error union members once up-front, instead of in each + * place that does codegen with the unions. + */ +object EventStreamNormalizer { + fun transform(model: Model): Model = ModelTransformer.create().mapShapes(model) { shape -> + if (shape is UnionShape && shape.isEventStream()) { + syntheticEquivalent(model, shape) + } else { + shape + } + } + + private fun syntheticEquivalent(model: Model, union: UnionShape): UnionShape { + val partitions = union.members().partition { member -> + model.expectShape(member.target).hasTrait() + } + val errorMembers = partitions.first + val eventMembers = partitions.second + + return union.toBuilder() + .members(eventMembers) + .addTrait(SyntheticEventStreamUnionTrait(errorMembers)) + .build() + } +} diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/OperationNormalizer.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/OperationNormalizer.kt index 054d386954..a19d68ba96 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/OperationNormalizer.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/OperationNormalizer.kt @@ -17,23 +17,25 @@ import software.amazon.smithy.rust.codegen.util.orNull import java.util.Optional import kotlin.streams.toList -typealias StructureModifier = (OperationShape, StructureShape?) -> StructureShape? - /** * Generate synthetic Input and Output structures for operations. */ -class OperationNormalizer(private val model: Model) { +object OperationNormalizer { + // Functions to construct synthetic shape IDs—Don't rely on these in external code. + // Rename safety: Operations cannot be renamed + private fun OperationShape.syntheticInputId() = ShapeId.fromParts(this.id.namespace, "${this.id.name}Input") + private fun OperationShape.syntheticOutputId() = ShapeId.fromParts(this.id.namespace, "${this.id.name}Output") /** * Add synthetic input & output shapes to every Operation in model. The generated shapes will be marked with * [SyntheticInputTrait] and [SyntheticOutputTrait] respectively. Shapes will be added _even_ if the operation does * not specify an input or an output. */ - fun transformModel(): Model { + fun transform(model: Model): Model { val transformer = ModelTransformer.create() val operations = model.shapes(OperationShape::class.java).toList() val newShapes = operations.flatMap { operation -> // Generate or modify the input and output of the given `Operation` to be a unique shape - syntheticInputShapes(operation) + syntheticOutputShapes(operation) + syntheticInputShapes(model, operation) + syntheticOutputShapes(model, operation) } val modelWithOperationInputs = model.toBuilder().addShapes(newShapes).build() return transformer.mapShapes(modelWithOperationInputs) { @@ -49,7 +51,7 @@ class OperationNormalizer(private val model: Model) { } } - private fun syntheticOutputShapes(operation: OperationShape): List { + private fun syntheticOutputShapes(model: Model, operation: OperationShape): List { val outputId = operation.syntheticOutputId() val outputShapeBuilder = operation.output.map { shapeId -> model.expectShape(shapeId, StructureShape::class.java).toBuilder().rename(outputId) @@ -63,7 +65,7 @@ class OperationNormalizer(private val model: Model) { return listOfNotNull(outputShape) } - private fun syntheticInputShapes(operation: OperationShape): List { + private fun syntheticInputShapes(model: Model, operation: OperationShape): List { val inputId = operation.syntheticInputId() val inputShapeBuilder = operation.input.map { shapeId -> model.expectShape(shapeId, StructureShape::class.java).toBuilder().rename(inputId) @@ -79,13 +81,6 @@ class OperationNormalizer(private val model: Model) { } private fun empty(id: ShapeId) = StructureShape.builder().id(id) - - companion object { - // Functions to construct synthetic shape IDs—Don't rely on these in external code. - // Rename safety: Operations cannot be renamed - private fun OperationShape.syntheticInputId() = ShapeId.fromParts(this.id.namespace, "${this.id.name}Input") - private fun OperationShape.syntheticOutputId() = ShapeId.fromParts(this.id.namespace, "${this.id.name}Output") - } } private fun StructureShape.Builder.rename(newId: ShapeId): StructureShape.Builder { diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/UnionGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/UnionGeneratorTest.kt index 0e70b6cabd..904e922f1a 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/UnionGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/UnionGeneratorTest.kt @@ -6,7 +6,6 @@ package software.amazon.smithy.rust.codegen.generators import io.kotest.matchers.string.shouldContain -import io.kotest.matchers.string.shouldNotContain import org.junit.jupiter.api.Test import software.amazon.smithy.codegen.core.SymbolProvider import software.amazon.smithy.rust.codegen.rustlang.RustWriter @@ -78,29 +77,6 @@ class UnionGeneratorTest { ) } - @Test - fun `filter out errors for Event Stream unions`() { - val writer = generateUnion( - """ - @error("client") structure BadRequestException { } - @error("client") structure ConflictException { } - structure NormalMessage { } - - @streaming - union MyUnion { - BadRequestException: BadRequestException, - ConflictException: ConflictException, - NormalMessage: NormalMessage, - } - """ - ) - - val code = writer.toString() - code shouldNotContain "BadRequestException" - code shouldNotContain "ConflictException" - code shouldContain "NormalMessage" - } - private fun generateUnion(modelSmithy: String, unionName: String = "MyUnion"): RustWriter { val model = "namespace test\n$modelSmithy".asSmithyModel() val provider: SymbolProvider = testSymbolProvider(model) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/RequestBindingGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/RequestBindingGeneratorTest.kt index bab4326177..0a47a272fa 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/RequestBindingGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/RequestBindingGeneratorTest.kt @@ -101,7 +101,7 @@ class RequestBindingGeneratorTest { stringHeader: String } """.asSmithyModel() - private val model = OperationNormalizer(baseModel).transformModel() + private val model = OperationNormalizer.transform(baseModel) private val operationShape = model.expectShape(ShapeId.from("smithy.example#PutObject"), OperationShape::class.java) private val inputShape = model.expectShape(operationShape.input.get(), StructureShape::class.java) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/ResponseBindingGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/ResponseBindingGeneratorTest.kt index d455bd774d..30460bc822 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/ResponseBindingGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/ResponseBindingGeneratorTest.kt @@ -66,7 +66,7 @@ class ResponseBindingGeneratorTest { additional: String, } """.asSmithyModel() - private val model = OperationNormalizer(baseModel).transformModel() + private val model = OperationNormalizer.transform(baseModel) private val operationShape = model.expectShape(ShapeId.from("smithy.example#PutObject"), OperationShape::class.java) private val symbolProvider = testSymbolProvider(model) private val testProtocolConfig: ProtocolConfig = testProtocolConfig(model) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/EventStreamSymbolProviderTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/EventStreamSymbolProviderTest.kt index a72a66024b..bee52cfd02 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/EventStreamSymbolProviderTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/EventStreamSymbolProviderTest.kt @@ -19,7 +19,7 @@ class EventStreamSymbolProviderTest { @Test fun `it should adjust types for operations with event streams`() { // Transform the model so that it has synthetic inputs/outputs - val model = OperationNormalizer( + val model = OperationNormalizer.transform( """ namespace test @@ -38,7 +38,7 @@ class EventStreamSymbolProviderTest { } service TestService { version: "123", operations: [TestOperation] } """.asSmithyModel() - ).transformModel() + ) val service = model.expectShape(ShapeId.from("test#TestService")) as ServiceShape val provider = EventStreamSymbolProvider(TestRuntimeConfig, SymbolVisitor(model, service, DefaultConfig), model) @@ -56,7 +56,7 @@ class EventStreamSymbolProviderTest { @Test fun `it should leave alone types for operations without event streams`() { - val model = OperationNormalizer( + val model = OperationNormalizer.transform( """ namespace test @@ -74,7 +74,7 @@ class EventStreamSymbolProviderTest { } service TestService { version: "123", operations: [TestOperation] } """.asSmithyModel() - ).transformModel() + ) val service = model.expectShape(ShapeId.from("test#TestService")) as ServiceShape val provider = EventStreamSymbolProvider(TestRuntimeConfig, SymbolVisitor(model, service, DefaultConfig), model) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/StreamingShapeSymbolProviderTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/StreamingShapeSymbolProviderTest.kt index 23eafa6d88..adbc85c962 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/StreamingShapeSymbolProviderTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/StreamingShapeSymbolProviderTest.kt @@ -34,7 +34,7 @@ internal class StreamingShapeSymbolProviderTest { fun `generates a byte stream on streaming output`() { // we could test exactly the streaming shape symbol provider, but we actually care about is the full stack // "doing the right thing" - val modelWithOperationTraits = OperationNormalizer(model).transformModel() + val modelWithOperationTraits = OperationNormalizer.transform(model) val symbolProvider = testSymbolProvider(modelWithOperationTraits) symbolProvider.toSymbol(modelWithOperationTraits.lookup("test#GenerateSpeechOutput\$data")).name shouldBe ("byte_stream::ByteStream") symbolProvider.toSymbol(modelWithOperationTraits.lookup("test#GenerateSpeechInput\$data")).name shouldBe ("byte_stream::ByteStream") @@ -42,7 +42,7 @@ internal class StreamingShapeSymbolProviderTest { @Test fun `streaming members have a default`() { - val modelWithOperationTraits = OperationNormalizer(model).transformModel() + val modelWithOperationTraits = OperationNormalizer.transform(model) val symbolProvider = testSymbolProvider(modelWithOperationTraits) val outputSymbol = symbolProvider.toSymbol(modelWithOperationTraits.lookup("test#GenerateSpeechOutput\$data")) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolTestGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolTestGeneratorTest.kt index bc49ca1b75..489695a2fa 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolTestGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolTestGeneratorTest.kt @@ -22,8 +22,6 @@ import software.amazon.smithy.rust.codegen.smithy.RuntimeType import software.amazon.smithy.rust.codegen.smithy.customize.RustCodegenDecorator import software.amazon.smithy.rust.codegen.smithy.generators.error.errorSymbol import software.amazon.smithy.rust.codegen.smithy.protocols.ProtocolMap -import software.amazon.smithy.rust.codegen.smithy.transformers.OperationNormalizer -import software.amazon.smithy.rust.codegen.smithy.transformers.RemoveEventStreamOperations import software.amazon.smithy.rust.codegen.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.testutil.generatePluginContext import software.amazon.smithy.rust.codegen.util.CommandFailed @@ -163,9 +161,7 @@ class HttpProtocolTestGeneratorTest { return TestProtocol(protocolConfig) } - override fun transformModel(model: Model): Model { - return OperationNormalizer(model).transformModel().let(RemoveEventStreamOperations::transform) - } + override fun transformModel(model: Model): Model = model override fun support(): ProtocolSupport { return ProtocolSupport(true, true, true, true) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/EventStreamTestTools.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/EventStreamTestTools.kt index ba93470caf..73a2d78b5c 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/EventStreamTestTools.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/EventStreamTestTools.kt @@ -25,6 +25,7 @@ import software.amazon.smithy.rust.codegen.smithy.generators.StructureGenerator import software.amazon.smithy.rust.codegen.smithy.generators.UnionGenerator import software.amazon.smithy.rust.codegen.smithy.generators.error.CombinedErrorGenerator import software.amazon.smithy.rust.codegen.smithy.generators.implBlock +import software.amazon.smithy.rust.codegen.smithy.transformers.EventStreamNormalizer import software.amazon.smithy.rust.codegen.smithy.transformers.OperationNormalizer import software.amazon.smithy.rust.codegen.testutil.TestWorkspace import software.amazon.smithy.rust.codegen.testutil.TestWriterDelegator @@ -220,7 +221,7 @@ data class TestEventStreamProject( object EventStreamTestTools { fun generateTestProject(model: Model): TestEventStreamProject { - val model = OperationNormalizer(model).transformModel() + val model = EventStreamNormalizer.transform(OperationNormalizer.transform(model)) val serviceShape = model.expectShape(ShapeId.from("test#TestService")) as ServiceShape val operationShape = model.expectShape(ShapeId.from("test#TestStreamOp")) as OperationShape val unionShape = model.expectShape(ShapeId.from("test#TestStream")) as UnionShape diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/AwsQueryParserGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/AwsQueryParserGeneratorTest.kt index 5b34b568de..81cc77407e 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/AwsQueryParserGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/AwsQueryParserGeneratorTest.kt @@ -42,7 +42,7 @@ class AwsQueryParserGeneratorTest { @Test fun `it modifies operation parsing to include Response and Result tags`() { - val model = RecursiveShapeBoxer.transform(OperationNormalizer(baseModel).transformModel()) + val model = RecursiveShapeBoxer.transform(OperationNormalizer.transform(baseModel)) val symbolProvider = testSymbolProvider(model) val parserGenerator = AwsQueryParserGenerator( testProtocolConfig(model), diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/Ec2QueryParserGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/Ec2QueryParserGeneratorTest.kt index 7d5cfa0d66..7912e114ef 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/Ec2QueryParserGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/Ec2QueryParserGeneratorTest.kt @@ -42,7 +42,7 @@ class Ec2QueryParserGeneratorTest { @Test fun `it modifies operation parsing to include Response and Result tags`() { - val model = RecursiveShapeBoxer.transform(OperationNormalizer(baseModel).transformModel()) + val model = RecursiveShapeBoxer.transform(OperationNormalizer.transform(baseModel)) val symbolProvider = testSymbolProvider(model) val parserGenerator = Ec2QueryParserGenerator( testProtocolConfig(model), diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGeneratorTest.kt index c4878c220d..9634b52015 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGeneratorTest.kt @@ -106,7 +106,7 @@ class JsonParserGeneratorTest { @Test fun `generates valid deserializers`() { - val model = RecursiveShapeBoxer.transform(OperationNormalizer(baseModel).transformModel()) + val model = RecursiveShapeBoxer.transform(OperationNormalizer.transform(baseModel)) val symbolProvider = testSymbolProvider(model) val parserGenerator = JsonParserGenerator( testProtocolConfig(model), diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/XmlBindingTraitParserGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/XmlBindingTraitParserGeneratorTest.kt index 0e20f5aeaf..42e675cd58 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/XmlBindingTraitParserGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/XmlBindingTraitParserGeneratorTest.kt @@ -90,7 +90,7 @@ internal class XmlBindingTraitParserGeneratorTest { @Test fun `generates valid parsers`() { - val model = RecursiveShapeBoxer.transform(OperationNormalizer(baseModel).transformModel()) + val model = RecursiveShapeBoxer.transform(OperationNormalizer.transform(baseModel)) val symbolProvider = testSymbolProvider(model) val parserGenerator = XmlBindingTraitParserGenerator( testProtocolConfig(model), diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/AwsQuerySerializerGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/AwsQuerySerializerGeneratorTest.kt index d166b36bce..813325fbee 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/AwsQuerySerializerGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/AwsQuerySerializerGeneratorTest.kt @@ -81,7 +81,7 @@ class AwsQuerySerializerGeneratorTest { @Test fun `generates valid serializers`() { - val model = RecursiveShapeBoxer.transform(OperationNormalizer(baseModel).transformModel()) + val model = RecursiveShapeBoxer.transform(OperationNormalizer.transform(baseModel)) val symbolProvider = testSymbolProvider(model) val parserGenerator = AwsQuerySerializerGenerator(testProtocolConfig(model)) val operationGenerator = parserGenerator.operationSerializer(model.lookup("test#Op")) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/Ec2QuerySerializerGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/Ec2QuerySerializerGeneratorTest.kt index bcdb0a73a6..4ce88b07aa 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/Ec2QuerySerializerGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/Ec2QuerySerializerGeneratorTest.kt @@ -80,7 +80,7 @@ class Ec2QuerySerializerGeneratorTest { @Test fun `generates valid serializers`() { - val model = RecursiveShapeBoxer.transform(OperationNormalizer(baseModel).transformModel()) + val model = RecursiveShapeBoxer.transform(OperationNormalizer.transform(baseModel)) val symbolProvider = testSymbolProvider(model) val parserGenerator = Ec2QuerySerializerGenerator(testProtocolConfig(model)) val operationGenerator = parserGenerator.operationSerializer(model.lookup("test#Op")) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGeneratorTest.kt index f52f49b98d..5e0ab99553 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGeneratorTest.kt @@ -98,7 +98,7 @@ class JsonSerializerGeneratorTest { @Test fun `generates valid serializers`() { - val model = RecursiveShapeBoxer.transform(OperationNormalizer(baseModel).transformModel()) + val model = RecursiveShapeBoxer.transform(OperationNormalizer.transform(baseModel)) val symbolProvider = testSymbolProvider(model) val parserSerializer = JsonSerializerGenerator( testProtocolConfig(model), diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGeneratorTest.kt index 63f585f546..5c21344d25 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGeneratorTest.kt @@ -103,7 +103,7 @@ internal class XmlBindingTraitSerializerGeneratorTest { @Test fun `generates valid serializers`() { - val model = RecursiveShapeBoxer.transform(OperationNormalizer(baseModel).transformModel()) + val model = RecursiveShapeBoxer.transform(OperationNormalizer.transform(baseModel)) val symbolProvider = testSymbolProvider(model) val parserGenerator = XmlBindingTraitSerializerGenerator( testProtocolConfig(model), diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/EventStreamNormalizerTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/EventStreamNormalizerTest.kt new file mode 100644 index 0000000000..4fcb7b410e --- /dev/null +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/EventStreamNormalizerTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package software.amazon.smithy.rust.codegen.smithy.transformers + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.model.shapes.UnionShape +import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticEventStreamUnionTrait +import software.amazon.smithy.rust.codegen.testutil.asSmithyModel +import software.amazon.smithy.rust.codegen.util.expectTrait +import software.amazon.smithy.rust.codegen.util.hasTrait + +class EventStreamNormalizerTest { + @Test + fun `it should leave normal unions alone`() { + val transformed = EventStreamNormalizer.transform( + """ + namespace test + union SomeNormalUnion { + Foo: String, + Bar: Long, + } + """.asSmithyModel() + ) + + val shape = transformed.expectShape(ShapeId.from("test#SomeNormalUnion"), UnionShape::class.java) + shape.hasTrait() shouldBe false + shape.memberNames shouldBe listOf("Foo", "Bar") + } + + @Test + fun `it should transform event stream unions`() { + val transformed = EventStreamNormalizer.transform( + """ + namespace test + + structure SomeMember { + } + + @error("client") + structure SomeError { + } + + @streaming + union SomeEventStream { + SomeMember: SomeMember, + SomeError: SomeError, + } + """.asSmithyModel() + ) + + val shape = transformed.expectShape(ShapeId.from("test#SomeEventStream"), UnionShape::class.java) + shape.hasTrait() shouldBe true + shape.memberNames shouldBe listOf("SomeMember") + + val trait = shape.expectTrait() + trait.errorMembers.map { it.memberName } shouldBe listOf("SomeError") + } +} diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/OperationNormalizerTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/OperationNormalizerTest.kt index 29b41bed57..91f2a7334b 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/OperationNormalizerTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/OperationNormalizerTest.kt @@ -26,8 +26,7 @@ internal class OperationNormalizerTest { """.asSmithyModel() val operationId = ShapeId.from("smithy.test#Empty") model.expectShape(operationId, OperationShape::class.java).input.isPresent shouldBe false - val sut = OperationNormalizer(model) - val modified = sut.transformModel() + val modified = OperationNormalizer.transform(model) val operation = modified.expectShape(operationId, OperationShape::class.java) operation.input.isPresent shouldBe true operation.input.get().name shouldBe "EmptyInput" @@ -55,8 +54,7 @@ internal class OperationNormalizerTest { """.asSmithyModel() val operationId = ShapeId.from("smithy.test#MyOp") model.expectShape(operationId, OperationShape::class.java).input.isPresent shouldBe true - val sut = OperationNormalizer(model) - val modified = sut.transformModel() + val modified = OperationNormalizer.transform(model) val operation = modified.expectShape(operationId, OperationShape::class.java) operation.input.isPresent shouldBe true val inputId = operation.input.get() @@ -79,8 +77,7 @@ internal class OperationNormalizerTest { """.asSmithyModel() val operationId = ShapeId.from("smithy.test#MyOp") model.expectShape(operationId, OperationShape::class.java).output.isPresent shouldBe true - val sut = OperationNormalizer(model) - val modified = sut.transformModel() + val modified = OperationNormalizer.transform(model) val operation = modified.expectShape(operationId, OperationShape::class.java) operation.output.isPresent shouldBe true val outputId = operation.output.get() diff --git a/rust-runtime/inlineable/src/lib.rs b/rust-runtime/inlineable/src/lib.rs index d225bb0e97..5e2818082d 100644 --- a/rust-runtime/inlineable/src/lib.rs +++ b/rust-runtime/inlineable/src/lib.rs @@ -5,8 +5,6 @@ #[allow(dead_code)] mod ec2_query_errors; -#[allow(unused)] -mod event_stream; #[allow(dead_code)] mod idempotency_token; #[allow(dead_code)] From 6df5edb4b355a467a1591ca4cca5262c5932ef2f Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Wed, 18 Aug 2021 17:22:59 -0700 Subject: [PATCH 15/17] Fix crash in SigV4SigningDecorator --- .../amazon/smithy/rustsdk/SigV4SigningDecorator.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4SigningDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4SigningDecorator.kt index c251045842..e6fe3d2eb7 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4SigningDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4SigningDecorator.kt @@ -33,6 +33,7 @@ import software.amazon.smithy.rust.codegen.util.expectTrait import software.amazon.smithy.rust.codegen.util.hasTrait import software.amazon.smithy.rust.codegen.util.isEventStream import software.amazon.smithy.rust.codegen.util.isInputEventStream +import software.amazon.smithy.rust.codegen.util.orNull /** * The SigV4SigningDecorator: @@ -55,8 +56,10 @@ class SigV4SigningDecorator : RustCodegenDecorator { return baseCustomizations.letIf(applies(protocolConfig)) { customizations -> val serviceHasEventStream = protocolConfig.serviceShape.operations .any { id -> - protocolConfig.model.expectShape(id, OperationShape::class.java) - .isEventStream(protocolConfig.model) + // Some models like `kinesisanalytics` have StructureShapes in their operation list, + // so don't assume they're always OperationShape. + val shape = protocolConfig.model.getShape(id).orNull() + shape is OperationShape && shape.isEventStream(protocolConfig.model) } customizations + SigV4SigningConfig( protocolConfig.runtimeConfig, From d0b147e85f53c908692682cea8abd75723c384bc Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Wed, 18 Aug 2021 17:50:26 -0700 Subject: [PATCH 16/17] Add test for unmarshalling errors --- .../smithy/protocols/EventStreamTestTools.kt | 38 +++++++++++++++++-- .../EventStreamUnmarshallerGeneratorTest.kt | 36 ++++++++++++++++-- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/EventStreamTestTools.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/EventStreamTestTools.kt index 73a2d78b5c..732120f8c2 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/EventStreamTestTools.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/EventStreamTestTools.kt @@ -118,6 +118,7 @@ object EventStreamTestModels { val validTestStruct: String, val validMessageWithNoHeaderPayloadTraits: String, val validTestUnion: String, + val validSomeError: String, val protocolBuilder: (ProtocolConfig) -> Protocol, ) { override fun toString(): String = protocolShapeId @@ -134,6 +135,7 @@ object EventStreamTestModels { validTestStruct = """{"someString":"hello","someInt":5}""", validMessageWithNoHeaderPayloadTraits = """{"someString":"hello","someInt":5}""", validTestUnion = """{"Foo":"hello"}""", + validSomeError = """{"Message":"some error"}""", ) { RestJson(it) } ), Arguments.of( @@ -144,6 +146,7 @@ object EventStreamTestModels { validTestStruct = """{"someString":"hello","someInt":5}""", validMessageWithNoHeaderPayloadTraits = """{"someString":"hello","someInt":5}""", validTestUnion = """{"Foo":"hello"}""", + validSomeError = """{"Message":"some error"}""", ) { AwsJson(it, AwsJsonVersion.Json11) } ), Arguments.of( @@ -163,7 +166,16 @@ object EventStreamTestModels { 5 """.trimIndent(), - validTestUnion = "hello" + validTestUnion = "hello", + validSomeError = """ + + + SomeError + SomeError + some error + + + """.trimIndent() ) { RestXml(it) } ), Arguments.of( @@ -183,7 +195,16 @@ object EventStreamTestModels { 5 """.trimIndent(), - validTestUnion = "hello" + validTestUnion = "hello", + validSomeError = """ + + + SomeError + SomeError + some error + + + """.trimIndent() ) { AwsQueryProtocol(it) } ), Arguments.of( @@ -203,7 +224,18 @@ object EventStreamTestModels { 5 """.trimIndent(), - validTestUnion = "hello" + validTestUnion = "hello", + validSomeError = """ + + + + SomeError + SomeError + some error + + + + """.trimIndent() ) { Ec2QueryProtocol(it) } ), ) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGeneratorTest.kt index db05f055df..d39f1c2811 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGeneratorTest.kt @@ -42,12 +42,12 @@ class EventStreamUnmarshallerGeneratorTest { test.project.lib { writer -> // TODO(EventStream): Add test for bad content type - // TODO(EventStream): Add test for modeled error parsing // TODO(EventStream): Add test for generic error parsing writer.rust( """ use smithy_eventstream::frame::{Header, HeaderValue, Message, UnmarshallMessage, UnmarshalledMessage}; use smithy_types::{Blob, Instant}; + use crate::error::*; use crate::model::*; fn msg( @@ -56,10 +56,14 @@ class EventStreamUnmarshallerGeneratorTest { content_type: &'static str, payload: &'static [u8], ) -> Message { - Message::new(payload) + let message = Message::new(payload) .add_header(Header::new(":message-type", HeaderValue::String(message_type.into()))) - .add_header(Header::new(":event-type", HeaderValue::String(event_type.into()))) - .add_header(Header::new(":content-type", HeaderValue::String(content_type.into()))) + .add_header(Header::new(":content-type", HeaderValue::String(content_type.into()))); + if message_type == "event" { + message.add_header(Header::new(":event-type", HeaderValue::String(event_type.into()))) + } else { + message.add_header(Header::new(":exception-type", HeaderValue::String(event_type.into()))) + } } fn expect_event(unmarshalled: UnmarshalledMessage) -> T { match unmarshalled { @@ -67,6 +71,12 @@ class EventStreamUnmarshallerGeneratorTest { _ => panic!("expected event, got: {:?}", unmarshalled), } } + fn expect_error(unmarshalled: UnmarshalledMessage) -> E { + match unmarshalled { + UnmarshalledMessage::Error(error) => error, + _ => panic!("expected error, got: {:?}", unmarshalled), + } + } """ ) @@ -211,6 +221,24 @@ class EventStreamUnmarshallerGeneratorTest { """, "message_with_no_header_payload_traits", ) + + writer.unitTest( + """ + let message = msg( + "exception", + "SomeError", + "${testCase.contentType}", + br#"${testCase.validSomeError}"# + ); + let result = ${writer.format(generator.render())}().unmarshall(&message); + assert!(result.is_ok(), "expected ok, got: {:?}", result); + match expect_error(result.unwrap()).kind { + TestStreamOpErrorKind::SomeError(err) => assert_eq!(Some("some error"), err.message()), + kind => panic!("expected SomeError, but got {:?}", kind), + } + """, + "some_error", + ) } test.project.compileAndTest() } From 553aec2aad704092b996daf13a834c83640e3303 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Fri, 20 Aug 2021 10:19:11 -0700 Subject: [PATCH 17/17] Incorporate CR feedback --- .../smithy/rustsdk/SigV4SigningDecorator.kt | 12 ++----- .../rust/codegen/rustlang/CargoDependency.kt | 6 ++-- .../rust/codegen/smithy/CodegenDelegator.kt | 32 ++++++++----------- .../parse/EventStreamUnmarshallerGenerator.kt | 20 ++++++------ .../transformers/EventStreamNormalizer.kt | 5 +-- .../amazon/smithy/rust/codegen/util/Smithy.kt | 7 ++++ 6 files changed, 36 insertions(+), 46 deletions(-) diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4SigningDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4SigningDecorator.kt index e6fe3d2eb7..83767912c7 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4SigningDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4SigningDecorator.kt @@ -30,10 +30,9 @@ import software.amazon.smithy.rust.codegen.smithy.generators.config.ServiceConfi import software.amazon.smithy.rust.codegen.smithy.letIf import software.amazon.smithy.rust.codegen.util.dq import software.amazon.smithy.rust.codegen.util.expectTrait +import software.amazon.smithy.rust.codegen.util.hasEventStreamOperations import software.amazon.smithy.rust.codegen.util.hasTrait -import software.amazon.smithy.rust.codegen.util.isEventStream import software.amazon.smithy.rust.codegen.util.isInputEventStream -import software.amazon.smithy.rust.codegen.util.orNull /** * The SigV4SigningDecorator: @@ -54,16 +53,9 @@ class SigV4SigningDecorator : RustCodegenDecorator { baseCustomizations: List ): List { return baseCustomizations.letIf(applies(protocolConfig)) { customizations -> - val serviceHasEventStream = protocolConfig.serviceShape.operations - .any { id -> - // Some models like `kinesisanalytics` have StructureShapes in their operation list, - // so don't assume they're always OperationShape. - val shape = protocolConfig.model.getShape(id).orNull() - shape is OperationShape && shape.isEventStream(protocolConfig.model) - } customizations + SigV4SigningConfig( protocolConfig.runtimeConfig, - serviceHasEventStream, + protocolConfig.serviceShape.hasEventStreamOperations(protocolConfig.model), protocolConfig.serviceShape.expectTrait() ) } 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 3d7c79821c..07523155c6 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 @@ -123,14 +123,14 @@ data class CargoDependency( val optional: Boolean = false, val features: Set = emptySet() ) : RustDependency(name) { + val key: Triple get() = Triple(name, location, scope) + + fun canMergeWith(other: CargoDependency): Boolean = key == other.key fun withFeature(feature: String): CargoDependency { return copy(features = features.toMutableSet().apply { add(feature) }) } - fun canMergeWith(other: CargoDependency): Boolean = - name == other.name && location == other.location && scope == other.scope - override fun version(): String = when (location) { is CratesIo -> location.version is Local -> "local" diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenDelegator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenDelegator.kt index e56253be02..beccf485df 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenDelegator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenDelegator.kt @@ -117,23 +117,17 @@ fun CodegenWriterDelegator.finalize( flushWriters() } -internal fun mergeDependencyFeatures(cargoDependencies: List): List { - val dependencies = cargoDependencies.toMutableList() - dependencies.sortBy { it.name } - - var index = 1 - while (index < dependencies.size) { - val first = dependencies[index - 1] - val second = dependencies[index] - if (first.canMergeWith(second)) { - dependencies[index - 1] = first.copy( - features = first.features + second.features, - optional = first.optional && second.optional - ) - dependencies.removeAt(index) - } else { - index += 1 - } - } - return dependencies +private fun CargoDependency.mergeWith(other: CargoDependency): CargoDependency { + check(key == other.key) + return copy( + features = features + other.features, + optional = optional && other.optional + ) } + +internal fun mergeDependencyFeatures(cargoDependencies: List): List = + cargoDependencies.groupBy { it.key } + .mapValues { group -> group.value.reduce { acc, next -> acc.mergeWith(next) } } + .values + .toList() + .sortedBy { it.name } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt index f5b68ff2a0..75c400fbae 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt @@ -55,7 +55,7 @@ class EventStreamUnmarshallerGenerator( "Error" to RuntimeType("Error", smithyEventStream, "smithy_eventstream::error"), "Header" to RuntimeType("Header", smithyEventStream, "smithy_eventstream::frame"), "HeaderValue" to RuntimeType("HeaderValue", smithyEventStream, "smithy_eventstream::frame"), - "ExpectFns" to RuntimeType("smithy", smithyEventStream, "smithy_eventstream"), + "expect_fns" to RuntimeType("smithy", smithyEventStream, "smithy_eventstream"), "Message" to RuntimeType("Message", smithyEventStream, "smithy_eventstream::frame"), "SmithyError" to RuntimeType("Error", CargoDependency.SmithyTypes(runtimeConfig), "smithy_types"), "UnmarshallMessage" to RuntimeType("UnmarshallMessage", smithyEventStream, "smithy_eventstream::frame"), @@ -102,7 +102,7 @@ class EventStreamUnmarshallerGenerator( ) { rustBlockTemplate( """ - let response_headers = #{ExpectFns}::parse_response_headers(&message)?; + let response_headers = #{expect_fns}::parse_response_headers(&message)?; match response_headers.message_type.as_str() """, *codegenScope @@ -186,14 +186,14 @@ class EventStreamUnmarshallerGenerator( val memberName = symbolProvider.toMemberName(member) withBlock("builder = builder.$memberName(", ");") { when (val target = model.expectShape(member.target)) { - is BooleanShape -> rustTemplate("#{ExpectFns}::expect_bool(header)?", *codegenScope) - is ByteShape -> rustTemplate("#{ExpectFns}::expect_byte(header)?", *codegenScope) - is ShortShape -> rustTemplate("#{ExpectFns}::expect_int16(header)?", *codegenScope) - is IntegerShape -> rustTemplate("#{ExpectFns}::expect_int32(header)?", *codegenScope) - is LongShape -> rustTemplate("#{ExpectFns}::expect_int64(header)?", *codegenScope) - is BlobShape -> rustTemplate("#{ExpectFns}::expect_byte_array(header)?", *codegenScope) - is StringShape -> rustTemplate("#{ExpectFns}::expect_string(header)?", *codegenScope) - is TimestampShape -> rustTemplate("#{ExpectFns}::expect_timestamp(header)?", *codegenScope) + is BooleanShape -> rustTemplate("#{expect_fns}::expect_bool(header)?", *codegenScope) + is ByteShape -> rustTemplate("#{expect_fns}::expect_byte(header)?", *codegenScope) + is ShortShape -> rustTemplate("#{expect_fns}::expect_int16(header)?", *codegenScope) + is IntegerShape -> rustTemplate("#{expect_fns}::expect_int32(header)?", *codegenScope) + is LongShape -> rustTemplate("#{expect_fns}::expect_int64(header)?", *codegenScope) + is BlobShape -> rustTemplate("#{expect_fns}::expect_byte_array(header)?", *codegenScope) + is StringShape -> rustTemplate("#{expect_fns}::expect_string(header)?", *codegenScope) + is TimestampShape -> rustTemplate("#{expect_fns}::expect_timestamp(header)?", *codegenScope) else -> throw IllegalStateException("unsupported event stream header shape type: $target") } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/EventStreamNormalizer.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/EventStreamNormalizer.kt index b435f118b5..de58611a9d 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/EventStreamNormalizer.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/EventStreamNormalizer.kt @@ -28,12 +28,9 @@ object EventStreamNormalizer { } private fun syntheticEquivalent(model: Model, union: UnionShape): UnionShape { - val partitions = union.members().partition { member -> + val (errorMembers, eventMembers) = union.members().partition { member -> model.expectShape(member.target).hasTrait() } - val errorMembers = partitions.first - val eventMembers = partitions.second - return union.toBuilder() .members(eventMembers) .addTrait(SyntheticEventStreamUnionTrait(errorMembers)) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/util/Smithy.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/util/Smithy.kt index ad572c3612..7130b24c62 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/util/Smithy.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/util/Smithy.kt @@ -11,6 +11,7 @@ import software.amazon.smithy.model.shapes.BooleanShape import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.NumberShape import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.model.shapes.Shape import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.model.shapes.StructureShape @@ -67,6 +68,12 @@ fun OperationShape.isOutputEventStream(model: Model): Boolean { fun OperationShape.isEventStream(model: Model): Boolean { return isInputEventStream(model) || isOutputEventStream(model) } +fun ServiceShape.hasEventStreamOperations(model: Model): Boolean = operations.any { id -> + // Don't assume all of the looked up operation ids are operation shapes. Our + // synthetic input/output structure shapes can have the same name as an operation, + // as is the case with `kinesisanalytics`. + model.getShape(id).orNull()?.let { it is OperationShape && it.isEventStream(model) } ?: false +} /* * Returns the member of this structure targeted with streaming trait (if it exists).