Introduction

The Rust guidelines are for the benefit of client library designers targeting service applications written in Rust. You do not have to write a client library for Rust if your service is not normally accessed from Rust.

Design Principles

The Azure SDK should be designed to enhance the productivity of developers connecting to Azure services. Other qualities (such as completeness, extensibility, and performance) are important but secondary. Productivity is achieved by adhering to the principles described below:

Idiomatic

  • The SDK should follow the general design guidelines and conventions for the Rust language. It should feel natural to a developer in the Rust language.
  • We embrace the ecosystem with its strengths and its flaws.
  • We work with the ecosystem to improve it for all developers.

Consistent

  • Client libraries should be consistent within the language, consistent with the service and consistent between all target languages. In cases of conflict, consistency within the language is the highest priority and consistency between all target languages is the lowest priority.
  • Service-agnostic concepts such as logging, HTTP communication, and error handling should be consistent. The developer should not have to relearn service-agnostic concepts as they move between client libraries.
  • Consistency of terminology between the client library and the service is a good thing that aids in diagnostics.
  • All differences between the service and client library must have a good (articulated) reason for existing, rooted in idiomatic usage rather than whim.
  • The Azure SDK for each target language feels like a single product developed by a single team.
  • There should be feature parity across target languages. This is more important than feature parity with the service.

Approachable

  • We are experts in the supported technologies so our customers, the developers, don’t have to be.
  • Developers should find great documentation (hero tutorial, how to articles, samples, and API documentation) that makes it easy to be successful with the Azure service.
  • Getting off the ground should be easy through the use of predictable defaults that implement best practices. Think about progressive concept disclosure.
  • The SDK should be easily acquired through the most normal mechanisms in the target language and ecosystem.
  • Developers can be overwhelmed when learning new service concepts. The core use cases should be discoverable.

Diagnosable

  • The developer should be able to understand what is going on.
  • It should be discoverable when and under what circumstances a network call is made.
  • Defaults are discoverable and their intent is clear.
  • Logging, tracing, and exception handling are fundamental and should be thoughtful.
  • Error messages should be concise, correlated with the service, actionable, and human readable. Ideally, the error message should lead the consumer to a useful action that they can take.
  • Integrating with the preferred debugger for the target language should be easy.

Dependable

  • Breaking changes are more harmful to a user’s experience than most new features and improvements are beneficial.
  • Incompatibilities should never be introduced deliberately without thorough review and very strong justification.
  • Do not rely on dependencies that can force our hand on compatibility.

General Guidelines

DO follow the General Azure SDK Guidelines.

DO use azure_core::Pipeline to implement all methods that call Azure REST services.

DO write idiomatic Rust code. If you’re not familiar with the language, a great place to start is https://www.rust-lang.org/learn. Do NOT simply attempt to translate your language of choice into Rust.

⛔️ DO NOT use grammar or features newer than the rust-version declared in the root Cargo.toml workspace.

⛔️ DO NOT unconditionally depend on any particular async runtime or HTTP stack.

☑️ YOU SHOULD depend on tokio (async runtime) and reqwest (HTTP stack) in the default feature for crates e.g.:

[dependencies]
reqwest = { workspace = true, optional = true }

[features]
default = [ "reqwest" ]
reqwest = [ "dep:reqwest" ]

Default features can be ignored by consumers and individual features enabled as needed. This allows consumers to ignore default features and use their own HTTP stack and/or async runtime to implement a client.

⛔️ DO NOT call unwrap(), expect(), or other functions that may panic unless you are absolutely sure they never will. It’s almost always better to use map(), unwrap_or_else(), or a myriad of related functions to remap errors, return suitable defaults, etc.

⛔️ DO NOT define a prelude module.

These may lead to name collisions, especially when multiple versions of a crate are imported.

Support for non-HTTP Protocols

This document contains guidelines developed primarily for typical Azure REST services i.e., stateless services with request-response based interaction model. Many of the guidelines in this document are more broadly applicable, but some might be specific to such REST services.

Azure SDK API Design

The API surface of your client library must have the most thought as it is the primary interaction that the consumer has with your service.

DO use clear, concise, and meaningful names.

DO follow Rust naming conventions.

⚠️ YOU SHOULD NOT use abbreviations unless necessary or when they are commonly used and understood. For example, iot is used since it is a commonly understood industry term; however, using kv for Key Vault would not be allowed since kv is not commonly used to refer to Key Vault.

