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:

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:

  1. 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 at SomeStruct::Builder:

    fn foo() {
    let ip_addr = IpAddress::builder().addr("192.168.1.1").build();
    }
  2. 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:

  1. The builder will change to become fallible, meaning that instead of returning T it will return Result<T, BuildError>.
  2. Previous builder invocations that did not set the new field will still stop compiling if this was the first required field.
  3. 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,
}