Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,6 @@ jobs:
#- name: azure_devops_rust_api check wasm build
# run: cd azure_devops_rust_api && cargo check --target wasm32-unknown-unknown --all-features
- name: azure_devops_rust_api test
run: cd azure_devops_rust_api && cargo test
run: cd azure_devops_rust_api && cargo test --all-features
- name: azure_devops_rust_api documentation generation
run: cd azure_devops_rust_api && cargo doc --all-features --no-deps
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Blocking (non-async) API ([#835](https://github.com/microsoft/azure-devops-rust-api/issues/835)),
enabled with the `blocking` feature. Each module has a `blocking` submodule with a `Client`
and `ClientBuilder` that wrap the async client, running each request on a hidden `tokio`
runtime. All credential types, retry options and pipeline policies are supported.

### Changes

- Operation `RequestBuilder` types now have a send mode type parameter, which defaults to
async, so existing code that names `RequestBuilder` is unaffected.

## [0.39.1]

### Changes
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ cargo check --all-features --examples
- `azure_devops_rust_api/src/telemetry.rs` — telemetry pipeline policy
- `azure_devops_rust_api/src/headers.rs` — common header helpers
- `azure_devops_rust_api/src/artifacts_download/` — Universal Package download client (dedup protocol)
- `azure_devops_rust_api/src/blocking.rs` — hidden tokio runtime used by the generated blocking clients

### Code generator internals (`autorust/`)

Expand All @@ -71,6 +72,7 @@ cargo check --all-features --examples
- `request_builder_into_future.rs` — into_body()/into_model() calls
- `response_code.rs` — Response type definitions
- `autorust/codegen/src/codegen_models.rs` — generates model structs/enums
- Blocking clients: when `RunConfig.generate_blocking` is set (in `ado-autorust/src/main.rs`), each module also gets a `blocking` submodule wrapping the async client (`create_client_and_builder.rs`), and request builders get a send mode type parameter (`request_builder_struct.rs`, `request_builder_send.rs`)
- `autorust/autorust.toml` — config for boxed/optional/invalid properties (fixes recursive type issues)

### Patcher (`vsts-api-patcher/`)
Expand Down
2 changes: 1 addition & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Edit `azure_devops_rust_api/Cargo.toml`:
### 4. Update the README version

Edit `azure_devops_rust_api/README.md`:
- Update the example `Cargo.toml` snippet: `azure_devops_rust_api = { version = "X.Y.0", ... }`
- Update the example `Cargo.toml` snippets: `azure_devops_rust_api = { version = "X.Y.0", ... }`

### 5. Update the CHANGELOG

Expand Down
2 changes: 2 additions & 0 deletions autorust/ado-autorust/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ fn main() -> Result<()> {
let package_config = autorust_toml::read("autorust.toml".into())?;

let run_config = &mut RunConfig::new("azure_devops_rust_api_");
// Also generate a blocking (non-async) client for each module.
run_config.generate_blocking = true;
let root_spec_folder: Utf8PathBuf = ROOT_SPEC_DIR.into();
let root_output_folder: Utf8PathBuf = "../azure_devops_rust_api/src".into();
let modules = [
Expand Down
4 changes: 4 additions & 0 deletions autorust/codegen/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ impl<'a> CodeGen<'a> {
&self.crate_config.output_folder
}

pub fn generate_blocking(&self) -> bool {
self.crate_config.generate_blocking()
}

pub fn should_workaround_case(&self) -> bool {
if let Some(title) = self.spec.title() {
self.fix_case_properties.contains(title)
Expand Down
36 changes: 35 additions & 1 deletion autorust/codegen/src/codegen_operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use proc_macro2::{Ident, TokenStream};
use quote::{quote, ToTokens};
use std::collections::BTreeSet;

use self::create_client_and_builder::create_client;
use self::create_client_and_builder::{create_blocking_client, create_client};
use self::operations::OperationCode;
use self::web_operation_gen::WebOperationGen;

Expand Down Expand Up @@ -89,15 +89,43 @@ pub fn create_operations(cg: &CodeGen) -> Result<TokenStream> {
}
}

let mut blocking_group_modules = TokenStream::new();
for (module_name, operation_code) in operations_code {
let OperationCode {
client_functions,
module_code,
} = operation_code;
let mut builders = TokenStream::new();
let mut blocking_builders = TokenStream::new();
for builder in client_functions {
if cg.generate_blocking() {
let async_client = format!(
"super::super::{}::Client",
module_name.as_deref().unwrap_or_default()
);
blocking_builders.extend(
builder
.for_blocking_client(async_client)
.into_token_stream(),
);
}
builders.extend(builder.into_token_stream());
}
if cg.generate_blocking() {
// All operations are in a group (checked in `OperationCode::new`).
let name = parse_ident(module_name.as_deref().unwrap_or_default())?;
blocking_group_modules.extend(quote! {
pub mod #name {
use super::super::models;
// The operation modules (containing the shared request builders).
use super::super::#name::*;
pub struct Client(pub(crate) super::Client);
impl Client {
#blocking_builders
}
}
});
}
match module_name {
Some(module_name) => {
let name = parse_ident(&module_name)?;
Expand Down Expand Up @@ -126,6 +154,12 @@ pub fn create_operations(cg: &CodeGen) -> Result<TokenStream> {
}
}
}
if cg.generate_blocking() {
file.extend(create_blocking_client(
&module_names,
blocking_group_modules,
)?);
}
Ok(file)
}

Expand Down
104 changes: 104 additions & 0 deletions autorust/codegen/src/codegen_operations/create_client_and_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,107 @@ pub fn create_client(modules: &[String], endpoint: Option<&str>) -> Result<Token
});
Ok(code)
}

/// Create the blocking (non-async) `Client` and `ClientBuilder`, placed in a `blocking` submodule.
///
/// These wrap the async `Client` and `ClientBuilder`: requests are sent using the async client,
/// run on a hidden tokio runtime. `group_modules` contains the blocking client for each
/// operation group.
pub fn create_blocking_client(
modules: &[String],
group_modules: TokenStream,
) -> Result<TokenStream> {
let mut clients = TokenStream::new();
for md in modules {
let client = format!("{md}_client").to_snake_case_ident()?;
let md = md.to_snake_case_ident()?;
clients.extend(quote! {
pub fn #client(&self) -> #md::Client {
#md::Client(self.clone())
}
});
}

Ok(quote! {
#[cfg(all(feature = "blocking", not(target_arch = "wasm32")))]
pub mod blocking {
#![doc = "Blocking (non-async) client."]
#![doc = ""]
#![doc = "This wraps the async [`Client`](super::Client), running each request on a hidden tokio runtime."]
#![doc = "It must not be used from within an async runtime."]
#[derive(Clone)]
pub struct Client(pub(crate) super::Client);

#[derive(Clone)]
pub struct ClientBuilder(super::ClientBuilder);

impl ClientBuilder {
#[doc = "Create a new instance of `ClientBuilder`."]
#[must_use]
pub fn new(credential: crate::Credential) -> Self {
Self(super::ClientBuilder::new(credential))
}

#[doc = "Set the endpoint."]
#[must_use]
pub fn endpoint(self, endpoint: impl Into<azure_core::http::Url>) -> Self {
Self(self.0.endpoint(endpoint))
}

#[doc = "Set the scopes."]
#[must_use]
pub fn scopes(self, scopes: &[&str]) -> Self {
Self(self.0.scopes(scopes))
}

#[doc = "Set the retry options."]
#[must_use]
pub fn retry(self, retry: impl Into<azure_core::http::RetryOptions>) -> Self {
Self(self.0.retry(retry))
}

#[doc = "Set the transport options."]
#[must_use]
pub fn transport(self, transport: impl Into<azure_core::http::Transport>) -> Self {
Self(self.0.transport(transport))
}

#[doc = "Set per-call policies."]
#[must_use]
pub fn per_call_policies(self, policies: impl Into<Vec<std::sync::Arc<dyn azure_core::http::policies::Policy>>>) -> Self {
Self(self.0.per_call_policies(policies))
}

#[doc = "Set per-try policies."]
#[must_use]
pub fn per_try_policies(self, policies: impl Into<Vec<std::sync::Arc<dyn azure_core::http::policies::Policy>>>) -> Self {
Self(self.0.per_try_policies(policies))
}

#[doc = "Convert the builder into a `Client` instance."]
pub fn build(self) -> Client {
Client(self.0.build())
}
}

impl Client {
#[doc = "Create a new `ClientBuilder`."]
#[must_use]
pub fn builder(credential: crate::Credential) -> ClientBuilder {
ClientBuilder::new(credential)
}

#clients
}

impl From<super::Client> for Client {
#[doc = "Create a blocking client that sends requests using an existing async client."]
fn from(client: super::Client) -> Self {
Self(client)
}
}

#group_modules
}
})
}
56 changes: 52 additions & 4 deletions autorust/codegen/src/codegen_operations/function_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@ pub(crate) struct ClientFunctionCode {
fname: Ident,
parameters: FunctionParams,
in_operation_group: bool,
/// The request builder has a send mode type parameter (see `RequestBuilderStructCode`).
generate_blocking: bool,
/// For a blocking client function, the path of the corresponding async client,
/// whose documentation is linked to rather than repeated.
blocking_async_client: Option<String>,
}