With mixed casing like “IoT”, consider the following guidelines:

  • For module and method names, always use lowercase e.g., get_iot_device().
  • For type names, use PascalCase e.g., IotClient.

DO consult the Architecture Board if you wish to use a dependency that is not on the list of centrally managed dependencies.

Service Client

Service clients are the main starting points for developers calling Azure services with the Azure SDK. Each client library should have at least one client in its main namespace, so it’s easy to discover. The guidelines in this section describe patterns for the design of a service client.

There exists a distinction that must be made clear with service clients: not all classes that perform HTTP (or otherwise) requests to a service are automatically designated as a service client. A service client designation is only applied to classes that are able to be directly constructed because they are uniquely represented on the service. Additionally, a service client designation is only applied if there is a specific scenario that applies where the direct creation of the client is appropriate. If a resource can not be uniquely identified or there is no need for direct creation of the type, then the service client designation should not apply.

DO name service client types with the Client suffix e.g., SecretClient.

Client names are specific to the service to avoid ambiguity when using multiple clients without requiring as to change the binding name when importing e.g.,

error[E0252]: the name `Client` is defined multiple times
  --> src/main.rs:2:38
   |
1  |     use azure_storage_blob::Client;
   |         ----------- previous import of the type `Client` here
2  |     use azure_security_keyvault::secrets::Client;
   |         ^^^^^^^^^^^ `Client` reimported here
   |

☑️ YOU SHOULD place service client types that the consumer is most likely to interact with in the root module of the client library e.g., azure_security_keyvault. Specialized service clients should be placed in submodules e.g., azure_security_keyvault::secrets.

DO ensure that all service client methods are thread safe (usually by making them immutable and stateless).

DO define a public endpoint(&self) -> &azure_core::Url method to get the endpoint used to create the client.

Service Client Constructors

DO define a public function new that takes the following form and returns Self or azure_core::Result<Self> if the function may fail.

impl SecretClient {
    pub fn new(endpoint: impl AsRef<str>, credential: std::sync::Arc<dyn azure_core::TokenCredential>, options: Option<SecretClientOptions>) -> azure_core::Result<Self> {
        let endpoint = azure_core::Url::parse(endpoint.as_ref())?;
        let options = options.unwrap_or_default();

        todo!()
    }
}

✔️ YOU MAY accept a different credential type if the service does not support AAD authentication.

DO define a new function that takes a TokenCredential and a with_{credential_type} function e.g., with_key_credential if a client supports both AAD authentication and other token credentials that do not implement TokenCredential.

In cases when different credential types are supported, we want the primary use case to support AAD authentication over other authentication schemes.

Client Configuration

DO name the client options struct with the same as the client name + “Options” e.g., a SecretClient takes a SecretClientOptions.

DO define a pub api_version: String field to pass to the service for the HTTP client.

DO define a pub options: azure_core::ClientOptions field on client-specific options to define global configuration shared by any HTTP client.

DO derive Clone to support cloning client configuration for other clients.

DO derive Default to support creating default client configuration including the default api-version used when calling into the service.

The requirements above would define an example client options struct like:

#[derive(Clone, Default)]
pub struct SecretClientOptions {
    pub api_version: String,
    pub options: azure_core::ClientOptions,
}

⛔️ DO NOT use client library-specific runtime configuration such as environment variables or configuration files. Some environments e.g., WASM or many IoT devices won’t have access to an environment block or file system.

⛔️ DO NOT change the default values of the client options based on system or program state.

⛔️ DO NOT change the default values of the client options based on how the client library was built.

⛔️ DO NOT change the behavior of the client after the client is constructed with the following exceptions:

  • Log level, which must take effect immediately across all client libraries.
  • Tracing on or off, which must take effect immediately across all client libraries.
Service Versions

DO call the latest supported service API version by default. Typically this will be the API version from which the client library was generated.

DO allow the consumer to explicitly set a service API version when instantiating the service client.

Mocking

DO define a trait named after the client name + “Methods” e.g., SecretClientMethods.

DO implement all methods of the client methods trait on the client which have the body unimplemented!() or std::future::ready(unimplemented!()) for async methods e.g.,

pub trait SecretClientMethods {
    fn endpoint(&self) -> &Url {
        unimplemented!()
    }

    async fn set_secret(
        &self,
        _name: impl Into<String>,
        _version: impl Into<String>,
        _options: Option<SetSecretOptions>,
        context: Option<&Context>,
    ) -> azure_core::Result<Response> {
        std::future::ready(unimplemented!())
    }
}

pub struct SecretClient {
    // ...
}

