parent
d36ebc8c74
commit
855048041d
89 changed files with 184 additions and 180 deletions
28
crates/http_client/Cargo.toml
Normal file
28
crates/http_client/Cargo.toml
Normal file
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "http_client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[lib]
|
||||
path = "src/http_client.rs"
|
||||
doctest = true
|
||||
|
||||
[dependencies]
|
||||
http = "1.0.0"
|
||||
anyhow.workspace = true
|
||||
derive_more.workspace = true
|
||||
futures.workspace = true
|
||||
isahc.workspace = true
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
futures-lite.workspace = true
|
||||
url.workspace = true
|
1
crates/http_client/LICENSE-APACHE
Symbolic link
1
crates/http_client/LICENSE-APACHE
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE-APACHE
|
149
crates/http_client/src/github.rs
Normal file
149
crates/http_client/src/github.rs
Normal file
|
@ -0,0 +1,149 @@
|
|||
use crate::HttpClient;
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use futures::AsyncReadExt;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use url::Url;
|
||||
|
||||
pub struct GitHubLspBinaryVersion {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct GithubRelease {
|
||||
pub tag_name: String,
|
||||
#[serde(rename = "prerelease")]
|
||||
pub pre_release: bool,
|
||||
pub assets: Vec<GithubReleaseAsset>,
|
||||
pub tarball_url: String,
|
||||
pub zipball_url: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct GithubReleaseAsset {
|
||||
pub name: String,
|
||||
pub browser_download_url: String,
|
||||
}
|
||||
|
||||
pub async fn latest_github_release(
|
||||
repo_name_with_owner: &str,
|
||||
require_assets: bool,
|
||||
pre_release: bool,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> Result<GithubRelease, anyhow::Error> {
|
||||
let mut response = http
|
||||
.get(
|
||||
&format!("https://api.github.com/repos/{repo_name_with_owner}/releases"),
|
||||
Default::default(),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.context("error fetching latest release")?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading latest release")?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
}
|
||||
|
||||
let releases = match serde_json::from_slice::<Vec<GithubRelease>>(body.as_slice()) {
|
||||
Ok(releases) => releases,
|
||||
|
||||
Err(err) => {
|
||||
log::error!("Error deserializing: {:?}", err);
|
||||
log::error!(
|
||||
"GitHub API response text: {:?}",
|
||||
String::from_utf8_lossy(body.as_slice())
|
||||
);
|
||||
return Err(anyhow!("error deserializing latest release"));
|
||||
}
|
||||
};
|
||||
|
||||
releases
|
||||
.into_iter()
|
||||
.filter(|release| !require_assets || !release.assets.is_empty())
|
||||
.find(|release| release.pre_release == pre_release)
|
||||
.ok_or(anyhow!("Failed to find a release"))
|
||||
}
|
||||
|
||||
pub async fn get_release_by_tag_name(
|
||||
repo_name_with_owner: &str,
|
||||
tag: &str,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> Result<GithubRelease, anyhow::Error> {
|
||||
let mut response = http
|
||||
.get(
|
||||
&format!("https://api.github.com/repos/{repo_name_with_owner}/releases/tags/{tag}"),
|
||||
Default::default(),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.context("error fetching latest release")?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading latest release")?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
}
|
||||
|
||||
let release = serde_json::from_slice::<GithubRelease>(body.as_slice()).map_err(|err| {
|
||||
log::error!("Error deserializing: {:?}", err);
|
||||
log::error!(
|
||||
"GitHub API response text: {:?}",
|
||||
String::from_utf8_lossy(body.as_slice())
|
||||
);
|
||||
anyhow!("error deserializing GitHub release")
|
||||
})?;
|
||||
|
||||
Ok(release)
|
||||
}
|
||||
|
||||
pub fn build_tarball_url(repo_name_with_owner: &str, tag: &str) -> Result<String> {
|
||||
let mut url = Url::parse(&format!(
|
||||
"https://github.com/{repo_name_with_owner}/archive/refs/tags",
|
||||
))?;
|
||||
// We're pushing this here, because tags may contain `/` and other characters
|
||||
// that need to be escaped.
|
||||
let tarball_filename = format!("{}.tar.gz", tag);
|
||||
url.path_segments_mut()
|
||||
.map_err(|_| anyhow!("cannot modify url path segments"))?
|
||||
.push(&tarball_filename);
|
||||
Ok(url.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::github::build_tarball_url;
|
||||
|
||||
#[test]
|
||||
fn test_build_tarball_url() {
|
||||
let tag = "release/2.3.5";
|
||||
let repo_name_with_owner = "microsoft/vscode-eslint";
|
||||
|
||||
let have = build_tarball_url(repo_name_with_owner, tag).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
have,
|
||||
"https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.tar.gz"
|
||||
);
|
||||
}
|
||||
}
|
319
crates/http_client/src/http_client.rs
Normal file
319
crates/http_client/src/http_client.rs
Normal file
|
@ -0,0 +1,319 @@
|
|||
pub mod github;
|
||||
|
||||
pub use anyhow::{anyhow, Result};
|
||||
use derive_more::Deref;
|
||||
use futures::future::BoxFuture;
|
||||
use futures_lite::FutureExt;
|
||||
use isahc::config::{Configurable, RedirectPolicy};
|
||||
pub use isahc::{
|
||||
http::{Method, StatusCode, Uri},
|
||||
AsyncBody, Error, HttpClient as IsahcHttpClient, Request, Response,
|
||||
};
|
||||
#[cfg(feature = "test-support")]
|
||||
use std::fmt;
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
pub use url::Url;
|
||||
|
||||
pub trait HttpClient: Send + Sync {
|
||||
fn send(
|
||||
&self,
|
||||
req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>;
|
||||
|
||||
fn get<'a>(
|
||||
&'a self,
|
||||
uri: &str,
|
||||
body: AsyncBody,
|
||||
follow_redirects: bool,
|
||||
) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
|
||||
let request = isahc::Request::builder()
|
||||
.redirect_policy(if follow_redirects {
|
||||
RedirectPolicy::Follow
|
||||
} else {
|
||||
RedirectPolicy::None
|
||||
})
|
||||
.method(Method::GET)
|
||||
.uri(uri)
|
||||
.body(body);
|
||||
match request {
|
||||
Ok(request) => self.send(request),
|
||||
Err(error) => async move { Err(error.into()) }.boxed(),
|
||||
}
|
||||
}
|
||||
|
||||
fn post_json<'a>(
|
||||
&'a self,
|
||||
uri: &str,
|
||||
body: AsyncBody,
|
||||
) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
|
||||
let request = isahc::Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body);
|
||||
match request {
|
||||
Ok(request) => self.send(request),
|
||||
Err(error) => async move { Err(error.into()) }.boxed(),
|
||||
}
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Uri>;
|
||||
}
|
||||
|
||||
/// An [`HttpClient`] that may have a proxy.
|
||||
#[derive(Deref)]
|
||||
pub struct HttpClientWithProxy {
|
||||
#[deref]
|
||||
client: Arc<dyn HttpClient>,
|
||||
proxy: Option<Uri>,
|
||||
}
|
||||
|
||||
impl HttpClientWithProxy {
|
||||
/// Returns a new [`HttpClientWithProxy`] with the given proxy URL.
|
||||
pub fn new(proxy_url: Option<String>) -> Self {
|
||||
let proxy_url = proxy_url
|
||||
.and_then(|input| {
|
||||
input
|
||||
.parse::<Uri>()
|
||||
.inspect_err(|e| log::error!("Error parsing proxy settings: {}", e))
|
||||
.ok()
|
||||
})
|
||||
.or_else(read_proxy_from_env);
|
||||
|
||||
Self {
|
||||
client: client(proxy_url.clone()),
|
||||
proxy: proxy_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for HttpClientWithProxy {
|
||||
fn send(
|
||||
&self,
|
||||
req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
|
||||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Uri> {
|
||||
self.proxy.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for Arc<HttpClientWithProxy> {
|
||||
fn send(
|
||||
&self,
|
||||
req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
|
||||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Uri> {
|
||||
self.proxy.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`HttpClient`] that has a base URL.
|
||||
pub struct HttpClientWithUrl {
|
||||
base_url: Mutex<String>,
|
||||
client: HttpClientWithProxy,
|
||||
}
|
||||
|
||||
impl HttpClientWithUrl {
|
||||
/// Returns a new [`HttpClientWithUrl`] with the given base URL.
|
||||
pub fn new(base_url: impl Into<String>, proxy_url: Option<String>) -> Self {
|
||||
let client = HttpClientWithProxy::new(proxy_url);
|
||||
|
||||
Self {
|
||||
base_url: Mutex::new(base_url.into()),
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the base URL.
|
||||
pub fn base_url(&self) -> String {
|
||||
self.base_url
|
||||
.lock()
|
||||
.map_or_else(|_| Default::default(), |url| url.clone())
|
||||
}
|
||||
|
||||
/// Sets the base URL.
|
||||
pub fn set_base_url(&self, base_url: impl Into<String>) {
|
||||
let base_url = base_url.into();
|
||||
self.base_url
|
||||
.lock()
|
||||
.map(|mut url| {
|
||||
*url = base_url;
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
/// Builds a URL using the given path.
|
||||
pub fn build_url(&self, path: &str) -> String {
|
||||
format!("{}{}", self.base_url(), path)
|
||||
}
|
||||
|
||||
/// Builds a Zed API URL using the given path.
|
||||
pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
|
||||
let base_url = self.base_url();
|
||||
let base_api_url = match base_url.as_ref() {
|
||||
"https://zed.dev" => "https://api.zed.dev",
|
||||
"https://staging.zed.dev" => "https://api-staging.zed.dev",
|
||||
"http://localhost:3000" => "http://localhost:8080",
|
||||
other => other,
|
||||
};
|
||||
|
||||
Ok(Url::parse_with_params(
|
||||
&format!("{}{}", base_api_url, path),
|
||||
query,
|
||||
)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for Arc<HttpClientWithUrl> {
|
||||
fn send(
|
||||
&self,
|
||||
req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
|
||||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Uri> {
|
||||
self.client.proxy.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for HttpClientWithUrl {
|
||||
fn send(
|
||||
&self,
|
||||
req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
|
||||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Uri> {
|
||||
self.client.proxy.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client(proxy: Option<Uri>) -> Arc<dyn HttpClient> {
|
||||
Arc::new(HttpClientWithProxy {
|
||||
client: Arc::new(
|
||||
isahc::HttpClient::builder()
|
||||
.connect_timeout(Duration::from_secs(5))
|
||||
.low_speed_timeout(100, Duration::from_secs(5))
|
||||
.proxy(proxy.clone())
|
||||
.build()
|
||||
.unwrap(),
|
||||
),
|
||||
proxy,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_proxy_from_env() -> Option<Uri> {
|
||||
const ENV_VARS: &[&str] = &[
|
||||
"ALL_PROXY",
|
||||
"all_proxy",
|
||||
"HTTPS_PROXY",
|
||||
"https_proxy",
|
||||
"HTTP_PROXY",
|
||||
"http_proxy",
|
||||
];
|
||||
|
||||
for var in ENV_VARS {
|
||||
if let Ok(env) = std::env::var(var) {
|
||||
return env.parse::<Uri>().ok();
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
impl HttpClient for isahc::HttpClient {
|
||||
fn send(
|
||||
&self,
|
||||
req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
|
||||
let client = self.clone();
|
||||
Box::pin(async move { client.send_async(req).await })
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Uri> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
type FakeHttpHandler = Box<
|
||||
dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
>;
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub struct FakeHttpClient {
|
||||
handler: FakeHttpHandler,
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
impl FakeHttpClient {
|
||||
pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
|
||||
where
|
||||
Fut: futures::Future<Output = Result<Response<AsyncBody>, Error>> + Send + 'static,
|
||||
F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
|
||||
{
|
||||
Arc::new(HttpClientWithUrl {
|
||||
base_url: Mutex::new("http://test.example".into()),
|
||||
client: HttpClientWithProxy {
|
||||
client: Arc::new(Self {
|
||||
handler: Box::new(move |req| Box::pin(handler(req))),
|
||||
}),
|
||||
proxy: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_404_response() -> Arc<HttpClientWithUrl> {
|
||||
Self::create(|_| async move {
|
||||
Ok(Response::builder()
|
||||
.status(404)
|
||||
.body(Default::default())
|
||||
.unwrap())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_200_response() -> Arc<HttpClientWithUrl> {
|
||||
Self::create(|_| async move {
|
||||
Ok(Response::builder()
|
||||
.status(200)
|
||||
.body(Default::default())
|
||||
.unwrap())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
impl fmt::Debug for FakeHttpClient {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("FakeHttpClient").finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
impl HttpClient for FakeHttpClient {
|
||||
fn send(
|
||||
&self,
|
||||
req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
|
||||
let future = (self.handler)(req);
|
||||
Box::pin(async move { future.await.map(Into::into) })
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Uri> {
|
||||
None
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue