Safety
The following guidelines are to foster secure code not only within Azure SDK for Rust, but on behalf of our customers.
Debug Trait
✔️ YOU MAY derive or implement Debug on types as long as you guarantee no PII may be leaked.
To elide some fields from Debug output, you may use finish_non_exhaustive() like so:
use std::fmt;
impl fmt::Debug for MyModel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MyModel")
.field("id", &self.id)
.finish_non_exhaustive()
}
}
☑️ YOU SHOULD derive or implement azure_core::fmt::SafeDebug on types if you need a Debug implementation but cannot reasonably guarantee no PII may be leaked.
SafeDebug will only output the name of the type or, if information is available in TypeSpec, show only fields that have been declared safe from leaking PII.
Service Clients
Implementation details of service clients.
Convenience Clients
Most service client crates are generated from TypeSpec. Clients that want to provide convenience methods can choose any or all of the options as appropriate:
✔️ YOU MAY implement a separate client that provides features not described in a service specification.
✔️ YOU MAY implement a client which wraps a generated client e.g., using newtype, and exposes necessary methods from the underlying client as well as any convenience methods. You might consider this approach if you want to effectively hide most generated methods and define replacements. You are responsible for transposing documentation and following all guidelines herein.
✔️ YOU MAY define extension methods in a trait that call existing public methods or directly on the pipeline, e.g.,
pub trait SecretClientExt {
async fn deserialize_secret<T: serde::de::DeserializeOwned>(
&self,
name: impl AsRef<str>,
version: Option<impl AsRef<str>>,
) -> Result<Response<T>>;
}
impl SecretClientExt for SecretClient {
async fn deserialize_secret<T: serde::de::DeserializeOwned>(
&self,
name: impl AsRef<str>,
version: Option<impl AsRef<str>>,
) -> Result<T> {
let value = self.get_secret(name, version).await?;
serde_json::from_str(&value).map_err(Error::from)
}
}
- The trait MUST be exported from the crate root.
- The trait MUST use the name of the client it extends with an “Ext” suffix e.g., “SecretClientExt”.
You might consider this approach if the generated methods are sufficient but you want to add convenience methods.
Convenience Client Telemetry
In all options above except if merely re-exposing public APIs without alteration:
✅ DO telemeter the convenience client methods just like any service client methods.
Tests
We will implement tests idiomatically with cargo.
Unit tests
✅ DO include unit tests in the module containing the subject being tested.
✔️ YOU MAY separate unit tests in a separate file named tests.rs under a separate directory with the same name as the module you’re testing e.g., tests may go into foo/tests.rs to test module foo. Module foo would then include $[cfg(test)] mod tests;. This is useful if your module contains a lot of code and you have a lot of tests that make maintaining and reviewing the source file more difficult.
☑️ YOU SHOULD put all tests into a tests submodule.
☑️ YOU SHOULD preface tests with “test_” unless you need to disambiguate with the function being tested.
Putting these requirements together, you should have code similar to:
pub fn hello() -> String {
todo!()
}
pub async fn read_config() -> azure_core::Result<Configuration> {
todo!()
}
#[cfg(test)]
mod tests {
#[test]
fn test_hello() {
assert_eq!(hello(), String::from("Hello, world!"));
}
#[tokio::test]
async fn reads_config() -> azure_core::Result<Configuration> {
let config = read_config().await?;
assert_eq!(config.id, 1234);
assert_eq!(config.sections.len(), 3);
}
}
Integration tests
✅ DO include integration tests under the tests/ subdirectory of your crate.
☑️ YOU SHOULD write integration tests as recorded tests.
Examples
☑️ YOU SHOULD include examples under the examples/ subdirectory for primary use cases. These are written as standalone executables but may include shared code modules.
Documentation examples
Documentation tests are powerful. Not only are they compiled (unless ignore), but can be executed (unless no_run) with cargo test --doc (run by default with cargo test). They also allow you to hide setup code e.g., if you want to call an async function:
/// ```no_run
/// # #[tokio::main] fn main() -> Result<(), Box<dyn std::error::Error>> {
/// # let credential: DeveloperToolsCredential = unimplemented!();
/// let client = SecretClient::new("https://my-vault.vault.azure.net", credential.clone(), None)?;
/// let secret = client.get_secret("my-secret", None).await?.into_model()?;
/// println!("{secret:#?}");
/// # Ok(()) }
In Rust documentation comments, on https://docs.rs, etc., you’ll only see:
let client = SecretClient::new("https://my-vault.vault.azure.net", credential.clone(), None)?;
let secret = client.get_secret("my-secret", None).await?.into_model()?;
println!("{secret:#?}");
But all those lines will render in plain markdown like in README.md on https://github.com and elsewhere.
Instead, there’s include-file that lets you achieve the same result in README.md while compiling or even executing those snippets as tests.
✔️ YOU MAY use the include_file::include_markdown!() macro to render Rust code snippets while compiling or even executing those snippets as tests.
In your README.md, you’ll include only the code you want to show in a rust ignore code fence (it has to be ignored because it won’t compile without a lot of setup code) along with a unique name within that file that identifies your example, like get-secret:
```rust ignore get-secret
let secret = client.get_secret("my-secret", None).await?.into_model()?;
println!("{secret:#?}");
```
Add a tests/readme.rs file that includes your setup code similar to what we would’ve hidden in Rust documentation comments and reference section in the README.md (relative to the crate root):
use azure_identity::DeveloperToolsCredential;
use azure_security_keyvault_secrets::SecretClient;
use include_file::include_markdown;
#[ignore = "requires provisioned resources"]
#[tokio::test]
async fn readme() -> Result<(), Box<dyn std::error::Error>> {
let credential = DeveloperToolsCredential::new(None)?;
let client = SecretClient::new("https://my-vault.vault.azure.net", credential, None)?;
include_markdown("README.md", "get-secret");
Ok(())
}
The test has #[ignore] because we can’t actually execute it without provisioning resources, but we can compile it; however, you can use recorded tests.
✔️ YOU MAY use recorded tests to actually execute documentation examples.
The markdown is the same, but we change the signature of the test like so:
use azure_core::Result;
use azure_core_test::{recorded, TestContext};
use azure_security_keyvault_secrets::{SecretClient, SecretClientOptions};
use include_file::include_markdown;
#[recorded::test]
async fn readme(ctx: TestContext) -> Result<()> {
let recording = ctx.recording();
let mut options = SecretClientOptions::default();
recording.instrument(&mut options.client_options);
let client = SecretClient::new(
"https://my-vault.vault.azure.net",
recording.credential(),
Some(options),
)?;
include_markdown("README.md", "get-secret");
Ok(())
}
Now you can record and later play back your tests. See our contribution guide for integration tests for details.
For a complete example, see pull request Azure/azure-sdk-for-rust#3337.
Traits
✅ DO attribute traits and trait implementations with async functions with the async_trait::async_trait procedural macro to desugar the async functions. This allows requiring futures to also be Send. See Azure/azure-sdk-for-rust#1796 for details.
use async_trait::async_trait;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait Policy {
async fn send(
&self,
ctx: &Context,
request: &mut Request,
next: &[Arc<dyn Policy>],
) -> PolicyResult;
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl Policy for TelemetryPolicy {
async fn send(
&self,
ctx: &Context,
request: &mut Request,
next: &[Arc<dyn Policy>],
) -> PolicyResult {
todo!()
}
}
We loosen constrains for wasm32 because threads are not supported.
Directory Layout
In addition to Cargo’s project layout, service clients’ source files should be laid out in the following manner:
Azure/azure-sdk-for-rust/
├─ .vscode/cspell.json
├─ doc/ # general documentation
├─ eng/ # engineering system pipeline, scripts, etc.
└─ sdk/
└─ {service directory}/ # example: keyvault
├─ .dict.txt
└─ {service client crate}/ # example: azure_security_keyvault_secrets
├─ assets.json # best location for most crates, or in {service directory} for all crates
├─ examples/
│ ├─ {optional shared code}/
│ ├─ example1.rs
│ └─ example2.rs
├─ src/
│ ├─ generated/
│ │ ├─ clients/
│ │ │ ├─ foo.rs
│ │ │ └─ bar.rs
│ │ ├─ enums.rs
│ │ └─ models.rs
│ ├─ lib.rs
│ ├─ models.rs
│ └─ {other modules}
├─ tests/
│ ├─ {shared code}/
│ ├─ integration_test1.rs
│ └─ integration_test2.rs
└─ Cargo.toml
Module Layout
Rust modules should be defined such that:
- All clients and their client options that the user can create are exported from the crate root e.g.,
azure_security_keyvault_secrets. - All subclients and their client options that can only be created from other clients should only be exported from the
clientssubmodule e.g.,azure_security_keyvault_secrets::clients. - All client method options are exported from the
modelsmodule e.g.,azure_security_keyvault_secrets::models. - Extension methods on clients should be exported from the same module(s) from which their associated clients are exported.
- Extension methods on models should be exported from the same module(s) from which their associated models are exported.
Effectively, export creatable clients from the root and keep associated items together. These creatable types are often the only types that users will need to reference by name so we want them easily discoverable.
All clients will be exported from a clients submodule so they are easy to find, but creatable clients would be re-exported from the crate root e.g.,
// lib.rs
mod generated;
mod helpers;
pub use generated::*;
pub use helpers::*;
// generated/mod.rs
pub mod clients;
pub mod models;
pub use clients::{SecretClient, SecretClientOptions};
If you need to define clients or models in addition to those generated e.g., you want to wrap generated clients instead of exposing them directly,
you can create your own clients and models modules and re-export generated::clients::* and generated::models::*, respectively, from there.
// lib.rs
pub mod clients;
pub mod models;
pub use clients::{SecretClient, SecretClientOptions};
// clients/mod.rs
use crate::generated::clients::SecretClient as GeneratedSecretClient;
pub use crate::generated::SecretClientOptions;
pub struct SecretClient {
client: GeneratedSecretClient,
} // ...
// models/mod.rs
pub use crate::generated::models::*;
#[derive(SafeDebug)]
pub struct ExtraModel {
// ...
}
Miscellaneous
Spelling
☑️ YOU SHOULD put general words used across different services and client libraries in the .vscode/cspell.json file.
☑️ YOU SHOULD put words specific to a service or otherwise limited use in a .dict.txt file in the {service directory} as shown in the directory layout.
If you’re creating this file, add an entry to .vscode/cspell.json as shown below:
{
"dictionaryDefinitions": [
{
"name": "service-name",
"path": "../sdk/service-directory/.dict.txt",
"noSuggest": true
}
],
"overrides": [
{
"filename": "sdk/service-directory/**",
"dictionaries": [
"crates",
"rust-custom",
"service-name"
]
}
]
}