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