Skip to main content

dig_urn_resolver/
native.rs

1//! The native runtime surface: a `reqwest` HTTP transport plus convenience entry
2//! points (`resolve`, `resolve_with`, `resolve_blocking`) that wire it up.
3//!
4//! Only compiled with the `native` feature (the default). A short connect timeout
5//! makes the `/health` ladder probes fall through fast when no local node is up.
6
7use crate::error::Result;
8use crate::resolver::{ResolveOptions, ResolveOutcome, Resolver};
9use crate::transport::{HttpResponse, HttpTransport, TransportError};
10use async_trait::async_trait;
11use std::time::Duration;
12
13/// A [`HttpTransport`] backed by `reqwest`. Cheap to clone/reuse.
14#[derive(Clone)]
15pub struct ReqwestTransport {
16    client: reqwest::Client,
17}
18
19impl ReqwestTransport {
20    /// Build a transport with sensible timeouts (fast connect so dead ladder tiers
21    /// fall through quickly; a generous overall budget for large assets).
22    pub fn new() -> Self {
23        let client = reqwest::Client::builder()
24            .connect_timeout(Duration::from_secs(2))
25            .timeout(Duration::from_secs(30))
26            .build()
27            .expect("reqwest client builds with default TLS");
28        ReqwestTransport { client }
29    }
30}
31
32impl Default for ReqwestTransport {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38// Local alias so the impls read cleanly without leaking into the public API.
39type Result0<T> = core::result::Result<T, TransportError>;
40
41/// Collect a `reqwest` response into the transport-agnostic [`HttpResponse`].
42async fn collect(resp: reqwest::Response) -> Result0<HttpResponse> {
43    let status = resp.status().as_u16();
44    let headers = resp
45        .headers()
46        .iter()
47        .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
48        .collect();
49    let body = resp
50        .bytes()
51        .await
52        .map_err(|e| TransportError(e.to_string()))?
53        .to_vec();
54    Ok(HttpResponse {
55        status,
56        headers,
57        body,
58    })
59}
60
61#[async_trait(?Send)]
62impl HttpTransport for ReqwestTransport {
63    async fn get(&self, url: &str) -> Result0<HttpResponse> {
64        let resp = self
65            .client
66            .get(url)
67            .send()
68            .await
69            .map_err(|e| TransportError(e.to_string()))?;
70        collect(resp).await
71    }
72
73    async fn post_json(&self, url: &str, body: String) -> Result0<HttpResponse> {
74        let resp = self
75            .client
76            .post(url)
77            .header("content-type", "application/json")
78            .body(body)
79            .send()
80            .await
81            .map_err(|e| TransportError(e.to_string()))?;
82        collect(resp).await
83    }
84}
85
86/// Resolve a DIG URN with the default `reqwest` transport and default options.
87pub async fn resolve(urn: &str) -> Result<ResolveOutcome> {
88    resolve_with(urn, ResolveOptions::default()).await
89}
90
91/// Resolve a DIG URN with the default `reqwest` transport and explicit options.
92pub async fn resolve_with(urn: &str, options: ResolveOptions) -> Result<ResolveOutcome> {
93    Resolver::with_options(ReqwestTransport::new(), options)
94        .resolve(urn)
95        .await
96}
97
98/// Blocking convenience: resolve on a private current-thread tokio runtime. For
99/// callers outside an async context (a CLI, a sync FFI boundary).
100pub fn resolve_blocking(urn: &str) -> Result<ResolveOutcome> {
101    let rt = tokio::runtime::Builder::new_current_thread()
102        .enable_all()
103        .build()
104        .expect("current-thread tokio runtime builds");
105    rt.block_on(resolve(urn))
106}