Update Rust guidelines for options builder pattern (#7587)

* Update Rust guidelines for options builder pattern

* Resolve feedback, remove DRAFT status

* Fix broken link
This commit is contained in:
Heath Stewart 2024-06-05 11:43:49 -07:00 коммит произвёл GitHub
Родитель 4eb47c9708
Коммит 3a8f3dc574
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
3 изменённых файлов: 212 добавлений и 183 удалений

Просмотреть файл

@ -6,182 +6,85 @@ folder: rust
sidebar: general_sidebar
---
{% include draft.html content="The Rust Language guidelines are in DRAFT status" %}
## Service Clients {#rust-client}
> TODO: This section needs to be driven by code in the Core library.
Implementation details of [service clients](introduction.md#rust-client).
## API Implementation
### Convenience Clients {#rust-client-convenience}
This section describes guidelines for implementing Azure SDK client libraries. Please note that some of these guidelines are automatically enforced by code generation tools.
Most service client crates are generated from [TypeSpec](https://aka.ms/typespec). Clients that want to provide convenience methods can choose any or all of the options as appropriate:
### Service Client
{% include requirement/MAY id="rust-client-convenience-separate" %} implement a separate client that provides features not described in a service specification.
When configuring your client library, particular care must be taken to ensure that the consumer of your client library can properly configure the connectivity to your Azure service both globally (along with other client libraries the consumer is using) and specifically with your client library.
{% include requirement/MAY id="rust-client-convenience-wrap" %} implement a client which wraps a generated client e.g., using [newtype][rust-lang-newtype], and exposes necessary methods from the underlying client as well as any convenience methods.
> TODO: add a brief mention of the approach to implementing service clients.
{% include requirement/MAY id="rust-client-convenience-extension" %} define [extension methods][rust-lang-extension-methods] that call existing public methods.
#### Service Methods
In all options above except if merely re-exposing public APIs without alteration:
> TODO: Briefly introduce that service methods are implemented via an `HttpPipeline` instance. Mention that much of this is done for you using code generation.
{% include requirement/MUST id="rust-client-convenience-telemetry" %} must telemeter the convenience client methods just like any service client methods.
##### HttpPipeline
### Options Builders {#rust-client-options-builders}
The following example shows a typical way of using `HttpPipeline` to implement a service call method. The `HttpPipeline` will handle common HTTP requirements such as the user agent, logging, distributed tracing, retries, and proxy configuration.
The `azure_core` crate depends on an `azure_core_macros` crate that defines the `ClientOptions` and `ClientMethodOptions` derive macros. These intentionally have the same name as their associated traits in `azure_core` as with `std` crate macros.
These macros make it easy for code emitters and developers to create standard client options and client method options while maintaining standard builder setters by the `azure_core` developers.
> TODO: Show an example of invoking the pipeline
Though the derive macros and traits differ in setters and use, they share similar functionality. The following implementation details will focus primarily on `ClientOptions` and `ClientOptionsBuilder`.
##### HttpPipelinePolicy/Custom Policies
{% include requirement/SHOULD id="rust-client-options-builders-client-options" %} use the `ClientOptions` derive macro for client options.
The HTTP pipeline includes a number of policies that all requests pass through. Examples of policies include setting required headers, authentication, generating a request ID, and implementing proxy authentication. `HttpPipelinePolicy` is the base type of all policies (plugins) of the `HttpPipeline`. This section describes guidelines for designing custom policies.
{% include requirement/SHOULD id="rust-client-options-builders-client-method-options" %} use the `ClientMethodOptions` derive macro for client method options.
> TODO: Show how to customize a pipeline
Client options and client method options should follow the form:
#### Service Method Parameters
```rust
use azure_core::{ClientOptions, ClientMethodOptions};
> TODO: This section needs to be driven by code in the Core library.
pub struct SecretClient {
// ...
}
##### Parameter Validation
#[derive(Clone, Debug, ClientOptions)]
pub struct SecretClientOptions {
api_version: Option<String>,
// Other client-specific options ...,
client_options: ClientOptions,
}
In addition to [general parameter validation guidelines](introduction.md#rust-parameters):
impl SecretClient {
pub fn get_secret(
&self,
name: impl AsRef<str>,
options: Option<SecretClientGetSecretOptions>,
) -> Result<Response<KeyVaultSecret>> {
todo!()
}
}
> TODO: Briefly show common patterns for parameter validation
#[derive(Clone, Debug, Default, ClientMethodOptions)]
pub struct SecretClientGetSecretOptions {
// Other client method-specific options ...,
method_options: ClientMethodOptions,
}
```
### Supporting Types
If either `client_options` or `method_options` conflicts, you can name the field whatever you want and attribute it with `#[options]` for the derive macro to discover it.
> TODO: This section needs to be driven by code in the Core library.
## Directory Layout {#rust-directories}
#### Serialization {#rust-usage-json}
In addition to Cargo's [project layout][rust-lang-project-layout], service clients' source files should be layed out in the following manner:
> TODO: This section needs to be driven by code in the Core library.
##### JSON Serialization
> TODO: This section needs to be driven by code in the Core library.
#### Enumeration-like Structs
> TODO: Add section> TODO: Add section
#### Using Azure Core Types
> TODO: Add section> TODO: Add section
### SDK Feature Implementation
#### Configuration
> TODO: This section needs to be driven by code in the Core library.
#### Logging
> TODO: Add section> TODO: Add section
##### Rust Logging specific details
> TODO: Add section
#### Distributed Tracing {#rust-distributedtracing}
{% include draft.html content="Guidance coming soon ..." %}
> TODO: Add section
#### Telemetry
{% include draft.html content="Guidance coming soon ..." %}
> TODO: Add section
### Testing
{% include draft.html content="Guidance coming soon ..." %}
> TODO: Add section
### Language-specific other
{% include draft.html content="Guidance coming soon ..." %}
> TODO: Add section
#### Complexity Management
{% include draft.html content="Guidance coming soon ..." %}
> TODO: Add section
#### Templates
{% include draft.html content="Guidance coming soon ..." %}
> TODO: Add section
#### Macros
{% include draft.html content="Guidance coming soon ..." %}
> TODO: Add section
#### Type Safety Recommendations
{% include draft.html content="Guidance coming soon ..." %}
> TODO: Add section
#### Const and Reference members
{% include draft.html content="Guidance coming soon ..." %}
> TODO: Add section
#### Integer sizes
{% include draft.html content="Guidance coming soon ..." %}
> TODO: Add section
#### Secure functions
{% include draft.html content="Guidance coming soon ..." %}
> TODO: Add section
#### Enumerations
{% include draft.html content="Guidance coming soon ..." %}
> TODO: Add section
#### Physical Design
{% include draft.html content="Guidance coming soon ..." %}
> TODO: Add section
#### Class Types (including `union`s and `struct`s)
{% include draft.html content="Guidance coming soon ..." %}
> TODO: Add section
#### Tooling
We use a common build and test pipeline to provide for automatic distribution of client libraries. To support this, we use common tooling.
> TODO: Add section> TODO: Add section
## Supported platforms
{% include requirement/MUST id="rust-platform-min" %} support the following platforms and associated compilers when implementing your client library.
### Windows
> TODO: Add support matrix
### Mac
> TODO: Add support matrix
#### Linux
> TODO: Add support matrix
* Azure/azure-sdk-for-rust/
* sdk/
* {service client moniker}/
* src/
* generated/
* clients/
* foo.rs
* bar.rs
* enums.rs
* models.rs
* lib.rs
* models.rs
* {other modules}
* Cargo.toml

Просмотреть файл

@ -6,8 +6,6 @@ folder: rust
sidebar: general_sidebar
---
{% include draft.html content="The Rust Language guidelines are in DRAFT status" %}
## 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.
@ -101,8 +99,8 @@ The API surface of your client library must have the most thought as it is the p
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`.
* For module and method names, always use lowercase e.g., `get_secret()`.
* For type names, use PascalCase e.g., `SecretClient`.
{% include requirement/MUST id="rust-api-dependencies" %} consult the [Architecture Board] if you wish to use a dependency that is not on the list of [centrally managed dependencies][rust-lang-workspace-dependencies].
@ -156,23 +154,81 @@ In cases when different credential types are supported, we want the primary use
##### Client Configuration {#rust-client-configuration}
{% include requirement/MUST id="rust-client-configuration-name" %} name the client options struct with the same as the client name + "Options" e.g., a `SecretClient` takes a `SecretClientOptions`.
{% include requirement/MUST id="rust-client-configuration-name" %} define a client options struct with the same as the client name + "Options" e.g., a `SecretClient` takes a `SecretClientOptions`.
{% include requirement/MUST id="rust-client-configuration-version" %} define a `pub api_version: String` field to pass to the service for the HTTP client.
{% include requirement/SHOULD id="rust-client-configuration-namespace" %} place client option structs in the root module of the client library e.g., `azure_security_keyvault`. Specialized clients options should be placed in submodules e.g., `azure_security_keyvault::secrets`.
{% include requirement/MUST id="rust-client-configuration-core" %} define a `pub options: azure_core::ClientOptions` field on client-specific options to define global configuration shared by any HTTP client.
{% include requirement/MUST id="rust-client-configuration-fields" %} define all client-specific fields of client option structs as private and of type `Option<T>`.
{% include requirement/MUST id="rust-client-configuration-fields-options" %} define an `client_options: azure_core::ClientOptions` private field.
{% include requirement/MUST id="rust-client-configuration-clone" %} derive `Clone` to support cloning client configuration for other clients.
{% include requirement/MUST id="rust-client-configuration-default" %} derive `Default` to support creating default client configuration including the default `api-version` used when calling into the service.
{% include requirement/MUST id="rust-client-configuration-debug" %} derive `Debug` to support printing members for diagnostics purposes.
{% include requirement/MUST id="rust-client-configuration-default" %} implement `Default` to support creating default client configuration including the default `api-version` used when calling into the service.
{% include requirement/MUST id="rust-client-configuration-builder-derive" %} derive `azure_core::ClientOptionsBuilders` to automatically implement builder setters for `azure_core::ClientOptions`.
{% include requirement/MUST id="rust-client-configuration-builder-function" %} define a `builder()` associated function that does not take `self` and that returns an instance of its associated builder as defined below.
{% include requirement/MUST id="rust-client-configuration-builder" %} implement an client options builder to construct options with the same name as the options struct + "Builder" e.g., `SecretClientOptionsBuilder`.
{% include requirement/MUST id="rust-client-configuration-builder-namespace" %} place client option builders in submodule of the root named `builders` e.g., `azure_security_keyvault::builders`. Specialized clients options should be placed in submodules e.g., `azure_security_keyvault::secrets::builders`.
{% include requirement/MUST id="rust-client-configuration-builder-methods" %} define builder setters using the "with_" prefix, take a `mut self`, and return `Self`.
{% include requirement/MUST id="rust-client-configuration-builder-methods-build" %} define a `build(&self)` method that borrows self and returns the associated client options struct. This allows creating multiple client options structs from a single builder, or even updating the builder to create variants.
The requirements above would define an example client options struct like:
```rust
#[derive(Clone, Default)]
use azure_core::{ClientOptions, ClientOptionsBuilder};
#[derive(ClientOptions, Clone, Debug)]
pub struct SecretClientOptions {
pub api_version: String,
pub options: azure_core::ClientOptions,
api_version: Option<String>,
client_options: ClientOptions,
}
impl SecretClientOptions {
pub fn builder() -> builders::SecretClientOptionsBuilder {
builders::SecretClientOptionsBuilder::new()
}
}
impl Default for SecretClientOptions {
fn default() -> Self {
Self {
api_version: Some("7.5".to_string()),
options: ClientOptions::default(),
}
}
}
pub mod builders {
use super::*;
pub struct SecretClientOptionsBuilder {
options: SecretClientOptions,
}
impl SecretClientOptionsBuilder {
pub(super) fn new() -> Self {
Self {
options: SecretClientOptions::default(),
}
}
pub fn with_api_version(mut self , api_version: impl Into<String>) -> Self {
self.options.api_version = Some(api_version.into());
self
}
pub fn build(&self) -> SecretClientOptions {
self.options.clone()
}
}
}
```
@ -197,6 +253,8 @@ pub struct SecretClientOptions {
{% include requirement/MUST id="rust-client-mocking-trait-name" %} define a trait named after the client name + "Methods" e.g., `SecretClientMethods`.
{% include requirement/MUST id="rust-client-mocking-trait-namespace" %} place these traits in the root module e.g., `azure_storage_blobs` or `azure_security_keyvault::secrets` so they are automatically discoverable by the Language Server Protocol (LSP).
{% include requirement/MUST id="rust-client-mocking-trait-methods" %} 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.,
```rust
@ -209,8 +267,7 @@ pub trait SecretClientMethods {
&self,
_name: impl Into<String>,
_version: impl Into<String>,
_options: Option<SetSecretOptions>,
context: Option<&Context>,
_options: Option<SetSecretMethodOptions>,
) -> azure_core::Result<Response> {
std::future::ready(unimplemented!())
}
@ -233,8 +290,7 @@ impl SecretClientMethods for SecretClient {
&self,
name: impl Into<String>,
version: impl Into<String>,
options: Option<SetSecretOptions>,
context: Option<&Context>,
options: Option<SetSecretMethodOptions>,
) -> azure_core::Result<Response> {
todo!()
}
@ -243,17 +299,87 @@ impl SecretClientMethods for SecretClient {
#### Service Methods {#rust-client-methods}
_Service methods_ are the methods on the client that invoke operations on the service and will follow the form:
{%include requirement/MUST id="rust-client-methods" %} take a `content: RequestContent<T>` if and only if the service method accepts a request body e.g., `POST` or `PUT`.
{% include requirement/MUST id="rust-client-methods-configuration-name" %} define a client method options struct with the same 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.
{% include requirement/SHOULD id="rust-client-methods-configuration-namespace" %} place client method option structs in the root module of the client library e.g., `azure_security_keyvault`. Specialized clients options should be placed in submodules e.g., `azure_security_keyvault::secrets`.
{% include requirement/MUST id="rust-client-methods-configuration-fields" %} define all client method-specific fields of method option structs as private and of type `Option<T>`.
{% include requirement/MUST id="rust-client-methods-configuration-fields-options" %} define a `method_options: azure_core::ClientMethodOptions` private field.
{% include requirement/MUST id="rust-client-methods-configuration-clone" %} derive `Clone` to support cloning method configuration for additional client method invocations.
{% include requirement/MUST id="rust-client-methods-configuration-debug" %} derive `Debug` to support printing members for diagnostics purposes.
{% include requirement/MUST id="rust-client-methods-configuration-default" %} derive or implement `Default` to support creating default method configuration.
{% include requirement/MUST id="rust-client-methods-configuration-builder-derive" %} derive `azure_core::ClientMethodOptionsBuilders` to automatically implement builder setters for `azure_core::ClientMethodOptions`.
{% include requirement/MUST id="rust-client-methods-configuration-builder-function" %} define a `builder()` associated function that does not take `self` and that returns an instance of its associated builder as defined below.
{% include requirement/MUST id="rust-client-methods-configuration-builder" %} implement an method options builder to construct options with the same name as the options struct + "Builder" e.g., `SecretClientSetSecretOptions`.
{% include requirement/MUST id="rust-client-methods-configuration-builder-namespace" %} place method option builders in submodule of the root named `builders` e.g., `azure_security_keyvault::builders`. Specialized clients options should be placed in submodules e.g., `azure_security_keyvault::secrets::builders`.
{% include requirement/MUST id="rust-client-methods-configuration-builder-methods" %} define builder setters using the "with_" prefix, take a `mut self`, and return `Self`.
{% include requirement/MUST id="rust-client-methods-configuration-builder-methods-build" %} define a `build(&self)` method that borrows self and returns the associated method options struct. This allows creating multiple method options structs from a single builder, or even updating the builder to create variants.
The requirements above would define an example client options struct like:
```rust
async fn method_name(
use azure_core::{ClientMethodOptions, ClientMethodOptionsBuilder};
impl SecretClientMethods for SecretClient {
async fn set_secret(
&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>;
name: impl Into<String>,
value: impl Into<String>,
options: Option<SecretClientSetSecretOptions>,
) -> azure_core::Result<Response<KeyVaultSecret>> {
todo!()
}
}
#[derive(ClientMethodOptions, Clone, Debug, Default)]
pub struct SecretClientSetSecretOptions {
enabled: Option<bool>,
method_options: ClientMethodOptions,
}
impl SecretClientSetSecretOptions {
pub fn builder() -> builders::SecretClientSetSecretOptionsBuilder {
builders::SecretClientSetSecretOptionsBuilder::new()
}
}
pub mod builders {
use super::*;
pub struct SecretClientSetSecretOptionsBuilder {
options: SecretClientSetSecretOptions,
}
impl SecretClientSetSecretOptionsBuilder {
pub(super) fn new() -> Self {
Self {
options: SecretClientSetSecretOptions::default(),
}
}
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.options.enabled = Some(enabled);
self
}
pub fn build(&self) -> SecretClientSetSecretOptions {
self.options.clone()
}
}
}
```
##### Sync and Async {#rust-client-methods-async}
@ -299,6 +425,7 @@ Any data passed to client methods to alter the pipeline e.g., retry policy optio
{% include requirement/MUST id="rust-client-methods-return-lro" %} return an `azure_core::Result<azure_core::Poller<T>>` from an `async fn` when the service implements the operation a [long-running operation](#rust-lro).
{% include requirement/MUST id="rust-client-methods-return-result" %} 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.
{% include requirement/MUST id="rust-client-methods-return-raw-response" %} provide the status code, headers, and self-consuming async raw response stream from all return types e.g.,
@ -863,13 +990,11 @@ This will impact line numbers, so you should only export APIs publicly from `lib
/// * `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>,
options: Option<SetSecretMethodOptions>,
) -> Result<Response>;
```

Просмотреть файл

@ -1,5 +1,6 @@
<!-- General Rust Language links should start with "rust-lang-" -->
[rust-lang-async-traits]: https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html
[rust-lang-extension-methods]: https://rust-lang.github.io/rfcs/0445-extension-trait-conventions.html
[rust-lang-dependencies]: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html
[rust-lang-doc-meta]: https://doc.rust-lang.org/rust-by-example/meta/doc.html
[rust-lang-guidelines]: https://rust-lang.github.io/api-guidelines/about.html