Backwards Compatibility
AWS SDKs require that clients can evolve in a backwards compatible way as new fields and operations are added. The types
generated by smithy-rs
are specifically designed to meet these requirements. Specifically, the following
transformations must not break compilation when upgrading to a new version:
- New operation added
- New member added to structure
- New union variant added
- New error added (todo)
- New enum variant added (todo)
However, the following changes are not backwards compatible:
- Error removed from operation.
In general, the best tool in Rust to solve these issues in the #[non_exhaustive]
attribute which will be explored in
detail below.
New Operation Added
Before
$version: "1"
namespace s3
service S3 {
operations: [GetObject]
}
After
$version: "1"
namespace s3
service S3 {
operations: [GetObject, PutObject]
}
Adding support for a new operation is backwards compatible because SDKs to not expose any sort of "service trait" that provides an interface over an entire service. This prevents clients from inheriting or implementing an interface that would be broken by the addition of a new operation.
New member added to structure
Summary
- Structures are marked
#[non_exhaustive]
- Structures must be instantiated using builders
- Structures must not derive
Default
in the event that required fields are added in the future.
In general, adding a new public
member to a structure in Rust is not backwards compatible. However, by applying
the #[non_exhaustive]
to the structures generated by the Rust SDK, the Rust compiler will prevent users from using our
structs in ways that prevent new fields from being added in the future. Note: in this context, the optionality of
the fields is irrelevant.
Specifically, #[non_exhaustive]
prohibits the
following patterns:
-
Direct structure instantiation:
fn foo() { let ip_addr = IpAddress { addr: "192.168.1.1" }; }
If a new member
is_local: boolean
was added to the IpAddress structure, this code would not compile. To enable users to still construct our structures while maintaining backwards compatibility, all structures expose a builder, accessible atSomeStruct::Builder
:fn foo() { let ip_addr = IpAddress::builder().addr("192.168.1.1").build(); }
-
Structure destructuring:
fn foo() { let IpAddress { addr } = some_ip_addr(); }
This will also fail to compile if a new member is added, however, by adding
#[non_exhaustive]
, the..
multifield wildcard MUST be added to support new fields being added in the future:fn foo() { let IpAddress { addr, .. } = some_ip_addr(); }
Validation & Required Members
Adding a required member to a structure is not considered backwards compatible. When a required member is added to a structure:
- The builder will change to become fallible, meaning that instead of returning
T
it will returnResult<T, BuildError>
. - Previous builder invocations that did not set the new field will still stop compiling if this was the first required field.
- Previous builder invocations will now return a
BuildError
because the required field is unset.
New union variant added
Similar to structures, #[non_exhaustive]
also applies to unions. In order to allow new union variants to be added in
the future, all unions (enum
in Rust) generated by the Rust SDK must be marked with #[non_exhaustive]
. Note:
because new fields cannot be added to union variants, the union variants themselves do not need
to be #[non_exhaustive]
. To support new variants from services, each union contains an Unknown
variant. By
marking Unknown
as non_exhaustive, we prevent customers from instantiating it directly.
#[non_exhaustive]
#[derive(std::clone::Clone, std::cmp::PartialEq, std::fmt::Debug)]
pub enum AttributeValue {
B(aws_smithy_types::Blob),
Bool(bool),
Bs(std::vec::Vec<aws_smithy_types::Blob>),
L(std::vec::Vec<crate::model::AttributeValue>),
M(std::collections::HashMap<std::string::String, crate::model::AttributeValue>),
N(std::string::String),
Ns(std::vec::Vec<std::string::String>),
Null(bool),
S(std::string::String),
Ss(std::vec::Vec<std::string::String>),
// By marking `Unknown` as non_exhaustive, we prevent client code from instantiating it directly.
#[non_exhaustive]
Unknown,
}