impl SecretClient {
    // pub fn new(..) -> Result<Self>
}

impl SecretClientMethods for SecretClient {
    fn endpoint(&self) -> &Url {
        todo!()
    }

    async fn set_secret(
        &self,
        name: impl Into<String>,
        version: impl Into<String>,
        options: Option<SetSecretOptions>,
        context: Option<&Context>,
    ) -> azure_core::Result<Response> {
        todo!()
    }
}

Service Methods

Service methods are the methods on the client that invoke operations on the service and will follow the form:

async fn method_name(
    &self,
    mandatory_param1: impl Into<P1>,
    mandatory_param2: impl Into<P2>,
    content: RequestContent<T>, // elided if no body is defined
    options: Option<MethodNameOptions>,
    context: Option<&Context>,
) -> azure_core::Result<Response>;
Sync and Async

The Rust SDK is designed for asynchronous API calls. Customers who need synchronous calls may use something like futures::executor::block_on to wait synchronously on a Future.

DO provide an asynchronous programming model for service methods.

⛔️ DO NOT provide a synchronous programming model for service methods.

Naming

DO use snake_case method names converted from likely either camelCase or PascalCase declared in the service specification e.g., getResource would be declared as get_resource.

DO use the list_ prefix for service methods that return one or more pages containing a list of resources e.g., list_properties_of_secrets().

DO use the following prefixes in the described scenarios:

Prefix Scenario Example
(none) field getter field_name(&self) -> FieldType
with_ field setter returning Self - typically used in builders with_field_name(&mut self, value: FieldType) -> &mut Self
set_ field setter returning nothing or anything else set_field_name(&mut self, value: FieldType)
Operation Options

DO separate the Context containing client method options from service method options. See example above.

The azure_core::Pipeline is constructed when the service client constructed, and because the clients must be immutable the pipeline cannot be altered directly. Any data passed to client methods to alter the pipeline e.g., retry policy options, must be passed to pipeline policies via the Context.

DO pass pipeline policy options in the Context passed to each pipeline policy.

DO clone the azure_core::Pipeline if the list of pipeline policies is altered by a client method e.g., a custom pipeline policy is added per-call.

✔️ YOU MAY allow the caller to change the API version e.g., api-version when calling the endpoint.

Return Types

DO return an azure_core::Result<azure_core::Pager<T>> from an async fn when the service returns a pageable response.

DO return an azure_core::Result<azure_core::Poller<T>> from an async fn when the service implements the operation a long-running operation.

DO return an azure_core::Result<azure_core::Response<T>> from an async fn for all other service responses.

DO provide the status code, headers, and self-consuming async raw response stream from all return types e.g.,

impl<T> Response<T> {
    pub fn status(&self) -> &StatusCode {
        todo!()
    }

    pub fn headers(&self) -> &Headers {
        todo!()
    }

    pub async fn into_content(self) -> ResponseContent {
        todo!()
    }
}

This is equivalent to returning an impl Future<Output = azure_core::Result<azure_core::Response<T>>> from an fn.

Cancellation

Cancelling an asynchronous method call in Rust is done by dropping the Future.

The Rust std crate itself does not implement an async runtime, so different async runtimes must be chosen by the caller and may support cancellation different. tokio is common and should be the default, but not required. Various extensions also exist that the caller may use that may otherwise not work with passing in a cancellation token like in some other Azure SDK languages.

Service Method Parameters

DO take a &self as the first parameter. All service clients must be immutable

DO declare parameter types as impl Into<T> where T is a common std type that implements Into<T> e.g., String when the parameter data will be owned.

This will be most common when the data passed to a function will be stored in a struct e.g.:

pub struct SecretClientOptions {
    api_version: String,
}

impl SecretClientOptions {
    pub fn new(api_version: impl Into<String>) -> Self {
        Self {
            api_version: api_version.into(),
        }
    }
}

This allows callers to pass a String or str e.g., SecretClientOptions::new("7.4").

DO declare parameter types as impl AsRef<T> where T is a common std reference type that implements AsRef<T> e.g., str, when the parameter data is merely borrowed.

This is useful when the parameter data is temporary, such as allowing a str endpoint to be passed that will be parsed into an azure_core::Url e.g.:

impl SecretClient {
    pub fn new(endpoint: impl AsRef<str>) -> Result<Self> {
        let endpoint = azure_core::Url::parse(endpoint.as_ref())?;

        todo!()
    }
}

