Skip to content

Sync api [prototype] - #836

Open
John Batty (johnbatty) wants to merge 4 commits into
microsoft:mainfrom
johnbatty:sync-api-prototype
Open

John Batty (johnbatty) wants to merge 4 commits into
microsoft:mainfrom
johnbatty:sync-api-prototype

Conversation

@johnbatty

Copy link
Copy Markdown
Collaborator

Summary

Adds a blocking (non-async) API alongside the existing async API, as requested in #835. It is additive and opt-in: existing async code compiles
unchanged, and default builds have the same dependencies as before.

use azure_devops_rust_api::{blocking::UreqClient, git, Credential};

let credential = Credential::from_pat(pat);
let git_client = git::blocking::ClientBuilder::new(credential, UreqClient::new()).build();

let repos = git_client
    .repositories_client()
    .list(organization, project)
    .send()?          // blocking; no `.await` available
    .into_body()?
    .value;

A blocking-only build doesn't need tokio, hyper or async reqwest:

azure_devops_rust_api = { version = "...", default-features = false, features = ["git", "ureq"] }

Addresses #835.

Design

  • Request construction is separate from sending. Each operation's RequestBuilder has a sync build_request() that returns the full
    azure_core::http::Request (URL, query, headers, body) without an Authorization header. The async send() now uses it too.
  • Request builders are generic over the client type: RequestBuilder<C = Client>. The default keeps the async client, so code that names
    list::RequestBuilder is unaffected. Setters and build_request() are shared by both clients.
    • The async builder keeps .await and an async send().
    • RequestBuilder<blocking::Client> has only a blocking send(), so the compiler rejects any attempt to .await a blocking request.
  • Each module gets a generated blocking submodule (blocking::Client, blocking::ClientBuilder and a client per operation group). The HTTP
    client is supplied once, when the ClientBuilder is created, and is used for every call.
  • blocking::HttpClient trait (hand-written): implement it to use any HTTP client. The crate provides:
    • UreqClient (ureq feature)
    • ReqwestBlockingClient (reqwest_blocking feature), which reuses the reqwest 0.13 already in the dependency tree via azure_core.
  • Errors match the async API: error status codes go through azure_core's check_success.
  • User-Agent: blocking requests send a User-Agent in the same format azure_core uses.

Features and dependencies

Feature Enables
reqwest (default) azure_core's async reqwest transport and tokio (azure_core's default features)
blocking The generated blocking clients and the HttpClient trait
ureq blocking + UreqClient
reqwest_blocking blocking + ReqwestBlockingClient
  • azure_core's default features are now enabled by this crate's reqwest feature rather than always. Default and --all-features builds resolve
    exactly the same crates, versions and azure_core features as before.
  • With default-features = false, features = ["git", "ureq"], the build doesn't depend on tokio, hyper or async reqwest (141 → 110 crates). A CI step
    checks this.
  • Builds without the blocking feature don't compile any blocking code.

Limitations

  • Only PAT and unauthenticated credentials are supported. TokenCredential (OAuth) returns an error, because getting a token is async.
  • Blocking requests don't go through the azure_core pipeline, so no retry policy or custom per-call/per-try policies are applied.
  • artifacts_download is async-only.
  • reqwest_blocking still pulls in tokio, because reqwest::blocking uses it internally. Use ureq for a build without tokio.

Breaking changes

  • Operation RequestBuilder types have a new client type parameter. It defaults to the async client, so code that names the type is unaffected.
  • Users who set default-features = false and use the async client no longer get azure_core's HTTP transport. They need to enable the reqwest
    feature or provide their own transport; otherwise azure_core panics with a message saying the reqwest feature is needed.

Code generator changes (autorust)

  • RunConfig.generate_blocking (set in ado-autorust/src/main.rs) turns on blocking client generation.
  • Generation fails with a clear error if an operation isn't in an operation group, or is pageable or long-running. None are today, across all 40
    modules.
  • Blocking client functions keep each operation's description and link to the async function for argument details, instead of repeating them.

Cost

Measured on cargo check/build of azure_devops_rust_api with --all-features:

Before After
Generated source 7.39 MB 9.97 MB
cargo check ~16.4s ~17.4s
Debug build ~18s ~21s
Release build ~116s ~119s

The timings were measured before the blocking code was put behind the blocking feature, so all of it was being compiled. Builds without the feature
compile none of it.

Other changes

  • Examples:
    • git_repo_list_blocking (ureq)
    • git_repo_list_reqwest_blocking (reqwest)
    • wit_work_item_create_blocking (a JSON Patch body)
  • tests/blocking.rs: tests against a local mock HTTP server. The blocking tests run with both ureq and reqwest.
    • request line, Basic auth header, User-Agent
    • JSON body on POST
    • a 404 becomes an HttpResponse error
    • TokenCredential is rejected
    • the async path sends the same request as before
  • CI:
    • The test step now runs cargo test --all-features, so the new tests run.
    • A new step lints the blocking-only (ureq) build and fails if tokio, hyper or reqwest are in its dependency tree.
  • Docs: new README section "Blocking (non-async) API", CHANGELOG entry, CLAUDE.md and SKILL.md updates.
  • Separate commit: removes the stale autorust/codegen/src/bin/gen_ado.rs (an outdated copy of the generator entry point) and two codegen examples
    (list_crates, owned_crates) that no longer compiled.

Testing

  • ./build.sh reproduces the committed generated code.
  • cargo fmt --check, clippy --all-features (library and examples) with --deny warnings, cargo test (default and --all-features) and cargo doc --all-features all pass.
  • Clippy also passes for blocking-only builds: --no-default-features with git,ureq, git,blocking and git,reqwest_blocking.
  • The examples have not been run against a live Azure DevOps organization.

🤖 Generated with Claude Code

gen_ado.rs was an outdated copy of ado-autorust/src/main.rs, and the
list_crates and owned_crates examples no longer compile.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each module now has a generated `blocking` submodule (behind the new
`blocking` feature) with a ClientBuilder that takes an HttpClient to
send requests. HttpClient implementations are provided for ureq (`ureq`
feature) and reqwest::blocking (`reqwest_blocking` feature). Only PAT
and unauthenticated credentials are supported.

Request builders are now generic over the client type (defaulting to
the async client), and expose build_request() to build the HTTP request
without sending it.

See microsoft#835.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
azure_core's default features (async reqwest transport, tokio) are now
enabled by this crate's `reqwest` feature (on by default) rather than
unconditionally, so blocking-only builds using ureq don't depend on
tokio, hyper or async reqwest. Default builds are unchanged.

CI checks that a `--no-default-features --features git,ureq` build
excludes tokio, hyper and reqwest.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@johnbatty John Batty (johnbatty) changed the title Sync api prototype Sync api [prototype] Sep 27, 2026
Omit the `azure_` crate name prefix, as azure_core does, so blocking
requests send `azsdk-rust-devops_rust_api/...` like async requests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant