feat(fetch_azure): adapt fetch HttpClient to Azure's HttpClient#494
feat(fetch_azure): adapt fetch HttpClient to Azure's HttpClient#494martintmk wants to merge 22 commits into
Conversation
Add a new fetch_azure crate providing FetchHttpClient, an adapter that implements typespec_client_core::http::HttpClient on top of a fetch::HttpClient. This lets the Azure SDK for Rust use fetch as its HTTP transport. The adapter converts a typespec Request into a fetch request (method, uri, headers, and bytes or seekable-stream body), executes it through the fetch client, and maps the fetch response back into an AsyncRawResponse with a streamed body. A new_http_client helper returns an Arc<dyn HttpClient> for convenience. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #494 +/- ##
=======================================
Coverage 99.9% 100.0%
=======================================
Files 336 338 +2
Lines 24829 24932 +103
=======================================
+ Hits 24818 24932 +114
+ Misses 11 0 -11 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Avoid the possessive form of the SDK acronym, which Hunspell flags, in the to_headers doc comment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Incorporate the best ideas from the internal azure_core adapter to reduce allocations and improve error classification: - split request building (mapped to DataConversion errors) from execution (mapped to Io errors) via layered::Service, instead of the combined builder .fetch() path - add an empty-body fast path that reuses fetch's shared empty body instead of allocating - stream the response body via HttpBody::into_stream, dropping the http-body-util dependency - forward seekable request-stream read errors with a descriptive message Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add examples/azure_transport.rs showing how to adapt a Tokio-based fetch client into an Arc<dyn HttpClient> Azure SDK transport and issue a request through the typespec HttpClient trait. Enable the tokio and rustls features on the fetch dev-dependency so the example can build a real client. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add integration tests for the previously-uncovered branches so the crate reaches full line coverage: - request build failure maps to a DataConversion error - non-UTF8 response header values are skipped - a failing seekable request-body stream surfaces through the body error map - a failing response body surfaces through the response error map Adds async-trait and futures dev-dependencies for the erroring SeekableStream helper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bundle a runtime abstraction alongside the transport: SpawnerRuntime implements azure_core::async_runtime::AsyncRuntime on top of an anyspawn::Spawner (spawn via the spawner, sleep on its blocking pool, and yield), with a new_async_runtime helper returning Arc<dyn AsyncRuntime>. Switch the crate from typespec_client_core to azure_core (which re-exports the same typespec http and async_runtime traits) so both abstractions come from one Azure SDK crate. Add anyspawn and azure_core dependencies and drop the direct typespec_client_core dependency. Add integration tests for spawn, abort, sleep, yield, the dyn-runtime helper, and the From/inner round trip; lib.rs remains at 100% coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SpawnerRuntime now holds a tick::Clock alongside the spawner and implements AsyncRuntime::sleep via Clock::delay, instead of blocking a spawner thread with std::thread::sleep. Constructors and new_async_runtime take the clock; spawner() and clock() accessors replace inner()/into_inner(), and From now accepts a (Spawner, Clock) tuple. lib.rs remains at 100% coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The adapter implements azure_core::http::HttpClient, so AzureHttpClient reads more naturally than FetchHttpClient. Pure rename; no behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop the new_http_client free function in favor of From impls: From<fetch::HttpClient> for AzureHttpClient (unchanged) and a new From<AzureHttpClient> for Arc<dyn HttpClient>, so callers write AzureHttpClient::from(client).into(). A direct From<fetch::HttpClient> for Arc<dyn HttpClient> is not possible under the orphan rules, so the Arc conversion goes through the local type. lib.rs remains at 100% coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address PR review feedback: - move AzureHttpClient into a client module - move the runtime into a runtime module and rename SpawnerRuntime to Runtime - fix cancellation: spawn tasks wrapped in futures::future::Abortable and abort via AbortHandle so aborting wakes pending waiters, instead of the flag-based approach that left parked awaiters hanging lib.rs is now a thin module root; client.rs and runtime.rs are each at 100% coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address PR review feedback: - drop AzureHttpClient::inner and into_inner (not needed publicly) - drop the new_async_runtime free function; add From<Runtime> for Arc<dyn AsyncRuntime> to mirror the client's From-based Arc conversion - split integration tests into tests/client.rs and tests/runtime.rs client.rs and runtime.rs remain at 100% coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add an optional �zure-identity feature under which Runtime implements �zure_identity::Executor, running developer-credential commands on the spawner's blocking pool. Add a tokio-based blob-listing example that wires the transport, executor, and DeveloperToolsCredential together. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The CI examples runner executes every example and requires exit 0. The blob_list example needs a live Storage account and developer sign-in, so it now skips gracefully (printing a message and returning Ok) when \AZURE_STORAGE_SERVICE_ENDPOINT\ is unset, while still running the full flow when configured. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mark to_fetch_body with mutants::skip: the empty-body fast path is observationally equivalent to the general bytes path (both yield a zero-length body), so the is_empty() guard is an equivalent mutant. Bound the abort cancellation test with a timeout so a no-op abort fails fast (caught) instead of hanging until the mutation-test timeout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract the AsyncRuntime adapter (Runtime) and the azure_identity::Executor impl out of fetch_azure into a dedicated anyspawn_azure crate. fetch_azure now provides only the AzureHttpClient transport; anyspawn_azure owns the spawner/clock-backed runtime and the optional azure-identity executor. Also drop the From<(Spawner, Clock)> for Runtime conversion (callers use Runtime::new) and keep the blob_list example in fetch_azure, now dev-depending on anyspawn_azure for the executor. Register both crates in the root README and CHANGELOG. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
fetch_azure no longer depends on anyspawn after the runtime extraction, so the crate-doc reference to it must be a plain code span rather than an intra-doc link (which fails the docs build under -D warnings). Regenerate the README accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Both crate libraries used azure_core only to reach the HttpClient and AsyncRuntime traits, which azure_core re-exports from typespec_client_core (the allowed_external_types lists already targeted typespec_client_core). Depend on typespec_client_core directly in both libraries (fetch_azure enables its \http\ feature). azure_core is retained only as a fetch_azure dev-dependency for the blob_list example, which uses Azure-specific ClientOptions/Transport/credentials. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per review, rename the transport adapter to \etch_azure::HttpClient\ so it reads naturally alongside \�nyspawn_azure::Runtime\. Since the struct now shares its name with the \ ypespec_client_core::http::HttpClient\ trait it implements, the trait is imported under the \HttpClientTrait\ alias where it must be named (impl site, \Arc<dyn ...>\). Updates the lib/struct docs, examples, and tests accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mirror the existing \From<Runtime> for Arc<dyn AsyncRuntime>\ with a feature-gated \From<Runtime> for Arc<dyn azure_identity::Executor>\ so a Runtime can be handed to credentials as a boxed executor, with a test covering the conversion. Also enable the \http\ feature on anyspawn_azure's typespec_client_core dependency: typespec_client_core's always-compiled \stream\ module imports the http-gated \crate::http\, so the crate does not build without it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add \#![cfg_attr(docsrs, feature(doc_cfg))]\ to fetch's crate root so feature-gated items render with their feature requirements on docs.rs, matching fetch_azure and anyspawn_azure. The attribute is gated on the \docsrs\ cfg, so it is inert on stable builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Miri cannot run tokio's runtime or spawn OS processes, so the runtime and transport integration tests fail under \cargo miri test\. Gate both test files with \#![cfg(not(miri))]\ (matching anyspawn's tokio test files); neither crate has unsafe code, so Miri loses no UB coverage. Also fix a stale doc reference to fetch_azure::Runtime in the moved runtime tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Under Miri the cfg(not(miri)) guard strips each test crate's contents, including its module doc, leaving an empty crate that trips the denied missing_docs lint. Add an allow(missing_docs) attribute before the cfg guard (matching anyspawn's tokio test files) so the empty Miri build compiles cleanly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
heaths
left a comment
There was a problem hiding this comment.
The only place I should see "typespec*" is in Cargo.lock.
| async-trait = { workspace = true } | ||
| futures = { workspace = true, features = ["std"] } | ||
| http = { workspace = true } | ||
| typespec_client_core = { workspace = true, features = ["http"] } |
There was a problem hiding this comment.
Use azure_core. Forget typespec_client_core and below exist. Everything is re-exported from azure_core. It's an initiative we had to support but likely won't happen.
| Adapt a [`fetch::HttpClient`][__link0] into an Azure SDK HTTP transport. | ||
|
|
||
| The Azure SDK abstracts its HTTP transport behind the | ||
| [`typespec_client_core::http::HttpClient`][__link1] trait. [`HttpClient`][__link2] implements that |
There was a problem hiding this comment.
azure_core. We don't talk about typespec* crates.
|
|
||
| The Azure SDK abstracts its HTTP transport behind the | ||
| [`typespec_client_core::http::HttpClient`][__link1] trait. [`HttpClient`][__link2] implements that | ||
| trait on top of a [`fetch::HttpClient`][__link3], so Azure SDK pipelines run over |
There was a problem hiding this comment.
I know we devs are bad at naming variables, but is __linkN really the best convention? Why not something more descriptive that is easier to associate should you need to fix something. And if you only refer to them once, why even have links. In fact, __link0 an __link3 are dups.
| # Workspace sibling crates | ||
| "fetch::*", | ||
| # External dependency that defines the HttpClient trait | ||
| "typespec_client_core::*", |
There was a problem hiding this comment.
Last time I'll mention it, but azure_core. That depends on - and re-exports all of - typespec_client_core and typespec so maybe they are still needed here as transitive dependencies, but typespec* should not appear in any docs or Cargo.toml files.
There was a problem hiding this comment.
You put your .ico in LFS but not you .png?
| //! fn transport(client: FetchClient) -> Arc<dyn typespec_client_core::http::HttpClient> { | ||
| //! HttpClient::from(client).into() | ||
| //! } | ||
| //! # let _ = transport; |
There was a problem hiding this comment.
Probably should do something with it. This sample isn't inherently useful.
| /// ``` | ||
| /// # use std::sync::Arc; | ||
| /// # use fetch_azure::HttpClient; | ||
| /// # fn wrap(client: fetch::HttpClient) -> Arc<dyn typespec_client_core::http::HttpClient> { |
There was a problem hiding this comment.
Why does your elided sample func return an Arc? Seems unnecessary.
That said, elsewhere, you should state the reason the Azure SDK wants an Arc is to promote sharing. Same is true of HttpClient, Policy, and TokenCredential traits. Our pattern of having their concrete implementation constructors return an Arc isn't bad Rust code. It's intentional to promote sharing. Most TokenCredentials, for example, share an endpoint/token cache and reduce network calls. Same is true across all the Azure SDK languages - but all of them (besides C++) are ref-counted so get it for free.
Summary
Adds two crates that adapt Oxidizer building blocks to the Azure SDK abstractions, built directly on
typespec_client_core1.0.0 (the crate that defines theHttpClientandAsyncRuntimetraits thatazure_corere-exports):fetch_azure—AzureHttpClientimplementstypespec_client_core::http::HttpClienton top of afetch::HttpClienttransport.anyspawn_azure—Runtimeimplementstypespec_client_core::async_runtime::AsyncRuntimeon top of ananyspawn::Spawner(spawning) and atick::Clock(sleeping). With the optionalazure-identityfeature it also implementsazure_identity::Executor, running developer-credential commands on the spawner.fetchis depended on without enabling any of its features.Details
AzureHttpClientconverts a typespecRequest(method, URI, headers, and bytes or seekable-stream body) into afetchrequest, executes it, and maps thefetchresponse back into anAsyncRawResponsewith a streamed body. Empty bodies and byte bodies avoid copies; non-UTF-8 response headers are skipped (matching the built-inreqwesttransport). Build errors map toDataConversion; transport errors toIo.From<fetch::HttpClient>andFrom<AzureHttpClient>intoArc<dyn HttpClient>;From<Runtime>intoArc<dyn AsyncRuntime>.Runtimespawns tasks wrapped infutures::future::Abortableso cancellation wakes pending waiters;sleepusestick::Clock::delay.azure_transport(transport round-trip) andblob_list(transport +anyspawn_azureexecutor +DeveloperToolsCredential, listing blobs on tokio).client.rsandruntime.rsare each at 100% line/region coverage.This mirrors the in-tree
reqwestadapter shipped bytypespec_client_core.