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 .build()
48 .unwrap(),
49 ),
50 })
51}
52
53pub trait HttpClient: Send + Sync {
54 fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>>;
55
56 fn get<'a>(
57 &'a self,
58 uri: &str,
59 body: AsyncBody,
60 follow_redirects: bool,
61 ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
62 let request = isahc::Request::builder()
63 .redirect_policy(if follow_redirects {
64 RedirectPolicy::Follow
65 } else {
66 RedirectPolicy::None
67 })
68 .method(Method::GET)
69 .uri(uri)
70 .body(body);
71 match request {
72 Ok(request) => self.send(request),
73 Err(error) => async move { Err(error.into()) }.boxed(),
74 }
75 }
76
77 fn post_json<'a>(
78 &'a self,
79 uri: &str,
80 body: AsyncBody,
81 ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
82 let request = isahc::Request::builder()
83 .method(Method::POST)
84 .uri(uri)
85 .header("Content-Type", "application/json")
86 .body(body);
87 match request {
88 Ok(request) => self.send(request),
89 Err(error) => async move { Err(error.into()) }.boxed(),
90 }
91 }
92}
93
94pub fn client() -> Arc<dyn HttpClient> {
95 Arc::new(
96 isahc::HttpClient::builder()
97 .connect_timeout(Duration::from_secs(5))
98 .low_speed_timeout(100, Duration::from_secs(5))
99 .proxy(http_proxy_from_env())
100 .build()
101 .unwrap(),
102 )
103}
104
105impl HttpClient for isahc::HttpClient {
106 fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>> {
107 Box::pin(async move { self.send_async(req).await })
108 }
109}
110
111#[cfg(feature = "test-support")]
112pub struct FakeHttpClient {
113 handler: Box<
114 dyn 'static
115 + Send
116 + Sync
117 + Fn(Request<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>,
118 >,
119}
120
121#[cfg(feature = "test-support")]
122impl FakeHttpClient {
123 pub fn create<Fut, F>(handler: F) -> Arc<ZedHttpClient>
124 where
125 Fut: 'static + Send + futures::Future<Output = Result<Response<AsyncBody>, Error>>,
126 F: 'static + Send + Sync + Fn(Request<AsyncBody>) -> Fut,
127 {
128 Arc::new(ZedHttpClient {
129 zed_host: Mutex::new("http://test.example".into()),
130 client: Box::new(Self {
131 handler: Box::new(move |req| Box::pin(handler(req))),
132 }),
133 })
134 }
135
136 pub fn with_404_response() -> Arc<ZedHttpClient> {
137 Self::create(|_| async move {
138 Ok(Response::builder()
139 .status(404)
140 .body(Default::default())
141 .unwrap())
142 })
143 }
144
145 pub fn with_200_response() -> Arc<ZedHttpClient> {
146 Self::create(|_| async move {
147 Ok(Response::builder()
148 .status(200)
149 .body(Default::default())
150 .unwrap())
151 })
152 }
153}
154
155#[cfg(feature = "test-support")]
156impl fmt::Debug for FakeHttpClient {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 f.debug_struct("FakeHttpClient").finish()
159 }
160}
161
162#[cfg(feature = "test-support")]
163impl HttpClient for FakeHttpClient {
164 fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>> {
165 let future = (self.handler)(req);
166 Box::pin(async move { future.await.map(Into::into) })
167 }
168}