Skip to content

Commit

Permalink
[smithy-rs] Rework enum for forward-compatibility (#1945)
Browse files Browse the repository at this point in the history
* Make enum forward-compatible

This commit implements the suggested approach described in
smithy-lang/smithy-rs#627 (comment)

The idea is that once the user writes a match expression against an enum
and assumes that an execution path comes to a particular match arm, we
should guarantee that when the user upgrades a version of SDK, the
execution path should come to the same match arm as before.

* Add unit test to ensure enums are forward-compatible

The test first mimics the user's interaction with the enum generated from
a model where the user writes a match expression on the enum, wishing to
hit the match arm corresponding to Variant3, which is not yet supported
by the model. The test then simulates a scenario where the user now has
access to the updated enum generated from the next version of the model
that does support Variant3.
The user's code should compile in both of the use cases.

* Generate rustdoc for enum's forward-compatibility

This commits generates rustdoc explaining to the users how they might
write a match expression against a generated enum so their code is
forward-compatible. While it is explained in
smithy-lang/smithy-rs#627 (comment),
it can be helpful if the users can read it in rustdoc right below where
the enum's definition is shown.

* Make snippet in rustdoc text

This commit updates a rustdoc code snippet generated for an enum from
`rust,no_run` to `text`. We observed that the snippet fails to compile
in CI because the name of the enum was not fully qualified; code in
rustdoc is treated in a similar way that code in integration tests is
treated as being outside of a crate, but not part of the crate, which
means the name of the crate needs to be spelled out for a `use` statement
if we want to import the enum to the code in rustdoc. However, the name
of the crate is usually one-off, generated on the fly for testing, making
it not obvious to hard-code it or obtain it programmatically from within
Kotlin code.

* Suppress missing doc lint for UnknownVariantValue

This commit marks UnknownVariantValue as `#[allow(missing_docs)]` because
failing to do so will cause tests in CI to fail.

* Update codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/EnumGenerator.kt

Co-authored-by: Russell Cohen <[email protected]>

* Generate UnknownVariantValue via forInlineFun

This commit attempts to create UnknownVariantValue using
RuntimeType.forInlineFun. Prior to this commit, it was generated per enum
but that caused multiple definitions of UnknownVariantValue in a single
file as there can be multiple enums in the file.
By using RuntimeType.forInlineFun, we can generate a single UnknownVariantValue
per module and the enums within the module can refer to the same instance.

This commit, however, fails to pass the unit tests in EnumGeneratorTest.
The issue seems to be that a RustWriter used in the tests does not end up
calling the finalize method, failing to render inline functions.

* Replace "target == CodegenTarget.CLIENT" with a helper

This commit addresses smithy-lang/smithy-rs#1945 (comment)
but uses an existing helper CodegenTarget.renderUnknownVariant.

* Update EnumGeneratorTests to use TestWorkspace

This commit addresses failures for unit tests in EnumGeneratorTests.
Now that we use RuntimeType.forInlineFcn in EnumGenerator, we need
to ensure that the inline functions should be rendered to a Rust
source file during testing. RustWriter, used in EnumGeneratorTests
prior to this commit, was not capable of doing so. We thus update
EnumGeneratorTests to use TestWorkspace by which we ensure that the
inline functions are rendered during testing.

* Make sure to use the passed-in variable for shapeId

This commit fixes a copy-and-paste error introduced in 712c983. The
function should use the passed-in variable rather than the hard-coded
literal "test#SomeEnum".

* Address https://github.com/awslabs/smithy-rs/pull/1945\#discussion_r1014389852

* Update CHANGELOG.next.toml

* Update CHANGELOG.next.toml

This commit addresses smithy-lang/smithy-rs#1945 (comment)

* Avoid potential name collisions by UnknownVariantValue

This commit addresses smithy-lang/smithy-rs#1945 (comment).
It changes the module in which the `UnknownVariantValue` is rendered.
Before the commit, it was emitted to the `model` module but that might cause
name collisions in the future when a Smithy model has a shape named
`UnknownVariantValue`. After this commit, we render it to the `types` module.

* Move re-exports from lib.rs to types.rs

This commit moves re-export statements in client crates from `lib.rs` to
`types.rs`. The motivation is that now that we render the struct
`UnknownVariantValue` in a separate file for the `types` module, we can
no longer have a `pub mod types {...}` block in `lib.rs` as it cannot
coexist with `pub mod types;`.

* Add docs on UnknownVariantValue

This commit adds public docs on `UnknownVariantValue`. Since it is
rendered in `types.rs`, the users may not find the `Unknown` variant
of an enum in the same file and may wonder what this struct is for when
seeing it in isolation.

* Update CHANGELOG.next.toml

This commit addresses smithy-lang/smithy-rs#1945 (comment)

* Update the module documentation for types

This commit updates rustdoc for the types module to reflect that fact
that the module now contains not only re-exports but also an auxiliary
struct `UnknownVariantValue`.

* Add extensions to run code block depending on the target

This commit introduces extensions on CodegenTarget that execute the block
of code depending on the target is for client or for server. These
extensions are intended to replace if-else expressions throughout the
codebase explicitly checking whether the target is for client or for server.
We do not update such if-else expressions all at once in this commit.
Rather, we are planning to make incremental changes to update them
opportunistically.

* Update codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/EnumGenerator.kt

Co-authored-by: John DiSanti <[email protected]>

* Move extensions into CodegenTarget as methods

This commit addresses smithy-lang/smithy-rs#1945 (comment).
By moving extensions into the CodegenTarget enum, no separate import is
required to use them.

Co-authored-by: Saito <[email protected]>
Co-authored-by: Russell Cohen <[email protected]>
Co-authored-by: Yuki Saito <[email protected]>
Co-authored-by: John DiSanti <[email protected]>
  • Loading branch information
5 people authored and aws-sdk-rust-ci committed Dec 14, 2022
1 parent 7a6f6e7 commit f336e05
Show file tree
Hide file tree
Showing 924 changed files with 242,513 additions and 24,235 deletions.
8 changes: 2 additions & 6 deletions sdk/accessanalyzer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,10 @@ mod operation_ser;
pub mod output;
/// Paginators for the service
pub mod paginator;
/// Data primitives referenced by other data types.
pub mod types;
/// Crate version number.
pub static PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
/// Re-exported types from supporting crates.
pub mod types {
pub use aws_smithy_http::result::SdkError;
pub use aws_smithy_types::error::display::DisplayErrorContext;
pub use aws_smithy_types::DateTime;
}
static API_METADATA: aws_http::user_agent::ApiMetadata =
aws_http::user_agent::ApiMetadata::new("accessanalyzer", PKG_VERSION);
pub use aws_smithy_http::endpoint::Endpoint;
Expand Down
926 changes: 846 additions & 80 deletions sdk/accessanalyzer/src/model.rs

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions sdk/accessanalyzer/src/types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Code generated by software.amazon.smithy.rust.codegen.smithy-rs. DO NOT EDIT.
pub use aws_smithy_http::result::SdkError;
pub use aws_smithy_types::error::display::DisplayErrorContext;
pub use aws_smithy_types::DateTime;

/// Opaque struct used as inner data for the `Unknown` variant defined in enums in
/// the crate
///
/// While this is not intended to be used directly, it is marked as `pub` because it is
/// part of the enums that are public interface.
#[non_exhaustive]
#[derive(
std::clone::Clone,
std::cmp::Eq,
std::cmp::Ord,
std::cmp::PartialEq,
std::cmp::PartialOrd,
std::fmt::Debug,
std::hash::Hash,
)]
pub struct UnknownVariantValue(pub(crate) String);
impl UnknownVariantValue {
pub(crate) fn as_str(&self) -> &str {
&self.0
}
}
7 changes: 2 additions & 5 deletions sdk/account/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,10 @@ mod operation_deser;
mod operation_ser;
/// Output structures for operations.
pub mod output;
/// Data primitives referenced by other data types.
pub mod types;
/// Crate version number.
pub static PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
/// Re-exported types from supporting crates.
pub mod types {
pub use aws_smithy_http::result::SdkError;
pub use aws_smithy_types::error::display::DisplayErrorContext;
}
static API_METADATA: aws_http::user_agent::ApiMetadata =
aws_http::user_agent::ApiMetadata::new("account", PKG_VERSION);
pub use aws_smithy_http::endpoint::Endpoint;
Expand Down
46 changes: 42 additions & 4 deletions sdk/account/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,42 @@ impl ContactInformation {
}
}

/// When writing a match expression against `AlternateContactType`, it is important to ensure
/// your code is forward-compatible. That is, if a match arm handles a case for a
/// feature that is supported by the service but has not been represented as an enum
/// variant in a current version of SDK, your code should continue to work when you
/// upgrade SDK to a future version in which the enum does include a variant for that
/// feature.
///
/// Here is an example of how you can make a match expression forward-compatible:
///
/// ```text
/// # let alternatecontacttype = unimplemented!();
/// match alternatecontacttype {
/// AlternateContactType::Billing => { /* ... */ },
/// AlternateContactType::Operations => { /* ... */ },
/// AlternateContactType::Security => { /* ... */ },
/// other @ _ if other.as_str() == "NewFeature" => { /* handles a case for `NewFeature` */ },
/// _ => { /* ... */ },
/// }
/// ```
/// The above code demonstrates that when `alternatecontacttype` represents
/// `NewFeature`, the execution path will lead to the second last match arm,
/// even though the enum does not contain a variant `AlternateContactType::NewFeature`
/// in the current version of SDK. The reason is that the variable `other`,
/// created by the `@` operator, is bound to
/// `AlternateContactType::Unknown(UnknownVariantValue("NewFeature".to_owned()))`
/// and calling `as_str` on it yields `"NewFeature"`.
/// This match expression is forward-compatible when executed with a newer
/// version of SDK where the variant `AlternateContactType::NewFeature` is defined.
/// Specifically, when `alternatecontacttype` represents `NewFeature`,
/// the execution path will hit the second last match arm as before by virtue of
/// calling `as_str` on `AlternateContactType::NewFeature` also yielding `"NewFeature"`.
///
/// Explicitly matching on the `Unknown` variant should
/// be avoided for two reasons:
/// - The inner data `UnknownVariantValue` is opaque, and no further information can be extracted.
/// - It might inadvertently shadow other intended match arms.
#[allow(missing_docs)] // documentation missing in model
#[non_exhaustive]
#[derive(
Expand All @@ -308,16 +344,18 @@ pub enum AlternateContactType {
Operations,
#[allow(missing_docs)] // documentation missing in model
Security,
/// Unknown contains new variants that have been added since this code was generated.
Unknown(String),
/// `Unknown` contains new variants that have been added since this code was generated.
Unknown(crate::types::UnknownVariantValue),
}
impl std::convert::From<&str> for AlternateContactType {
fn from(s: &str) -> Self {
match s {
"BILLING" => AlternateContactType::Billing,
"OPERATIONS" => AlternateContactType::Operations,
"SECURITY" => AlternateContactType::Security,
other => AlternateContactType::Unknown(other.to_owned()),
other => {
AlternateContactType::Unknown(crate::types::UnknownVariantValue(other.to_owned()))
}
}
}
}
Expand All @@ -335,7 +373,7 @@ impl AlternateContactType {
AlternateContactType::Billing => "BILLING",
AlternateContactType::Operations => "OPERATIONS",
AlternateContactType::Security => "SECURITY",
AlternateContactType::Unknown(s) => s.as_ref(),
AlternateContactType::Unknown(value) => value.as_str(),
}
}
/// Returns all the `&str` values of the enum members.
Expand Down
25 changes: 25 additions & 0 deletions sdk/account/src/types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Code generated by software.amazon.smithy.rust.codegen.smithy-rs. DO NOT EDIT.
pub use aws_smithy_http::result::SdkError;
pub use aws_smithy_types::error::display::DisplayErrorContext;

/// Opaque struct used as inner data for the `Unknown` variant defined in enums in
/// the crate
///
/// While this is not intended to be used directly, it is marked as `pub` because it is
/// part of the enums that are public interface.
#[non_exhaustive]
#[derive(
std::clone::Clone,
std::cmp::Eq,
std::cmp::Ord,
std::cmp::PartialEq,
std::cmp::PartialOrd,
std::fmt::Debug,
std::hash::Hash,
)]
pub struct UnknownVariantValue(pub(crate) String);
impl UnknownVariantValue {
pub(crate) fn as_str(&self) -> &str {
&self.0
}
}
9 changes: 2 additions & 7 deletions sdk/acm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,10 @@ mod operation_ser;
pub mod output;
/// Paginators for the service
pub mod paginator;
/// Data primitives referenced by other data types.
pub mod types;
/// Crate version number.
pub static PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
/// Re-exported types from supporting crates.
pub mod types {
pub use aws_smithy_http::result::SdkError;
pub use aws_smithy_types::error::display::DisplayErrorContext;
pub use aws_smithy_types::Blob;
pub use aws_smithy_types::DateTime;
}
static API_METADATA: aws_http::user_agent::ApiMetadata =
aws_http::user_agent::ApiMetadata::new("acm", PKG_VERSION);
pub use aws_smithy_http::endpoint::Endpoint;
Expand Down
Loading

0 comments on commit f336e05

Please sign in to comment.