Smithy async docs (#1278)

* Add lints to aws-smithy-async

* Write some event stream docs, and enable lints

* Codegen docs

* Fix one missing doc

* Compile fixes

* remove accidentally added code

* Update codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/SymbolVisitor.kt

* Update response headers docs
This commit is contained in:
Russell Cohen 2022-04-05 09:17:00 -07:00 committed by GitHub
parent e78d40f0d5
commit 6bbc776d67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 228 additions and 26 deletions

View File

@ -44,6 +44,45 @@
//! let client = aws_sdk_dynamodb::Client::new(&config);
//! # }
//! ```
//!
//! Override configuration after construction of `SdkConfig`:
//!
//! ```no_run
//! # use aws_types::SdkConfig;
//! # mod aws_sdk_dynamodb {
//! # pub mod config {
//! # pub struct Builder;
//! # impl Builder {
//! # pub fn credentials_provider(
//! # self,
//! # credentials_provider: impl aws_types::credentials::ProvideCredentials + 'static) -> Self { self }
//! # pub fn build(self) -> Builder { self }
//! # }
//! # impl From<&aws_types::SdkConfig> for Builder {
//! # fn from(_: &aws_types::SdkConfig) -> Self {
//! # todo!()
//! # }
//! # }
//! # }
//! # pub struct Client;
//! # impl Client {
//! # pub fn from_conf(conf: config::Builder) -> Self { Client }
//! # pub fn new(config: &aws_types::SdkConfig) -> Self { Client }
//! # }
//! # }
//! # async fn docs() {
//! # use aws_config::meta::region::RegionProviderChain;
//! # fn custom_provider(base: &SdkConfig) -> impl aws_types::credentials::ProvideCredentials {
//! # base.credentials_provider().unwrap().clone()
//! # }
//! let sdk_config = aws_config::load_from_env().await;
//! let custom_credentials_provider = custom_provider(&sdk_config);
//! let dynamo_config = aws_sdk_dynamodb::config::Builder::from(&sdk_config)
//! .credentials_provider(custom_credentials_provider)
//! .build();
//! let client = aws_sdk_dynamodb::Client::from_conf(dynamo_config);
//! # }
//! ```
#[allow(dead_code)]
const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");

View File

@ -225,11 +225,11 @@ private class AwsFluentClientDocs(codegenContext: CodegenContext) : FluentClient
/// ```rust,no_run
/// use #{aws_config}::RetryConfig;
/// ## async fn docs() {
/// let shared_config = #{aws_config}::load_from_env().await;
/// let config = $crateName::config::Builder::from(&shared_config)
/// .retry_config(RetryConfig::disabled())
/// .build();
/// let client = $crateName::Client::from_conf(config);
/// let shared_config = #{aws_config}::load_from_env().await;
/// let config = $crateName::config::Builder::from(&shared_config)
/// .retry_config(RetryConfig::disabled())
/// .build();
/// let client = $crateName::Client::from_conf(config);
/// ## }
""",
*codegenScope

View File

@ -9,6 +9,9 @@ import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.model.shapes.ShapeId
/**
* Code generation mode: In some situations, codegen has different behavior for client vs. server (eg. required fields)
*/
sealed class CodegenMode {
object Server : CodegenMode()
object Client : CodegenMode()

View File

@ -9,6 +9,13 @@ import software.amazon.smithy.model.node.Node
import software.amazon.smithy.model.shapes.ShapeId
import software.amazon.smithy.model.traits.Trait
/**
* Trait indicating that this shape should be represented with `Box<T>` when converted into Rust
*
* This is used to handle recursive shapes. See RecursiveShapeBoxer.
*
* This trait is synthetic, applied during code generation, and never used in actual models.
*/
class RustBoxTrait : Trait {
val ID = ShapeId.from("software.amazon.smithy.rust.codegen.smithy.rust.synthetic#box")
override fun toNode(): Node = Node.objectNode()

View File

@ -48,6 +48,14 @@ class StreamingShapeSymbolProvider(private val base: RustSymbolProvider, private
}
}
/**
* SymbolProvider to drop the clone and PartialEq bounds in streaming shapes
*
* Streaming shapes cannot be cloned and equality cannot be checked without reading the body. Because of this, these shapes
* do not implement `Clone` or `PartialEq`.
*
* Note that since streaming members can only be used on the root shape, this can only impact input and output shapes.
*/
class StreamingShapeMetadataProvider(private val base: RustSymbolProvider, private val model: Model) : SymbolMetadataProvider(base) {
override fun memberMeta(memberShape: MemberShape): RustMetadata {
return base.toSymbol(memberShape).expectRustMetadata()

View File

@ -48,6 +48,7 @@ import software.amazon.smithy.rust.codegen.util.toPascalCase
import software.amazon.smithy.rust.codegen.util.toSnakeCase
import kotlin.reflect.KClass
/** Map from Smithy Shapes to Rust Types */
val SimpleShapes: Map<KClass<out Shape>, RustType> = mapOf(
BooleanShape::class to RustType.Bool,
FloatShape::class to RustType.Float(32),
@ -74,6 +75,11 @@ val DefaultConfig =
codegenConfig = CodegenConfig()
)
/**
* Container type for the file a symbol should be written to
*
* Downstream code uses symbol location to determine which file to use acquiring a writer
*/
data class SymbolLocation(val namespace: String) {
val filename = "$namespace.rs"
}
@ -85,6 +91,11 @@ val Serializers = SymbolLocation("serializer")
val Inputs = SymbolLocation("input")
val Outputs = SymbolLocation("output")
/**
* Make the Rust type of a symbol optional (hold `Option<T>`)
*
* This is idempotent and will have no change if the type is already optional.
*/
fun Symbol.makeOptional(): Symbol {
return if (isOptional()) {
this
@ -98,6 +109,7 @@ fun Symbol.makeOptional(): Symbol {
}
}
/** Map the RustType of a symbol with [f] */
fun Symbol.mapRustType(f: (RustType) -> RustType): Symbol {
val newType = f(this.rustType())
return Symbol.builder().rustType(newType)
@ -106,17 +118,7 @@ fun Symbol.mapRustType(f: (RustType) -> RustType): Symbol {
.build()
}
fun Symbol.makeRustBoxed(): Symbol {
val symbol = this
val rustType = RustType.Box(symbol.rustType())
return with(Symbol.builder()) {
rustType(rustType)
addReference(symbol)
name(rustType.name)
build()
}
}
/** Set the symbolLocation for this symbol builder */
fun Symbol.Builder.locatedIn(symbolLocation: SymbolLocation): Symbol.Builder {
val currentRustType = this.build().rustType()
check(currentRustType is RustType.Opaque) { "Only Opaque can have their namespace updated" }
@ -126,8 +128,20 @@ fun Symbol.Builder.locatedIn(symbolLocation: SymbolLocation): Symbol.Builder {
.rustType(newRustType)
}
/**
* Track both the past and current name of a symbol
*
* When a symbol name conflicts with another name, we need to rename it. This tracks both names enabling us to generate helpful
* docs that cover both cases.
*
* Note that this is only used for enum shapes an enum variant does not have it's own symbol. For structures, the [Symbol.renamedFrom]
* field will be set.
*/
data class MaybeRenamed(val name: String, val renamedFrom: String?)
/**
* SymbolProvider interface that carries both the inner configuration and a function to produce an enum variant name.
*/
interface RustSymbolProvider : SymbolProvider {
fun config(): SymbolVisitorConfig
fun toEnumVariantName(definition: EnumDefinition): MaybeRenamed?
@ -142,6 +156,13 @@ fun SymbolProvider.wrapOptional(member: MemberShape, value: String): String = va
*/
fun SymbolProvider.toOptional(member: MemberShape, value: String): String = value.letIf(!toSymbol(member).isOptional()) { "Some($value)" }
/**
* Base converter from `Shape` to `Symbol`. Shapes are the direct contents of the `Smithy` model. `Symbols` carry information
* about Rust types, namespaces, dependencies, metadata as well as other information required to render a symbol.
*
* This is composed with other symbol visitors to handle behavior like Streaming shapes and determining the correct
* derives for a given shape.
*/
class SymbolVisitor(
private val model: Model,
private val serviceShape: ServiceShape?,
@ -155,6 +176,10 @@ class SymbolVisitor(
return shape.accept(this)
}
/**
* Services can rename their contained shapes. See https://awslabs.github.io/smithy/1.0/spec/core/model.html#service
* specifically, `rename`
*/
private fun Shape.contextName(): String {
return if (serviceShape != null) {
id.getName(serviceShape)
@ -163,6 +188,12 @@ class SymbolVisitor(
}
}
/**
* Return the name of a given `enum` variant. Note that this refers to `enum` in the Smithy context
* where enum is a trait that can be applied to [StringShape] and not in the Rust context of an algebraic data type.
*
* Because enum variants are not member shape, a separate handler is required.
*/
override fun toEnumVariantName(definition: EnumDefinition): MaybeRenamed? {
val baseName = definition.name.orNull()?.toPascalCase() ?: return null
return MaybeRenamed(baseName, null)
@ -187,6 +218,9 @@ class SymbolVisitor(
symbol
}
/**
* Produce `Box<T>` when the shape has the `RustBoxTrait`
*/
private fun handleRustBoxing(symbol: Symbol, shape: Shape): Symbol {
return if (shape.hasTrait<RustBoxTrait>()) {
val rustType = RustType.Box(symbol.rustType())
@ -336,6 +370,9 @@ fun Symbol.Builder.setDefault(default: Default): Symbol.Builder {
return this.putProperty(SYMBOL_DEFAULT, default)
}
/**
* Type representing the default value for a given type. (eg. for Strings, this is `""`)
*/
sealed class Default {
/**
* This symbol has no default value. If the symbol is not optional, this will be an error during builder construction
@ -361,12 +398,15 @@ fun Symbol.isOptional(): Boolean = when (this.rustType()) {
else -> false
}
fun Symbol.isBoxed(): Boolean = rustType().stripOuter<RustType.Option>() is RustType.Box
fun Symbol.isRustBoxed(): Boolean = rustType().stripOuter<RustType.Option>() is RustType.Box
// Symbols should _always_ be created with a Rust type & shape attached
fun Symbol.rustType(): RustType = this.getProperty(RUST_TYPE_KEY, RustType::class.java).get()
fun Symbol.shape(): Shape = this.expectProperty(SHAPE_KEY, Shape::class.java)
/**
* Utility function similar to `let` that conditionally applies [f] only if [cond] is true.
*/
fun <T> T.letIf(cond: Boolean, f: (T) -> T): T {
return if (cond) {
f(this)

View File

@ -19,6 +19,9 @@ import software.amazon.smithy.rust.codegen.util.hasTrait
import software.amazon.smithy.rust.codegen.util.inputShape
import software.amazon.smithy.rust.codegen.util.outputShape
/**
* KnowledgeIndex to determine the name for a given shape based on the XmlName trait and the shape's id.
*/
class XmlNameIndex(private val model: Model) : KnowledgeIndex {
companion object {
fun of(model: Model): XmlNameIndex {

View File

@ -41,7 +41,7 @@ import software.amazon.smithy.rust.codegen.smithy.generators.UnionGenerator
import software.amazon.smithy.rust.codegen.smithy.generators.builderSymbol
import software.amazon.smithy.rust.codegen.smithy.generators.renderUnknownVariant
import software.amazon.smithy.rust.codegen.smithy.generators.setterName
import software.amazon.smithy.rust.codegen.smithy.isBoxed
import software.amazon.smithy.rust.codegen.smithy.isRustBoxed
import software.amazon.smithy.rust.codegen.smithy.protocols.HttpBindingResolver
import software.amazon.smithy.rust.codegen.smithy.protocols.HttpLocation
import software.amazon.smithy.rust.codegen.smithy.protocols.deserializeFunctionName
@ -230,7 +230,7 @@ class JsonParserGenerator(
else -> PANIC("unexpected shape: $target")
}
val symbol = symbolProvider.toSymbol(memberShape)
if (symbol.isBoxed()) {
if (symbol.isRustBoxed()) {
rust(".map(Box::new)")
}
}

View File

@ -44,8 +44,8 @@ import software.amazon.smithy.rust.codegen.smithy.generators.UnionGenerator
import software.amazon.smithy.rust.codegen.smithy.generators.builderSymbol
import software.amazon.smithy.rust.codegen.smithy.generators.renderUnknownVariant
import software.amazon.smithy.rust.codegen.smithy.generators.setterName
import software.amazon.smithy.rust.codegen.smithy.isBoxed
import software.amazon.smithy.rust.codegen.smithy.isOptional
import software.amazon.smithy.rust.codegen.smithy.isRustBoxed
import software.amazon.smithy.rust.codegen.smithy.protocols.XmlMemberIndex
import software.amazon.smithy.rust.codegen.smithy.protocols.XmlNameIndex
import software.amazon.smithy.rust.codegen.smithy.protocols.deserializeFunctionName
@ -356,7 +356,7 @@ class XmlBindingTraitParserGenerator(
val target = model.expectShape(memberShape.target)
val symbol = symbolProvider.toSymbol(memberShape)
conditionalBlock("Some(", ")", forceOptional || symbol.isOptional()) {
conditionalBlock("Box::new(", ")", symbol.isBoxed()) {
conditionalBlock("Box::new(", ")", symbol.isRustBoxed()) {
when (target) {
is StringShape, is BooleanShape, is NumberShape, is TimestampShape, is BlobShape ->
parsePrimitiveInner(memberShape) {

View File

@ -11,6 +11,9 @@ import software.amazon.smithy.model.traits.AnnotationTrait
/**
* Indicates that a shape is a synthetic input (see `OperationNormalizer.kt`)
*
* All operations are normalized to have an input, even when they are defined without on. This is done for backwards compatibility
* and to produce a consistent API.
*/
class SyntheticInputTrait(
val operation: ShapeId,

View File

@ -11,6 +11,9 @@ import software.amazon.smithy.model.traits.AnnotationTrait
/**
* Indicates that a shape is a synthetic input (see `OperationNormalizer.kt`)
*
* All operations are normalized to have an input, even when they are defined without on. This is done for backwards compatibility
* and to produce a consistent API.
*/
class SyntheticOutputTrait constructor(val operation: ShapeId, val originalId: ShapeId?) :
AnnotationTrait(ID, Node.objectNode()) {

View File

@ -17,8 +17,24 @@ import java.util.logging.Logger
fun StructureShape.errorMessageMember(): MemberShape? = this.getMember("message").or { this.getMember("Message") }.orNull()
/**
* Ensure that all errors have error messages
*
* Not all errors are modeled with an error message field. However, in many cases, the server can still send an error.
* If an error, specifically, a structure shape with the error trait does not have a member `message` or `Message`,
* this transformer will add a `message` member targetting a string.
*
* This ensures that we always generate a modeled error message field enabling end users to easily extract the error
* message when present.
*
* Currently, this is run on all models, however, we may restrict this to AWS SDK code generation in the future.
*/
object AddErrorMessage {
private val logger = Logger.getLogger("AddErrorMessage")
/**
* Ensure that all errors have error messages
*/
fun transform(model: Model): Model {
return ModelTransformer.create().mapShapes(model) { shape ->
val addMessageField = shape.hasTrait<ErrorTrait>() && shape is StructureShape && shape.errorMessageMember() == null

View File

@ -37,7 +37,7 @@ import java.nio.file.Files.createTempDirectory
import java.nio.file.Path
/**
* Waiting for Kotlin to stabilize their temp directory stuff
* Waiting for Kotlin to stabilize their temp directory functionality
*/
private fun tempDir(directory: File? = null): File {
return if (directory != null) {
@ -167,6 +167,11 @@ fun RustWriter.unitTest(
}
}
/**
* WriterDelegator used for test purposes
*
* This exposes both the base directory and a list of [generatedFiles] for test purposes
*/
class TestWriterDelegator(private val fileManifest: FileManifest, symbolProvider: RustSymbolProvider) :
RustCrate(fileManifest, symbolProvider, DefaultPublicModules) {
val baseDir: Path = fileManifest.baseDir

View File

@ -13,7 +13,11 @@ import software.amazon.smithy.rust.codegen.smithy.generators.config.ConfigCustom
import software.amazon.smithy.rust.codegen.smithy.generators.config.ServiceConfig
import software.amazon.smithy.rust.codegen.smithy.generators.config.ServiceConfigGenerator
fun stubCustomization(name: String): ConfigCustomization {
/**
* Test helper to produce a valid config customization to test that a [ConfigCustomization] can be used in conjunction
* with other [ConfigCustomization]s.
*/
fun stubConfigCustomization(name: String): ConfigCustomization {
return object : ConfigCustomization() {
override fun section(section: ServiceConfig): Writable = writable {
when (section) {
@ -56,7 +60,7 @@ fun validateConfigCustomizations(
}
fun stubConfigProject(customization: ConfigCustomization, project: TestWriterDelegator): TestWriterDelegator {
val customizations = listOf(stubCustomization("a")) + customization + stubCustomization("b")
val customizations = listOf(stubConfigCustomization("a")) + customization + stubConfigCustomization("b")
val generator = ServiceConfigGenerator(customizations = customizations.toList())
project.withModule(RustModule.Config) {
generator.render(it)

View File

@ -2,6 +2,9 @@
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
//! Utility to drive a stream with an async function and a channel.
use crate::future::rendezvous;
use futures_util::StreamExt;
use pin_project_lite::pin_project;
@ -45,6 +48,9 @@ pin_project! {
}
impl<Item, F> FnStream<Item, F> {
/// Creates a new function based stream driven by `generator`.
///
/// For examples, see the documentation for [`FnStream`]
pub fn new<T>(generator: T) -> Self
where
T: FnOnce(rendezvous::Sender<Item>) -> F,
@ -80,6 +86,7 @@ where
/// When flattening paginated results, it's most convenient to produce an iterator where the `Result`
/// is present in each item. This provides `items()` which can wrap an stream of `Result<Page, Err>`
/// and produce a stream of `Result<Item, Err>`.
#[derive(Debug)]
pub struct TryFlatMap<I>(I);
impl<I> TryFlatMap<I> {
@ -88,6 +95,7 @@ impl<I> TryFlatMap<I> {
Self(i)
}
/// Produce a new [`Stream`] by mapping this stream with `map` then flattening the result
pub fn flat_map<M, Item, Iter, Page, Err>(self, map: M) -> impl Stream<Item = Result<Item, Err>>
where
I: Stream<Item = Result<Page, Err>>,

View File

@ -11,10 +11,11 @@ use std::task::{Context, Poll};
/// Future that never completes.
#[non_exhaustive]
#[derive(Default)]
#[derive(Default, Debug)]
pub struct Never;
impl Never {
/// Create a new `Never` future that never resolves
pub fn new() -> Never {
Default::default()
}

View File

@ -55,8 +55,10 @@ use std::task::{Context, Poll};
use pin_project_lite::pin_project;
/// Boxed future type alias
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
#[derive(Debug)]
/// Zero sized type for using NowOrLater when no future variant exists.
pub enum OnlyReady {}
@ -107,12 +109,14 @@ where
}
impl<T, F> NowOrLater<T, F> {
/// Creates a future that will resolve when `future` resolves
pub fn new(future: F) -> Self {
Self {
inner: Inner::Later { future },
}
}
/// Creates a future that immediately resolves to `value`
pub fn ready(value: T) -> NowOrLater<T, F> {
let value = Some(value);
Self {

View File

@ -38,6 +38,7 @@ pub fn channel<T>() -> (Sender<T>, Receiver<T>) {
)
}
#[derive(Debug)]
/// Sender-half of a channel
pub struct Sender<T> {
semaphore: Arc<Semaphore>,
@ -64,6 +65,7 @@ impl<T> Sender<T> {
}
}
#[derive(Debug)]
/// Receiver half of the rendezvous channel
pub struct Receiver<T> {
semaphore: Arc<Semaphore>,

View File

@ -42,6 +42,7 @@ use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
/// Error returned when [`Timeout`] times out
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct TimedOutError;
@ -54,6 +55,7 @@ impl fmt::Display for TimedOutError {
}
pin_project! {
/// Timeout Future
#[non_exhaustive]
#[must_use = "futures do nothing unless you `.await` or poll them"]
#[derive(Debug)]
@ -66,6 +68,9 @@ pin_project! {
}
impl<T, S> Timeout<T, S> {
/// Create a new future that will race `value` and `sleep`.
///
/// If `sleep` resolves first, a timeout error is returned. Otherwise, the value is returned.
pub fn new(value: T, sleep: S) -> Timeout<T, S> {
Timeout { value, sleep }
}

View File

@ -3,6 +3,13 @@
* SPDX-License-Identifier: Apache-2.0.
*/
#![warn(
missing_debug_implementations,
missing_docs,
rustdoc::all,
rust_2018_idioms
)]
//! Future utilities and runtime-agnostic abstractions for smithy-rs.
//!
//! Async runtime specific code is abstracted behind async traits, and implementations are

View File

@ -6,6 +6,7 @@
//! Provides an [`AsyncSleep`] trait that returns a future that sleeps for a given duration,
//! and implementations of `AsyncSleep` for different async runtimes.
use std::fmt::{Debug, Formatter};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
@ -45,6 +46,7 @@ pub fn default_async_sleep() -> Option<Arc<dyn AsyncSleep>> {
}
#[cfg(not(feature = "rt-tokio"))]
/// Returns a default sleep implementation based on the features enabled
pub fn default_async_sleep() -> Option<Arc<dyn AsyncSleep>> {
None
}
@ -53,7 +55,16 @@ pub fn default_async_sleep() -> Option<Arc<dyn AsyncSleep>> {
#[non_exhaustive]
pub struct Sleep(Pin<Box<dyn Future<Output = ()> + Send + 'static>>);
impl Debug for Sleep {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "Sleep")
}
}
impl Sleep {
/// Create a new [`Sleep`] future
///
/// The provided future will be Boxed.
pub fn new(future: impl Future<Output = ()> + Send + 'static) -> Sleep {
Sleep(Box::pin(future))
}
@ -75,6 +86,7 @@ pub struct TokioSleep;
#[cfg(feature = "rt-tokio")]
impl TokioSleep {
/// Create a new [`AsyncSleep`] implementation using the Tokio hashed wheel sleep implementation
pub fn new() -> TokioSleep {
Default::default()
}

View File

@ -3,6 +3,13 @@
* SPDX-License-Identifier: Apache-2.0.
*/
#![warn(
missing_debug_implementations,
/*missing_docs,
rustdoc::all,*/
rust_2018_idioms
)]
//! AWS Event Stream frame serialization/deserialization implementation.
mod buf;

View File

@ -10,6 +10,9 @@ use aws_smithy_types::{Blob, DateTime};
macro_rules! expect_shape_fn {
(fn $fn_name:ident[$val_typ:ident] -> $result_typ:ident { $val_name:ident -> $val_expr:expr }) => {
#[doc = "Expects that `header` is a `"]
#[doc = stringify!($result_typ)]
#[doc = "`."]
pub fn $fn_name(header: &Header) -> Result<$result_typ, Error> {
match header.value() {
HeaderValue::$val_typ($val_name) => Ok($val_expr),
@ -32,14 +35,32 @@ 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] -> DateTime { value -> *value });
/// Structured header data from a [`Message`]
#[derive(Debug)]
pub struct ResponseHeaders<'a> {
/// Content Type of the message
///
/// This can be a number of things depending on the protocol. For example, if the protocol is
/// AwsJson1, then this could be `application/json`, or `application/xml` for RestXml.
///
/// It will be `application/octet-stream` if there is a Blob payload shape, and `text/plain` if
/// there is a String payload shape.
pub content_type: Option<&'a StrBytes>,
/// Message Type field
///
/// This field is used to distinguish between events where the value is `event` and errors where
/// the value is `exception`
pub message_type: &'a StrBytes,
/// Smithy Type field
///
/// This field is used to determine which of the possible union variants that this message represents
pub smithy_type: &'a StrBytes,
}
impl<'a> ResponseHeaders<'a> {
/// Content-Type for this message
pub fn content_type(&self) -> Option<&str> {
self.content_type.map(|ct| ct.as_str())
}
@ -63,7 +84,11 @@ fn expect_header_str_value<'a>(
}
}
pub fn parse_response_headers(message: &Message) -> Result<ResponseHeaders, Error> {
/// Parse headers from [`Message`]
///
/// `:content-type`, `:message-type`, `:event-type`, and `:exception-type` headers will be parsed.
/// If any headers are invalid or missing, an error will be returned.
pub fn parse_response_headers(message: &Message) -> Result<ResponseHeaders<'_>, Error> {
let (mut content_type, mut message_type, mut event_type, mut exception_type) =
(None, None, None, None);
for header in message.headers() {