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 http_proxy_from_env() -> 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 try_env!(
31 "ALL_PROXY",
32 "all_proxy",
33 "HTTPS_PROXY",
34 "https_proxy",
35 "HTTP_PROXY",
36 "http_proxy"
37 );
38 None
39}
40
41/// An [`HttpClient`] that has a base URL.
42pub struct HttpClientWithUrl {
43 base_url: Mutex<String>,
44 client: Arc<dyn HttpClient>,
45}
46
47impl HttpClientWithUrl {
48 /// Returns a new [`HttpClientWithUrl`] with the given base URL.
49 pub fn new(base_url: impl Into<String>) -> Self {
50 Self {
51 base_url: Mutex::new(base_url.into()),
52 client: client(),
53 }
54 }
55
56 /// Returns the base URL.
57 pub fn base_url(&self) -> String {
58 self.base_url
59 .lock()
60 .map_or_else(|_| Default::default(), |url| url.clone())
61 }
62
63 /// Sets the base URL.
64 pub fn set_base_url(&self, base_url: impl Into<String>) {
65 let base_url = base_url.into();
66 self.base_url
67 .lock()
68 .map(|mut url| {
69 *url = base_url;
70 })
71 .ok();
72 }
73
74 /// Builds a URL using the given path.
75 pub fn build_url(&self, path: &str) -> String {
76 format!("{}{}", self.base_url(), path)
77 }
78
79 /// Builds a Zed API URL using the given path.
80 pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
81 let base_url = self.base_url();
82 let base_api_url = match base_url.as_ref() {
83 "https://zed.dev" => "https://api.zed.dev",
84 "https://staging.zed.dev" => "https://api-staging.zed.dev",
85 "http://localhost:3000" => "http://localhost:8080",
86 other => other,
87 };
88
89 Ok(Url::parse_with_params(
90 &format!("{}{}", base_api_url, path),
91 query,
92 )?)
93 }
94}
95
96impl HttpClient for Arc<HttpClientWithUrl> {
97 fn send(
98 &self,
99 req: Request<AsyncBody>,
100 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
101 self.client.send(req)
102 }
103}
104
105impl HttpClient for HttpClientWithUrl {
106 fn send(
107 &self,
108 req: Request<AsyncBody>,
109 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
110 self.client.send(req)
111 }
112}
113
114pub trait HttpClient: Send + Sync {
115 fn send(
116 &self,
117 req: Request<AsyncBody>,
118 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>;
119
120 fn get<'a>(
121 &'a self,
122 uri: &str,
123 body: AsyncBody,
124 follow_redirects: bool,
125 ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
126 let request = isahc::Request::builder()
127 .redirect_policy(if follow_redirects {
128 RedirectPolicy::Follow
129 } else {
130 RedirectPolicy::None
131 })
132 .method(Method::GET)
133 .uri(uri)
134 .body(body);
135 match request {
136 Ok(request) => self.send(request),
137 Err(error) => async move { Err(error.into()) }.boxed(),
138 }
139 }
140
141 fn post_json<'a>(
142 &'a self,
143 uri: &str,
144 body: AsyncBody,
145 ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
146 let request = isahc::Request::builder()
147 .method(Method::POST)
148 .uri(uri)
149 .header("Content-Type", "application/json")
150 .body(body);
151 match request {
152 Ok(request) => self.send(request),
153 Err(error) => async move { Err(error.into()) }.boxed(),
154 }
155 }
156}
157
158pub fn client() -> Arc<dyn HttpClient> {
159 Arc::new(
160 isahc::HttpClient::builder()
161 .connect_timeout(Duration::from_secs(5))
162 .low_speed_timeout(100, Duration::from_secs(5))
163 .proxy(http_proxy_from_env())
164 .build()
165 .unwrap(),
166 )
167}
168
169impl HttpClient for isahc::HttpClient {
170 fn send(
171 &self,
172 req: Request<AsyncBody>,
173 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
174 let client = self.clone();
175 Box::pin(async move { client.send_async(req).await })
176 }
177}
178
179#[cfg(feature = "test-support")]
180type FakeHttpHandler = Box<
181 dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>
182 + Send
183 + Sync
184 + 'static,
185>;
186
187#[cfg(feature = "test-support")]
188pub struct FakeHttpClient {
189 handler: FakeHttpHandler,
190}
191
192#[cfg(feature = "test-support")]
193impl FakeHttpClient {
194 pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
195 where
196 Fut: futures::Future<Output = Result<Response<AsyncBody>, Error>> + Send + 'static,
197 F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
198 {
199 Arc::new(HttpClientWithUrl {
200 base_url: Mutex::new("http://test.example".into()),
201 client: Arc::new(Self {
202 handler: Box::new(move |req| Box::pin(handler(req))),
203 }),
204 })
205 }
206
207 pub fn with_404_response() -> Arc<HttpClientWithUrl> {
208 Self::create(|_| async move {
209 Ok(Response::builder()
210 .status(404)
211 .body(Default::default())
212 .unwrap())
213 })
214 }
215
216 pub fn with_200_response() -> Arc<HttpClientWithUrl> {
217 Self::create(|_| async move {
218 Ok(Response::builder()
219 .status(200)
220 .body(Default::default())
221 .unwrap())
222 })
223 }
224}
225
226#[cfg(feature = "test-support")]
227impl fmt::Debug for FakeHttpClient {
228 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229 f.debug_struct("FakeHttpClient").finish()
230 }
231}
232
233#[cfg(feature = "test-support")]
234impl HttpClient for FakeHttpClient {
235 fn send(
236 &self,
237 req: Request<AsyncBody>,
238 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
239 let future = (self.handler)(req);
240 Box::pin(async move { future.await.map(Into::into) })
241 }
242}