The endpoint parameter is never saved so a reference is fine. This also allows callers to pass a String or str e.g., SecretClient::new("https://myvault.vault.azure.net").

DO declare a parameter named content of type RequestContent<T>, where T is the service-defined request model.

DO support converting to a RequestContent<T> from a T, Stream, or AsRef<str>.

✔️ YOU MAY use interior mutability e.g., std::sync::OnceLock for single-resource caching e.g., a single key-specific CryptographyClient that attempts to download the public key material for subsequent public key operations.

Parameter Validation

The service client will have several methods that perform requests on the service. Service parameters are directly passed across the wire to an Azure service. Client parameters are not passed directly to the service, but used within the client library to fulfill the request. Examples of client parameters include values that are used to construct a URI or a file that needs to be uploaded to storage.

DO validate client parameters.

⛔️ DO NOT validate service parameters. This includes null checks, empty strings, and other common validating conditions. Let the service validate any request parameters.

⛔️ DO NOT encode service parameter default values. These values may change from version to version and may cause unexpected results when calling older versions from a newer client. Let the service apply default parameter values.

DO validate the developer experience when the service parameters are invalid to ensure appropriate error messages are generated by the service. If the developer experience is compromised due to service-side error messages, work with the service team to correct prior to release.

Methods Returning Collections (Paging)

Rust is a lower-level language but often provides higher-level, zero-cost abstractions such as iterators. Iterators are an idiomatic way to enumerate vectors or streams such as futures::Stream.

DO return an azure_core::Pager<T> from pageable service client methods where T is model type being enumerated.

DO implement futures::Stream for azure_core::Pager<T>. This defines a poll_next() method that returns an Option<T> that returns None when the consumer has reached the end of the result set. This will enumerate all items for all pages.

DO implement an to_page(&self) -> &azure_core::Page<T> for azure_core::Pager<T> that returns the current page of items.

DO implement IntoIterator on azure_core::Page<T>. This allows customers to enumerate each page separately, and to enumerate each page of items therein.

DO implement Iterator::size_hint() on the returned IntoIterator for azure_core::Page<T>.

DO support reconstructing an azure_core::Pager<T> so that a caller can start paging from a previous state.

Methods Invoking Long-running Operations

Some service operations, known as Long-running Operations or LROs take a long time to execute - up to hours or even days. Such operations do not return their result immediately but rather are started and their progressed polled until the operation reaches a terminal state including Succeeded, Failed, or Canceled.

The azure_core crate exposes an abstract type called azure_core::Poller<T>, which represents LROs and supports operations for polling and waiting for status changes, and retrieving the final operation result. A service method invoking a long-running operation will return an azure_core::Poller<T> as described below.

DO name all methods that start an LRO with the begin_ prefix.

DO implement futures::Stream for azure_core::Poller<T>. This defines a poll_next() method that returns an Option<T> that returns None when the polling has terminated.

DO support reconstructing an azure_core::Poller<T> so that a caller can start polling from a previous state.

Conditional Request Methods

DO define ETag-related options e.g., if_match, if_none_match, etc., in the service method options e.g.:

#[derive(Clone, Debug)]
pub struct SetSecretOptions {
    enabled: Option<bool>,
    if_match: Option<azure_core::ETag>,
}
Hierarchical Clients

✔️ YOU MAY return clients from other clients e.g., a DatabaseClient from a CosmosClient.

DO name all client methods returning a client with the _client suffix e.g., CosmosClient::database_client().

⛔️ DO NOT define client methods returning a client as asynchronous.

DO clone the parent client azure_core::Pipeline so that lifetime parameters and guarantees are not required.

Supporting Types

In addition to service client types, Azure SDK APIs provide and use other supporting types as well.

Model Types

This section describes guidelines for the design model types and all their transitive closure of public dependencies (i.e. the model graph). A model type is a representation of a REST service’s resource.

DO derive or implement Clone and Default for all model structs.

DO derive or implement serde::Serialize and/or serde::Deserialize as appropriate i.e., if the model is input (serializable), output (deserializable), or both.

DO define all fields as pub.

DO define all fields using Option<T>.

Though uncommon, service definitions do not always match the service implementation when it comes to required fields. Upon the recommendation of the Breaking Change Reviewers, the specification is often changed to reflect the service if the service has already been deployed.

DO attribute fields with #[serde(skip_serializing_if = "Option::is_none")] unless an explicit null must be serialized.

DO attribute model structs with #[non_exhaustive].

This forces all downstream crates, for example, to use the .. operator to match any remaining fields that may be added in the future for pattern binding:

