1pub mod github;
2
3pub use anyhow::{anyhow, Result};
4use futures::future::BoxFuture;
5use futures_lite::FutureExt;
6use isahc::config::{Configurable, RedirectPolicy};
7pub use isahc::{
8 http::{Method, StatusCode, Uri},
9 AsyncBody, Error, HttpClient as IsahcHttpClient, Request, Response,
10};
11#[cfg(feature = "test-support")]
12use std::fmt;
13use std::{
14 sync::{Arc, Mutex},
15 time::Duration,
16};
17pub use url::Url;
18
19fn get_proxy(proxy: Option<String>) -> Option<isahc::http::Uri> {
20 macro_rules! try_env {
21 ($($env:literal),+) => {
22 $(
23 if let Ok(env) = std::env::var($env) {
24 return env.parse::<isahc::http::Uri>().ok();
25 }
26 )+
27 };
28 }
29
30 proxy
31 .and_then(|input| {
32 input
33 .parse::<isahc::http::Uri>()
34 .inspect_err(|e| log::error!("Error parsing proxy settings: {}", e))
35 .ok()
36 })
37 .or_else(|| {
38 try_env!(
39 "ALL_PROXY",
40 "all_proxy",
41 "HTTPS_PROXY",
42 "https_proxy",
43 "HTTP_PROXY",
44 "http_proxy"
45 );
46 None
47 })
48}
49
50/// An [`HttpClient`] that has a base URL.
51pub struct HttpClientWithUrl {
52 base_url: Mutex<String>,
53 client: Arc<dyn HttpClient>,
54 proxy: Option<String>,
55}
56
57impl HttpClientWithUrl {
58 /// Returns a new [`HttpClientWithUrl`] with the given base URL.
59 pub fn new(base_url: impl Into<String>, unparsed_proxy: Option<String>) -> Self {
60 let parsed_proxy = get_proxy(unparsed_proxy);
61 let proxy_string = parsed_proxy.as_ref().map(|p| p.to_string());
62 Self {
63 base_url: Mutex::new(base_url.into()),
64 client: client(parsed_proxy),
65 proxy: proxy_string,
66 }
67 }
68
69 /// Returns the base URL.
70 pub fn base_url(&self) -> String {
71 self.base_url
72 .lock()
73 .map_or_else(|_| Default::default(), |url| url.clone())
74 }
75
76 /// Sets the base URL.
77 pub fn set_base_url(&self, base_url: impl Into<String>) {
78 let base_url = base_url.into();
79 self.base_url
80 .lock()
81 .map(|mut url| {
82 *url = base_url;
83 })
84 .ok();
85 }
86
87 /// Builds a URL using the given path.
88 pub fn build_url(&self, path: &str) -> String {
89 format!("{}{}", self.base_url(), path)
90 }
91
92 /// Builds a Zed API URL using the given path.
93 pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
94 let base_url = self.base_url();
95 let base_api_url = match base_url.as_ref() {
96 "https://zed.dev" => "https://api.zed.dev",
97 "https://staging.zed.dev" => "https://api-staging.zed.dev",
98 "http://localhost:3000" => "http://localhost:8080",
99 other => other,
100 };
101
102 Ok(Url::parse_with_params(
103 &format!("{}{}", base_api_url, path),
104 query,
105 )?)
106 }
107}
108
109impl HttpClient for Arc<HttpClientWithUrl> {
110 fn send(
111 &self,
112 req: Request<AsyncBody>,
113 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
114 self.client.send(req)
115 }
116
117 fn proxy(&self) -> Option<&str> {
118 self.proxy.as_deref()
119 }
120}
121
122impl HttpClient for HttpClientWithUrl {
123 fn send(
124 &self,
125 req: Request<AsyncBody>,
126 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
127 self.client.send(req)
128 }
129
130 fn proxy(&self) -> Option<&str> {
131 self.proxy.as_deref()
132 }
133}
134
135pub trait HttpClient: Send + Sync {
136 fn send(
137 &self,
138 req: Request<AsyncBody>,
139 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>;
140
141 fn get<'a>(
142 &'a self,
143 uri: &str,
144 body: AsyncBody,
145 follow_redirects: bool,
146 ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
147 let request = isahc::Request::builder()
148 .redirect_policy(if follow_redirects {
149 RedirectPolicy::Follow
150 } else {
151 RedirectPolicy::None
152 })
153 .method(Method::GET)
154 .uri(uri)
155 .body(body);
156 match request {
157 Ok(request) => self.send(request),
158 Err(error) => async move { Err(error.into()) }.boxed(),
159 }
160 }
161
162 fn post_json<'a>(
163 &'a self,
164 uri: &str,
165 body: AsyncBody,
166 ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
167 let request = isahc::Request::builder()
168 .method(Method::POST)
169 .uri(uri)
170 .header("Content-Type", "application/json")
171 .body(body);
172 match request {
173 Ok(request) => self.send(request),
174 Err(error) => async move { Err(error.into()) }.boxed(),
175 }
176 }
177
178 fn proxy(&self) -> Option<&str>;
179}
180
181pub fn client(proxy: Option<isahc::http::Uri>) -> Arc<dyn HttpClient> {
182 Arc::new(
183 isahc::HttpClient::builder()
184 .connect_timeout(Duration::from_secs(5))
185 .low_speed_timeout(100, Duration::from_secs(5))
186 .proxy(proxy)
187 .build()
188 .unwrap(),
189 )
190}
191
192impl HttpClient for isahc::HttpClient {
193 fn send(
194 &self,
195 req: Request<AsyncBody>,
196 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
197 let client = self.clone();
198 Box::pin(async move { client.send_async(req).await })
199 }
200
201 fn proxy(&self) -> Option<&str> {
202 None
203 }
204}
205
206#[cfg(feature = "test-support")]
207type FakeHttpHandler = Box<
208 dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>
209 + Send
210 + Sync
211 + 'static,
212>;
213
214#[cfg(feature = "test-support")]
215pub struct FakeHttpClient {
216 handler: FakeHttpHandler,
217}
218
219#[cfg(feature = "test-support")]
220impl FakeHttpClient {
221 pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
222 where
223 Fut: futures::Future<Output = Result<Response<AsyncBody>, Error>> + Send + 'static,
224 F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
225 {
226 Arc::new(HttpClientWithUrl {
227 base_url: Mutex::new("http://test.example".into()),
228 client: Arc::new(Self {
229 handler: Box::new(move |req| Box::pin(handler(req))),
230 }),
231 proxy: None,
232 })
233 }
234
235 pub fn with_404_response() -> Arc<HttpClientWithUrl> {
236 Self::create(|_| async move {
237 Ok(Response::builder()
238 .status(404)
239 .body(Default::default())
240 .unwrap())
241 })
242 }
243
244 pub fn with_200_response() -> Arc<HttpClientWithUrl> {
245 Self::create(|_| async move {
246 Ok(Response::builder()
247 .status(200)
248 .body(Default::default())
249 .unwrap())
250 })
251 }
252}
253
254#[cfg(feature = "test-support")]
255impl fmt::Debug for FakeHttpClient {
256 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257 f.debug_struct("FakeHttpClient").finish()
258 }
259}
260
261#[cfg(feature = "test-support")]
262impl HttpClient for FakeHttpClient {
263 fn send(
264 &self,
265 req: Request<AsyncBody>,
266 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
267 let future = (self.handler)(req);
268 Box::pin(async move { future.await.map(Into::into) })
269 }
270
271 fn proxy(&self) -> Option<&str> {
272 None
273 }
274}