http.rs

  1use crate::http_proxy_from_env;
  2pub use anyhow::{anyhow, Result};
  3use futures::future::BoxFuture;
  4use isahc::config::{Configurable, RedirectPolicy};
  5pub use isahc::{
  6    http::{Method, StatusCode, Uri},
  7    Error,
  8};
  9pub use isahc::{AsyncBody, Request, Response};
 10use parking_lot::Mutex;
 11use smol::future::FutureExt;
 12#[cfg(feature = "test-support")]
 13use std::fmt;
 14use std::{sync::Arc, time::Duration};
 15pub use url::Url;
 16
 17pub struct ZedHttpClient {
 18    pub zed_host: Mutex<String>,
 19    client: Box<dyn HttpClient>,
 20}
 21
 22impl ZedHttpClient {
 23    pub fn zed_url(&self, path: &str) -> String {
 24        format!("{}{}", self.zed_host.lock(), path)
 25    }
 26}
 27
 28impl HttpClient for Arc<ZedHttpClient> {
 29    fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>> {
 30        self.client.send(req)
 31    }
 32}
 33
 34impl HttpClient for ZedHttpClient {
 35    fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>> {
 36        self.client.send(req)
 37    }
 38}
 39
 40pub fn zed_client(zed_host: &str) -> Arc<ZedHttpClient> {
 41    Arc::new(ZedHttpClient {
 42        zed_host: Mutex::new(zed_host.to_string()),
 43        client: Box::new(
 44            isahc::HttpClient::builder()
 45                .connect_timeout(Duration::from_secs(5))
 46                .low_speed_timeout(100, Duration::from_secs(5))
 47                .proxy(http_proxy_from_env())
 48                .build()
 49                .unwrap(),
 50        ),
 51    })
 52}
 53
 54pub trait HttpClient: Send + Sync {
 55    fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>>;
 56
 57    fn get<'a>(
 58        &'a self,
 59        uri: &str,
 60        body: AsyncBody,
 61        follow_redirects: bool,
 62    ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
 63        let request = isahc::Request::builder()
 64            .redirect_policy(if follow_redirects {
 65                RedirectPolicy::Follow
 66            } else {
 67                RedirectPolicy::None
 68            })
 69            .method(Method::GET)
 70            .uri(uri)
 71            .body(body);
 72        match request {
 73            Ok(request) => self.send(request),
 74            Err(error) => async move { Err(error.into()) }.boxed(),
 75        }
 76    }
 77
 78    fn post_json<'a>(
 79        &'a self,
 80        uri: &str,
 81        body: AsyncBody,
 82    ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
 83        let request = isahc::Request::builder()
 84            .method(Method::POST)
 85            .uri(uri)
 86            .header("Content-Type", "application/json")
 87            .body(body);
 88        match request {
 89            Ok(request) => self.send(request),
 90            Err(error) => async move { Err(error.into()) }.boxed(),
 91        }
 92    }
 93}
 94
 95pub fn client() -> Arc<dyn HttpClient> {
 96    Arc::new(
 97        isahc::HttpClient::builder()
 98            .connect_timeout(Duration::from_secs(5))
 99            .low_speed_timeout(100, Duration::from_secs(5))
100            .proxy(http_proxy_from_env())
101            .build()
102            .unwrap(),
103    )
104}
105
106impl HttpClient for isahc::HttpClient {
107    fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>> {
108        Box::pin(async move { self.send_async(req).await })
109    }
110}
111
112#[cfg(feature = "test-support")]
113pub struct FakeHttpClient {
114    handler: Box<
115        dyn 'static
116            + Send
117            + Sync
118            + Fn(Request<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>,
119    >,
120}
121
122#[cfg(feature = "test-support")]
123impl FakeHttpClient {
124    pub fn create<Fut, F>(handler: F) -> Arc<ZedHttpClient>
125    where
126        Fut: 'static + Send + futures::Future<Output = Result<Response<AsyncBody>, Error>>,
127        F: 'static + Send + Sync + Fn(Request<AsyncBody>) -> Fut,
128    {
129        Arc::new(ZedHttpClient {
130            zed_host: Mutex::new("http://test.example".into()),
131            client: Box::new(Self {
132                handler: Box::new(move |req| Box::pin(handler(req))),
133            }),
134        })
135    }
136
137    pub fn with_404_response() -> Arc<ZedHttpClient> {
138        Self::create(|_| async move {
139            Ok(Response::builder()
140                .status(404)
141                .body(Default::default())
142                .unwrap())
143        })
144    }
145
146    pub fn with_200_response() -> Arc<ZedHttpClient> {
147        Self::create(|_| async move {
148            Ok(Response::builder()
149                .status(200)
150                .body(Default::default())
151                .unwrap())
152        })
153    }
154}
155
156#[cfg(feature = "test-support")]
157impl fmt::Debug for FakeHttpClient {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        f.debug_struct("FakeHttpClient").finish()
160    }
161}
162
163#[cfg(feature = "test-support")]
164impl HttpClient for FakeHttpClient {
165    fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>> {
166        let future = (self.handler)(req);
167        Box::pin(async move { future.await.map(Into::into) })
168    }
169}