// struct Example {
//     pub foo: Option<String>,
//     pub bar: Option<i32>,
// }

let { foo, bar, .. } = client.method().await?.try_into()?;

See RFC 2008 for more information.

Model Type Naming

DO define models using the names from TypeSpec unless those names conflict with keywords or common types from std, futures, or other common dependencies.

If name collisions are likely and the TypeSpec cannot be changed, you can either use the @clientName TypeSpec decorator or update a client .tsp file.

DO define model fields using “camelCase”.

To facilitate this, attribute the model type:

#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Example {
    pub compound_name: String, // -> "compoundName"
}

⛔️ DO NOT rename fields using the #[serde] attribute or by other means. All model changes must only be done in TypeSpec.

Builders

Builders are an idiomatic pattern in Rust, such as the typestate builder pattern that can help guide developers into constructing a valid type variants.

✔️ YOU MAY implement builders for special cases e.g., URI builders.

✔️ YOU MAY borrow or consume self in with_ setter methods.

DO return an owned value from the final build() method.

Enumerations

DO implement all enumeration variations as PascalCase.

DO derive or implement Clone and Debug for all enums.

DO derive Copy for all fixed enums.

DO derive or implement serde::Serialize and/or serde::Deserialize as appropriate i.e., if the enum is used in input (serializable), output (deserializable), or both.

DO attribute all enums with #[non_exhaustive].

This forces all downstream crates, for example, to use the _ match arm to match any remaining enums that may be added in the future for pattern binding:

// enum Example {
//     Foo,
//     Bar,
// }

let value = match model.kind {
    Example:Foo => todo!(),
    Example::Bar => todo!(),
    _ => todo!(),
};

This is necessary for both fixed enums and extensible enums since new variants may be added and matched during deserialization.

See RFC 2008 for more information.

DO implement all fixed enumerations using only defined variants:

#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub enum FixedEnum {
    #[serde(rename = "foo")]
    Foo,
    #[serde(rename = "bar")]
    Bar,
}

DO implement all extensible enumerations - those which may take a variant that is not defined - using defined variants and an untagged UnknownValue:

#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub enum ExtensibleEnum {
    #[serde(rename = "foo")]
    Foo,
    #[serde(rename = "bar")]
    Bar,
    #[serde(untagged)]
    UnknownValue(String),
}

✔️ YOU MAY implement serde::Deserialize, serde::Serialize, or both as appropriate depending on whether the enumeration is found only in responses, requests, or both, respectively.

☑️ YOU SHOULD define variant attribute #[serde(rename = "name")] for generated code for each variant.

✔️ YOU MAY use container attribute #[serde(rename_all = "camelCase")] for convenience layers, or whatever casing is appropriate.

Using Azure Core Types

The azure_core package provides common functionality for client libraries. Documentation and usage examples can be found in the Azure/azure-sdk-for-rust repository.

Errors

DO return an azure_core::Result<T> which uses azure_core::Error.

DO call appropriate methods on azure_core::Error to wrap or otherwise convert to an appropriate azure_core::ErrorKind.

✔️ YOU MAY implement Into<azure_core::Error> for any other error type returned by functions you call if not already defined to support the ? operator.

Since your crate will define neither azure_core::Error or likely the error being returned to you from another dependency, you will need to use the newtype idiom e.g.:

#[derive(Debug)]
pub struct Error(dependency::Error);

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        self.0.source()
    }
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(f)
    }
}

impl Into<azure_core::Error> for Error {
    fn into(self) -> azure_core::Error {
        azure_core::Error::new(azure_core::ErrorKind::Other, Box::new(self))
    }
}

Authentication

Azure services use a variety of different authentication schemes to allow clients to access the service. Conceptually, there are two entities responsible in this process: a credential and an authentication policy. Credentials provide confidential authentication data. Authentication policies use the data provided by a credential to authenticate requests to the service.

DO support all authentication schemes supported by the service and implemented in azure_core and azure_identity.

☑️ YOU SHOULD use only credentials and authentication policies defined in azure_core.

Crates support different types of dependencies e.g., [dependencies] for code linked into applications and [dev-dependencies] used for tests, examples, etc.

⛔️ DO NOT take a dependency on azure_identity.

✔️ YOU MAY take a development dependency on azure_identity for tests and examples.

DO provide credential types that can be used to fetch all data needed to authenticate a request to the service in a non-blocking atomic manner for each authentication scheme that does not have an implementation in azure_core.