impl ClientFunctionCode {
pub fn new(
operation: &WebOperationGen,
parameters: &FunctionParams,
in_operation_group: bool,
generate_blocking: bool,
) -> Result<Self> {
let fname = operation.function_name()?;
let summary = operation.0.summary.clone();
Expand All @@ -31,17 +37,38 @@ impl ClientFunctionCode {
fname,
parameters: parameters.clone(),
in_operation_group,
generate_blocking,
blocking_async_client: None,
})
}

/// Returns a copy of this function for a blocking client.
///
/// `async_client` is the path of the corresponding async client.
pub fn for_blocking_client(&self, async_client: String) -> Self {
Self {
blocking_async_client: Some(async_client),
..self.clone()
}
}
}

impl ToTokens for ClientFunctionCode {
fn to_tokens(&self, tokens: &mut TokenStream) {
let mut params: Vec<TokenStream> = Vec::new();
if self.in_operation_group {
params.push(quote! { client: self.0.clone() });
if self.blocking_async_client.is_some() {
// A blocking group client wraps a blocking client, which wraps the async client.
params.push(quote! { client: self.0 .0.clone() });
params.push(quote! { mode: std::marker::PhantomData });
} else {
params.push(quote! { client: self.clone() });
if self.in_operation_group {
params.push(quote! { client: self.0.clone() });
} else {
params.push(quote! { client: self.clone() });
}
if self.generate_blocking {
params.push(quote! { mode: std::marker::PhantomData });
}
}
for param in self.parameters.required_params() {
let FunctionParam {
Expand Down Expand Up @@ -98,11 +125,32 @@ impl ToTokens for ClientFunctionCode {

let fname = &self.fname;
let parameters = FunctionCallParamsCode(self.parameters.clone());
let generics = if self.blocking_async_client.is_some() {
quote! { <crate::blocking::Blocking> }
} else {
quote! {}
};
// Blocking client functions link to the async function's documentation,
// rather than repeating the argument descriptions.
let param_descriptions = match &self.blocking_async_client {
Some(async_client) => {
let doc = format!(
"See [`{fname}`]({async_client}::{fname}) for details of the arguments."
);
let blank = if summary.is_empty() && description.is_empty() {
quote! {}
} else {
quote! { #[doc = ""] }
};
vec![quote! { #blank #[doc = #doc] }]
}
None => param_descriptions,
};
tokens.extend(quote! {
#summary
#description
#(#param_descriptions)*
pub fn #fname(#parameters) -> #fname::RequestBuilder {
pub fn #fname(#parameters) -> #fname::RequestBuilder #generics {
#fname::RequestBuilder {
#(#params),*
}
Expand Down
32 changes: 28 additions & 4 deletions autorust/codegen/src/codegen_operations/operation_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub struct OperationModuleCode {
pub request_builder_setters_code: RequestBuilderSettersCode,
pub request_builder_send_code: RequestBuilderSendCode,
pub request_builder_intofuture_code: RequestBuilderIntoFutureCode,
pub generate_blocking: bool,
}
impl ToTokens for OperationModuleCode {
fn to_tokens(&self, tokens: &mut TokenStream) {
Expand All @@ -24,7 +25,33 @@ impl ToTokens for OperationModuleCode {
request_builder_setters_code,
request_builder_send_code,
request_builder_intofuture_code,
generate_blocking,
} = &self;
// With a blocking client the request builder is generic over the send mode,
// and the send code provides its own impl blocks.
let request_builder_impls = if *generate_blocking {
let setters = request_builder_setters_code.to_token_stream();
let setters_impl = if setters.is_empty() {
quote! {}
} else {
quote! {
impl<M> RequestBuilder<M> {
#setters
}
}
};
quote! {
#setters_impl
#request_builder_send_code
}
} else {
quote! {
impl RequestBuilder {
#request_builder_setters_code
#request_builder_send_code
}
}
};
tokens.extend(quote! {
pub mod #module_name {
use super::models;
Expand All @@ -37,10 +64,7 @@ impl ToTokens for OperationModuleCode {

#request_builder_struct_code

impl RequestBuilder {
#request_builder_setters_code
#request_builder_send_code
}
#request_builder_impls

#request_builder_intofuture_code
}
Expand Down
Loading
Loading