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_secret()
. - For type names, use PascalCase e.g.,
SecretClient
.
✅ 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_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.
✅ DO define all fields within a client struct with pub(crate)
accessibility. This allows the fields e.g., the pipeline
, to be used in convenience clients’ extension methods.
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
Client options should be plain old data structures to allow easy, idiomatic creation of options and to easily share these options across multiple clients as needed.
✅ DO define a client options struct with the same as the client name + “Options” e.g., a SecretClient
takes a SecretClientOptions
.
☑️ YOU SHOULD export client option structs from the root module of the client library e.g., azure_security_keyvault_secrets
.
✅ DO define all client-specific fields of client option structs as public and of type Option<T>
except for api_version
of type String
, if applicable.
✅ DO define an client_options: azure_core::ClientOptions
public field.
✅ DO derive Clone
to support cloning client configuration for other clients.
⚠️ YOU SHOULD NOT derive Debug
since this may inadvertently leak PII. Derive azure_core::fmt::SafeDebug
instead.
✅ DO implement 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:
use azure_core::{ClientOptions, fmt::SafeDebug};
#[derive(Clone, SafeDebug)]
pub struct SecretClientOptions {
pub api_version: String,
pub client_options: ClientOptions,
}
impl Default for SecretClientOptions {
fn default() -> Self {
Self {
api_version: "7.5".to_string(),
options: ClientOptions::default(),
}
}
}
⛔️ 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
We are reevaluating options for mocking to provide the best experience for API consumers whether or not they plan to utilize mocks or fakes.
Service Methods
✅ DO take a body: RequestContent<T>
if and only if the service method accepts a request body e.g., POST
or PUT
.
✅ DO use the service specified name of all parameters.
✅ DO define a client method options struct with the same name as the client, client method name, and “Options” e.g., a set_secret
takes an Option<SecretClientSetSecretOptions>
as the last parameter.
This is required even if the service method does not currently take any options because - should it ever add options - the client method signature does not have to change and will not break callers.
☑️ YOU SHOULD export client method option structs from the root module of the client library e.g., azure_security_keyvault_secrets
.
✅ DO define all client method-specific fields of method option structs as public and of type Option<T>
.
✅ DO define a method_options: azure_core::ClientMethodOptions
public field.
✅ DO derive Clone
to support cloning method configuration for additional client method invocations.
✅ DO derive Debug
since this may inadvertently leak PII. Derive azure_core::fmt::SafeDebug
instead.
✅ DO derive or implement Default
to support creating default method configuration.
The requirements above would define an example client options struct like:
use azure_core::{ClientMethodOptions, fmt::SafeDebug};
impl SecretClientMethods for SecretClient {
async fn set_secret(
&self,
name: &str,
value: String,
options: Option<SecretClientSetSecretOptions>,
) -> azure_core::Result<Response<KeyVaultSecret>> {
todo!()
}
}
#[derive(Clone, SafeDebug, Default)]
pub struct SecretClientSetSecretOptions {
pub enabled: Option<bool>,
pub method_options: ClientMethodOptions,
}
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.
If the service method does not return any content e.g., HTTP 204, the client method should return a Result<Response<()>>
containing the ()
unit type.
✅ 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 concrete types e.g., String
(or any type T
) when the data will be owned e.g., a field in a request model; or &str
(or any reference to type &T
)` when the data only needs to be borrowed e.g., a URL parameter.
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: String) -> Self {
Self {
api_version,
}
}
}
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.:
use azure_core::SafeDebug;
#[derive(Clone, SafeDebug)]
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 NOT define constructors on subclients. They must be constructed only from other clients.
✅ DO name all client methods returning a client with the _client
suffix e.g., CosmosClient::database_client()
.
⛔️ DO NOT export subclients from the crate root.
⛔️ 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 derive or implement azure_core::Model
for HTTP response models. Your crate must also have a direct dependency on typespec_client_core
.
✅ DO attribute models with #[typespec(format = "...")]
if the response containing the model uses a format other than JSON.
#[derive(azure_core::Model)]
#[typespec(format = "xml")]
struct Example {
pub foo: String
}
✅ 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.
If you do implement a builder, it must be defined according to the following guidelines:
✅ DO define a builder()
factory method on the type to be constructed that returns a struct with the same as the type + “Builder” e.g., Model::builder()
returns a ModelBuilder
.
✅ DO consume mut self
in with_
setter methods and return Self
except in the final build(&self)
method.
✅ DO return an owned value from the final build(&self)
method.
Enumerations
✅ DO implement all enumeration variations as PascalCase.
✅ DO derive or implement Clone
, Eq
, and PartialEq
for all enums.
⚠️ YOU SHOULD NOT derive Debug
since this may inadvertently leak PII. Derive azure_core::fmt::SafeDebug
instead.
✅ 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:
use azure_core::SafeDebug;
#[derive(Clone, Copy, SafeDebug, Eq, PartialEq, 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
:
use azure_core::SafeDebug;
#[derive(Clone, SafeDebug, Eq, PartialEq, 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: &str, 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
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 use underscores, when necessary, in feature names e.g., reqwest_rustls
to enable the reqwest
-based HTTP client with rustls
support for TLS.
Dashes are supported in feature names as well as crate names, but using underscores in both crate names and feature names provides a consistent experience for developers.
✅ 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 define a separate crate for each TypeSpec project within a service directory e.g.,
specification/keyvault/data-plane/Microsoft.KeyVault/Security.KeyVault.Secrets
->sdk/keyvault/azure_security_keyvault_secrets
specification/keyvault/data-plane/Microsoft.KeyVault/Security.KeyVault.Keys
->sdk/keyvault/azure_security_keyvault_keys
specification/keyvault/data-plane/Microsoft.KeyVault/Security.KeyVault.Certificates
->sdk/keyvault/azure_security_keyvault_certificates
✔️ YOU MAY define a common crate under the service directory that all service client crates use. Unless there’s a name conflict, this should use the “common” suffix e.g., azure_security_keyvault_common
. The API must be public but you MAY document that those APIs are not intended for public use, similar to some other languages’ common libraries.
⛔️ DO NOT package multiple service specifications that version independently within the same crate.
Directory Structure
✅ DO place all service directories under the sdk/
root directory 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 under the service directory in a subdirectory using the name of the crate e.g., sdk/keyvault/azure_security_keyvault_secrets/Cargo.toml
. This crate directory should correspond to a TypeSpec project and the crate name configured in the TypeSpec project’s tspconfig.yaml
.
☑️ 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:
- 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).
- 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 - Though Rust allows a consumer to build multiple versions of the same crate, directly depending on different versions of the same crate, or importing types or calling functions from different versions of the same crate may be unintuitive.
- 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
.
✔️ YOU MAY include a separate README.md
for a module as module documentation e.g., for module http
defined in http/mod.rs
:
#![doc = include_str!("README.md")]`
This would include the contents of http/README.md
, which would render documentation for developers browing in the GitHub web UI, as well as compile and potentially run any tests you have defined as examples in the README.md
e.g.,
This is how you would construct a client:
```rust no_run
let client = SecretClient::new(...);
```
✅ 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.
async fn set_secret(
&self,
name: &str,
value: String,
options: Option<SetSecretMethodOptions>,
) -> 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.