DO provide service client constructors or factories that accept any supported authentication credentials.

⚠️ YOU SHOULD NOT support providing credential data via a connection string. Connection string interfaces should be provided ONLY IF the service provides a connection string to users via the Portal or other tooling. Use a with_connection_string() function to construct a client in that case e.g.:

impl ExampleClient {
    pub fn with_connection_string(connection_string: impl Into<String>, options: Option<ExampleClientOptions>) {
        todo!()
    }
}

When implementing authentication, don’t open up the consumer to security holes like PII (personally identifiable information) leakage or credential leakage. Credentials are generally issued with a time limit and must be refreshed periodically to ensure that the service connection continues to function as expected. Ensure your client library follows all current security recommendations and consider an independent security review of the client library to ensure you’re not introducing potential security problems for the consumer.

⛔️ DO NOT persist, cache, or reuse security credentials. Security credentials should be considered short lived to cover both security concerns and credential refresh situations.

If your service implements a non-standard credential system - one not supported by azure_core - then you need to implement an authentication policy for the HTTP pipeline that can authenticate requests given the alternative credential types provided by the client library.

✔️ YOU MAY implement an authentication policy and credential in a client library crate if the authentication scheme is supported only by the service.

DO securely free and zero authentication tokens and other credential data as soon as they are no longer needed.

Namespaces

DO use namespaces as defined by TypeSpecs for the service using all lowercase characters and underscores:

namespace Azure.Security.KeyVault;
// crate: azure_security_keyvault
namespace Azure.Security.KeyVault.Secrets {
    // module azure_security_keyvault::secrets
}

⚠️ YOU SHOULD NOT export modules used only within the crate. You may use pub(crate) when declaring these modules to export public types within that module to other types and functions within the crate:

// lib.rs
pub(crate) mod helpers;

// helpers.rs
pub fn helper() {} // not exported publicly

Support for Mocking

In addition to mocking clients:

DO declare all model fields public.

DO define a from() method for all helper types like pageables and LROs that allow callers to return those types from client mocks.

Azure SDK Library Design

Packaging

Packages in Rust are called “crates”. Crate names follow the same general guidance on namespaces using underscores as a separator e.g., azure_core, azure_security_keyvault, etc.

DO start the crate name with azure_ for data plane crates or azure_resourcemanager_ for control plane (ARM) crates.

TODO: Now that RFC 3243 is merged, having already-reserved azure_mgmt_* crates matters less; however, we should revisit using “mgmt” if the RFC hasn’t been implemented by the time we need it.

DO construct the crate name with all lowercase characters and underscores in the form azure_<group>_<service>. Uppercase characters and dashes are not allowed. For example, azure_security_keyvault.

Rust does support dashes in crate names, but it may create confusion with customers to reference a crate like azure-core then import a module like azure_core. Many older crates do this, but the trend has been to use underscores in both cases to avoid confusion.

DO register the chosen crate name with the Architecture Board. Open an issue to request the crate name. See the registered package list for a list of the currently registered packages.

DO package different endpoints within a service that version together in a single crate but under separate modules as needed.

Using Key Vault as an example and comparing with other languages like .NET:

  • Azure.Security.KeyVault.Secrets -> azure_security_keyvault::secrets
  • Azure.Security.KeyVault.Keys -> azure_security_keyvault::keys
  • Azure.Security.KeyVault.Certificates -> azure_security_keyvault::certificates

This makes efficient use of generated client code for each services’ TypeSpec or OpenAPI specification in a statically-compiled language like Rust.

⛔️ DO NOT package multiple service specifications that version independently within the same crate.

Directory Structure

DO place all crates’ source under the sdk/ root directory using an appropriate service directory name e.g., sdk/keyvault. The service directory name will often match what is in the Azure/azure-rest-api-specs repository and will most often be the same across Azure SDK languages.

DO put all crate source within the service directory e.g., sdk/keyvault/Cargo.toml.

If you have cause to release separate crates for a single service, please discuss first with the Architecture Board.

☑️ YOU SHOULD only export public APIs from the crate lib.rs and define all other types in suitable modules:

  • Single-file modules should be declared in a file next to their parent module.
  • Multi-file modules should be declared in a directory next to their parent module with a mod.rs file.

For example:

src/
  policies/
    client_id.rs
    mod.rs
    retry.rs
    transport.rs
  error.rs
  lib.rs
Cargo.lock
Cargo.toml

Common Libraries

DO review new macros with your language architect(s).

✔️ YOU MAY use common procedural macros from azure_core.

Versioning

Client Versions

DO be 100% backwards compatible with older versions of the same package.

DO increase the major semantic version number if an API breaking change is required.

See https://semver.org for more information.

DO discuss breaking changes with the language architect before making changes.

Note there are different types of breaking changes:

  1. The service introduced breaking changes that the client library must reflect in code. Approval may still be required, but should not burden code owner(s).
  2. The client library introduced breaking changes for good reason.

Breaking changes introduced by the client library should happen rarely, if ever. Register your intent to make client breaking changes with the Architecture Board.

Package Version Numbers

Consistent version number scheme allows consumers to determine what to expect from a new version of the library.

DO use MAJOR.MINOR.PATCH format for the version of the crate.

Use -beta._N suffix for beta package versions. For example, 1.0.0-beta.2.

See https://semver.org for more information.

DO change the version number of the client library when ANYTHING changes in the client library.

DO increment the patch version when fixing a bug.

⛔️ DO NOT include new APIs in a patch release.

DO increment the major or minor version when adding support for a service API version.

DO increment the major or minor version when adding a new method to the public API.

☑️ YOU SHOULD increment the major version when making large feature changes.

Dependencies

Dependencies bring in many considerations that are often easily avoided by avoiding the dependency.

  • Versioning - Many programming languages do not allow a consumer to load multiple versions of the same package. So, if we have an client library that requires v3 of package Foo and the consumer wants to use v5 of package Foo, then the consumer cannot build their application. This means that client libraries should not have dependencies by default.
  • Size - Consumer applications must be able to deploy as fast as possible into the cloud and move in various ways across networks. Removing additional code (like dependencies) improves deployment performance.
  • Licensing - You must be conscious of the licensing restrictions of a dependency and often provide proper attribution and notices when using them.
  • Compatibility - Often times you do not control a dependency and it may choose to evolve in a direction that is incompatible with your original use.
  • Security - If a security vulnerability is discovered in a dependency, it may be difficult or time consuming to get the vulnerability corrected if Microsoft does not control the dependencies code base.

DO declare all dependencies in the repository root Cargo.toml workspace in the [dependencies] section regardless of which type of dependency crates will inherit, e.g.:

[workspace.dependencies]
azure_core = { version = "0.1.0", path = "sdk/core" }
futures = "0.3.30"
tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread"] }

DO inherit all dependencies from the workspace in individual creates’ Cargo.toml files e.g.:

[dependencies]
azure_core = { workspace = true }
futures = { workspace = true }

[dev-dependencies]
tokio = { workspace = true }

✔️ YOU MAY override the features required for a crate.

Code Lints

DO centralized general linting rules, whether allowed or denied, into the root workspace Cargo.toml e.g.:

[workspace.lints.rust]
dead_code = "allow"

[workspace.lints.clippy]

DO inherit linting rules from the workspace in each member crate e.g., :

[lints]
workspace = true

✔️ YOU MAY define source-specific lint rules in .rs source files if they can’t be mitigated.

⛔️ DO NOT define crate-specific lint rules in Cargo.toml files since these will apply to all source and should not be so pervasive.

Documentation Comments

Documentation comments in Rust not only support markdown, but can contain examples that are optionally runnable as tests when executing cargo test. Read the rustdoc book for more information, especially about tests in doc comments.

DO document all public APIs prior to General Availability (GA). This includes functions, structs, methods, fields, and traits, e.g.:

/// A secret stored in Key Vault.
pub struct Secret {
    /// The name of the secret.
    pub name: String,

    // ...
}

DO include the crate README.md in the root lib.rs to provide an overview of the crate in rendered documentation e.g.:

// near the top of lib.rs:
#![doc = include_str!("../README.md")]

This will impact line numbers, so you should only export APIs publicly from lib.rs.

DO document all parameters. Prior to conventional doc comment markdown headers, declare an Arguments heading as needed (not needed for &self):

/// Sets a secret and returns more information about the secret from the service.
///
/// # Arguments
///
/// * `name` - The name of the secret.
/// * `value` - The value of the secret.
/// * `options` - Optional properties of the secret.
/// * `context` - Optional context to pass to the client.
async fn set_secret(
    &self,
    name: impl Into<String>,
    value: impl Into<String>,
    options: Option<SetSecretOptions>,
    context: Option<&Context>,
) -> Result<Response>;

See Rust by Example: Documentation for more information.

☑️ YOU SHOULD use testable examples in documentation which improve test coverage and show callers runnable examples.

✔️ YOU MAY use expect(&str) to unwrap a value or panic with an explanation useful to consumers only in doc comments.

Repository Guidelines

DO locate all source code and READMEs in the [Azure/azure-sdk-for-rust] GitHub repository.

DO follow Azure SDK engineering systems guidelines for working in the [Azure/azure-sdk-for-rust] GitHub repository.

DO commit Cargo.lock to the repository.

Documentation Style

There are several documentation deliverables that must be included in or as a companion to your client library. Beyond complete and helpful API documentation within the code itself (doc comments), you need a great README and other supporting documentation.

  • README.md - Resides in the root of your library’s directory within the SDK repository; includes package installation and client library usage information.
  • API reference - Generated from the doc comments in your code; published on https://learn.microsoft.com and https://docs.rs.
  • Code snippets - Short code examples that demonstrate single (atomic) operations for the champion scenarios you’ve identified for your library; included in your README, doc comments, and Quickstart.
  • Quickstart - Article on https://learn.microsoft.com that is similar to but expands on the README content; typically written by your service’s content developer.
  • Conceptual - Long-form documentation like Quickstarts, Tutorials, How-to guides, and other content on docs.microsoft.com; typically written by your service’s content developer.

DO include your service’s content developer in the Architecture Board review for your library. To find the content developer you should work with, check with your team’s Program Manager.

DO follow the Azure SDK Contributors Guide. (MICROSOFT INTERNAL)

DO adhere to the specifications set forth in the Microsoft style guides when you write public-facing documentation. This applies to both long-form documentation like a README and the doc comments in your code. (MICROSOFT INTERNAL)

☑️ YOU SHOULD attempt to document your library into silence. Preempt developers’ usage questions and minimize GitHub issues by clearly explaining your API in the doc comments. Include information on service limits and errors they might hit, and how to avoid and recover from those errors.

As you write your code, document it so you never hear about it again. The less questions you have to answer about your client library, the more time you have to build new features for your service.

Code Snippets

☑️ YOU SHOULD include runnable examples in documentation for client methods or convenience layers that may require additional explanation specific to those members.

DO add no_run to the code fence for documentation examples if that code requirements external resources.

☑️ YOU SHOULD use unwrap() or expect(&str) in examples and not the question mark operator ?, which requires additional setup.

⚠️ YOU SHOULD NOT include the main function in the signature, if even necessary e.g., for showing async examples.

/// ``` no_run
/// # async fn main() {
///     let client = SecretClient::new("https://myvault.vault.azure.net", Arc::new(DefaultAzureCredential::default()), None).unwrap();
///     let secret = client.set_secret("name", "value", None, None).await.unwrap();
///     println!("{secret:?}");
/// # }
/// ```

DO attribute code fences with no_run if the code cannot or should not run when running cargo test. There are additional documentation test attributes that may be of interest.

Build System Integration

DO test all crates impacted by a Pull Request (PR) using the minimum supported Rust version (MSRV) from the stable channel i.e., azure_core’s rust-version in its Cargo.toml.

DO test all crates impacted by a PR using the latest nightly toolchain.

DO test azure_core and any other crates that implement async functions separate from azure_core::Pipeline using tokio and monoio in both single- and multi-threaded configurations. These tests do not necessarily have to run for every PR e.g., they may run nightly or weekly.

☑️ YOU SHOULD test some partner Pipeline policies in nightly or weekly runs.

Formatting

DO format all source using rustfmt. .vscode/settings.json will do this automatically for Visual Studio Code.

DO check that all source is formatted in build pipelines.

This prevents noisy subsequent pull requests if another maintainer formats source, which is always recommended. All source should be formatted the same based on rustfmt defaults and any repo overrides that may be set.

README

DO have a README.md file in the component root folder.

An example of a good README.md file can be found here.

DO optimize the README.md for the consumer of the client library.

The contributor guide (CONTRIBUTING.md) should be a separate file.

Samples

DO include runnable examples using azure_identity::DefaultAzureCredential and library-specific environment variables e.g., AZURE_KEYVAULT_URL in crates’ examples/ directory.

DO use unique example file names throughout the workspace.

The example file names are compiled into executes with the same name as the source file; thus, they must have unique names throughout the workspace. To facilitate this, preface your example name with the client name - converting PascalCase type name to snake_case - or, if still ambiguous, the service directory or crate name e.g., secret_client_set_secret.rs or keyvault_secret_client_set_secret.rs.

See Cargo’s project layout for more information about conventional directories.