This commit is contained in:
2026-05-18 13:58:36 -04:00
parent d076dad356
commit 68a4507dbe
143 changed files with 15715 additions and 7204 deletions
@@ -0,0 +1,149 @@
use std::{
io::{Cursor, Read},
pin::Pin,
task::Poll,
};
use bytes::Bytes;
use futures::AsyncRead;
use http_body::{Body, Frame};
/// Based on the implementation of AsyncBody in
/// <https://github.com/sagebind/isahc/blob/5c533f1ef4d6bdf1fd291b5103c22110f41d0bf0/src/body/mod.rs>.
pub struct AsyncBody(pub Inner);
pub enum Inner {
/// An empty body.
Empty,
/// A body stored in memory.
Bytes(std::io::Cursor<Bytes>),
/// An asynchronous reader.
AsyncReader(Pin<Box<dyn futures::AsyncRead + Send + Sync>>),
}
impl AsyncBody {
/// Create a new empty body.
///
/// An empty body represents the *absence* of a body, which is semantically
/// different than the presence of a body of zero length.
pub fn empty() -> Self {
Self(Inner::Empty)
}
/// Create a streaming body that reads from the given reader.
pub fn from_reader<R>(read: R) -> Self
where
R: AsyncRead + Send + Sync + 'static,
{
Self(Inner::AsyncReader(Box::pin(read)))
}
pub fn from_bytes(bytes: Bytes) -> Self {
Self(Inner::Bytes(Cursor::new(bytes)))
}
}
impl Default for AsyncBody {
fn default() -> Self {
Self(Inner::Empty)
}
}
impl From<()> for AsyncBody {
fn from(_: ()) -> Self {
Self(Inner::Empty)
}
}
impl From<Bytes> for AsyncBody {
fn from(bytes: Bytes) -> Self {
Self::from_bytes(bytes)
}
}
impl From<Vec<u8>> for AsyncBody {
fn from(body: Vec<u8>) -> Self {
Self::from_bytes(body.into())
}
}
impl From<String> for AsyncBody {
fn from(body: String) -> Self {
Self::from_bytes(body.into())
}
}
impl From<&'static [u8]> for AsyncBody {
#[inline]
fn from(s: &'static [u8]) -> Self {
Self::from_bytes(Bytes::from_static(s))
}
}
impl From<&'static str> for AsyncBody {
#[inline]
fn from(s: &'static str) -> Self {
Self::from_bytes(Bytes::from_static(s.as_bytes()))
}
}
impl TryFrom<reqwest::Body> for AsyncBody {
type Error = anyhow::Error;
fn try_from(value: reqwest::Body) -> Result<Self, Self::Error> {
value
.as_bytes()
.ok_or_else(|| anyhow::anyhow!("Underlying data is a stream"))
.map(|bytes| Self::from_bytes(Bytes::copy_from_slice(bytes)))
}
}
impl<T: Into<Self>> From<Option<T>> for AsyncBody {
fn from(body: Option<T>) -> Self {
match body {
Some(body) => body.into(),
None => Self::empty(),
}
}
}
impl futures::AsyncRead for AsyncBody {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut [u8],
) -> std::task::Poll<std::io::Result<usize>> {
// SAFETY: Standard Enum pin projection
let inner = unsafe { &mut self.get_unchecked_mut().0 };
match inner {
Inner::Empty => Poll::Ready(Ok(0)),
// Blocking call is over an in-memory buffer
Inner::Bytes(cursor) => Poll::Ready(cursor.read(buf)),
Inner::AsyncReader(async_reader) => {
AsyncRead::poll_read(async_reader.as_mut(), cx, buf)
}
}
}
}
impl Body for AsyncBody {
type Data = Bytes;
type Error = std::io::Error;
fn poll_frame(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
let mut buffer = vec![0; 8192];
match AsyncRead::poll_read(self.as_mut(), cx, &mut buffer) {
Poll::Ready(Ok(0)) => Poll::Ready(None),
Poll::Ready(Ok(n)) => {
let data = Bytes::copy_from_slice(&buffer[..n]);
Poll::Ready(Some(Ok(Frame::data(data))))
}
Poll::Ready(Err(e)) => Poll::Ready(Some(Err(e))),
Poll::Pending => Poll::Pending,
}
}
}
+179
View File
@@ -0,0 +1,179 @@
use crate::HttpClient;
use anyhow::{Context as _, Result, anyhow, bail};
use futures::AsyncReadExt;
use serde::Deserialize;
use std::sync::Arc;
use url::Url;
pub struct GitHubLspBinaryVersion {
pub name: String,
pub url: String,
pub digest: Option<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 digest: Option<String>,
}
pub async fn latest_github_release(
repo_name_with_owner: &str,
require_assets: bool,
pre_release: bool,
http: Arc<dyn HttpClient>,
) -> anyhow::Result<GithubRelease> {
let mut response = http
.get(
format!("https://api.github.com/repos/{repo_name_with_owner}/releases").as_str(),
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())
);
anyhow::bail!("error deserializing latest release: {err:?}");
}
};
let mut release = releases
.into_iter()
.filter(|release| !require_assets || !release.assets.is_empty())
.find(|release| release.pre_release == pre_release)
.context("finding a prerelease")?;
release.assets.iter_mut().for_each(|asset| {
if let Some(digest) = &mut asset.digest
&& let Some(stripped) = digest.strip_prefix("sha256:")
{
*digest = stripped.to_owned();
}
});
Ok(release)
}
pub async fn get_release_by_tag_name(
repo_name_with_owner: &str,
tag: &str,
http: Arc<dyn HttpClient>,
) -> anyhow::Result<GithubRelease> {
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();
let status = response.status();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading latest release")?;
if 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: {err:?}")
})?;
Ok(release)
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum AssetKind {
TarGz,
Gz,
Zip,
}
pub fn build_asset_url(repo_name_with_owner: &str, tag: &str, kind: AssetKind) -> 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 asset_filename = format!(
"{tag}.{extension}",
extension = match kind {
AssetKind::TarGz => "tar.gz",
AssetKind::Gz => "gz",
AssetKind::Zip => "zip",
}
);
url.path_segments_mut()
.map_err(|()| anyhow!("cannot modify url path segments"))?
.push(&asset_filename);
Ok(url.to_string())
}
#[cfg(test)]
mod tests {
use crate::github::{AssetKind, build_asset_url};
#[test]
fn test_build_asset_url() {
let tag = "release/2.3.5";
let repo_name_with_owner = "microsoft/vscode-eslint";
let tarball = build_asset_url(repo_name_with_owner, tag, AssetKind::TarGz).unwrap();
assert_eq!(
tarball,
"https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.tar.gz"
);
let zip = build_asset_url(repo_name_with_owner, tag, AssetKind::Zip).unwrap();
assert_eq!(
zip,
"https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.zip"
);
}
}
@@ -0,0 +1,189 @@
use std::{path::Path, pin::Pin, task::Poll};
use anyhow::{Context, Result};
use async_compression::futures::bufread::GzipDecoder;
use futures::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, io::BufReader};
use sha2::{Digest, Sha256};
use crate::{HttpClient, github::AssetKind};
#[derive(serde::Deserialize, serde::Serialize, Debug)]
pub struct GithubBinaryMetadata {
pub metadata_version: u64,
pub digest: Option<String>,
}
impl GithubBinaryMetadata {
pub async fn read_from_file(metadata_path: &Path) -> Result<GithubBinaryMetadata> {
let metadata_content = async_fs::read_to_string(metadata_path)
.await
.with_context(|| format!("reading metadata file at {metadata_path:?}"))?;
serde_json::from_str(&metadata_content)
.with_context(|| format!("parsing metadata file at {metadata_path:?}"))
}
pub async fn write_to_file(&self, metadata_path: &Path) -> Result<()> {
let metadata_content = serde_json::to_string(self)
.with_context(|| format!("serializing metadata for {metadata_path:?}"))?;
async_fs::write(metadata_path, metadata_content.as_bytes())
.await
.with_context(|| format!("writing metadata file at {metadata_path:?}"))?;
Ok(())
}
}
pub async fn download_server_binary(
http_client: &dyn HttpClient,
url: &str,
digest: Option<&str>,
destination_path: &Path,
asset_kind: AssetKind,
) -> Result<(), anyhow::Error> {
log::info!("downloading github artifact from {url}");
let mut response = http_client
.get(url, Default::default(), true)
.await
.with_context(|| format!("downloading release from {url}"))?;
let body = response.body_mut();
match digest {
Some(expected_sha_256) => {
let temp_asset_file = tempfile::NamedTempFile::new()
.with_context(|| format!("creating a temporary file for {url}"))?;
let (temp_asset_file, _temp_guard) = temp_asset_file.into_parts();
let mut writer = HashingWriter {
writer: async_fs::File::from(temp_asset_file),
hasher: Sha256::new(),
};
futures::io::copy(&mut BufReader::new(body), &mut writer)
.await
.with_context(|| {
format!("saving archive contents into the temporary file for {url}",)
})?;
let asset_sha_256 = format!("{:x}", writer.hasher.finalize());
anyhow::ensure!(
asset_sha_256 == expected_sha_256,
"{url} asset got SHA-256 mismatch. Expected: {expected_sha_256}, Got: {asset_sha_256}",
);
writer
.writer
.seek(std::io::SeekFrom::Start(0))
.await
.with_context(|| format!("seeking temporary file {destination_path:?}",))?;
stream_file_archive(&mut writer.writer, url, destination_path, asset_kind)
.await
.with_context(|| {
format!("extracting downloaded asset for {url} into {destination_path:?}",)
})?;
}
None => stream_response_archive(body, url, destination_path, asset_kind)
.await
.with_context(|| {
format!("extracting response for asset {url} into {destination_path:?}",)
})?,
}
Ok(())
}
async fn stream_response_archive(
response: impl AsyncRead + Unpin,
url: &str,
destination_path: &Path,
asset_kind: AssetKind,
) -> Result<()> {
match asset_kind {
AssetKind::TarGz => extract_tar_gz(destination_path, url, response).await?,
AssetKind::Gz => extract_gz(destination_path, url, response).await?,
AssetKind::Zip => {
util::archive::extract_zip(destination_path, response).await?;
}
};
Ok(())
}
async fn stream_file_archive(
file_archive: impl AsyncRead + AsyncSeek + Unpin,
url: &str,
destination_path: &Path,
asset_kind: AssetKind,
) -> Result<()> {
match asset_kind {
AssetKind::TarGz => extract_tar_gz(destination_path, url, file_archive).await?,
AssetKind::Gz => extract_gz(destination_path, url, file_archive).await?,
#[cfg(not(windows))]
AssetKind::Zip => {
util::archive::extract_seekable_zip(destination_path, file_archive).await?;
}
#[cfg(windows)]
AssetKind::Zip => {
util::archive::extract_zip(destination_path, file_archive).await?;
}
};
Ok(())
}
async fn extract_tar_gz(
destination_path: &Path,
url: &str,
from: impl AsyncRead + Unpin,
) -> Result<(), anyhow::Error> {
let decompressed_bytes = GzipDecoder::new(BufReader::new(from));
let archive = async_tar::Archive::new(decompressed_bytes);
archive
.unpack(&destination_path)
.await
.with_context(|| format!("extracting {url} to {destination_path:?}"))?;
Ok(())
}
async fn extract_gz(
destination_path: &Path,
url: &str,
from: impl AsyncRead + Unpin,
) -> Result<(), anyhow::Error> {
let mut decompressed_bytes = GzipDecoder::new(BufReader::new(from));
let mut file = async_fs::File::create(&destination_path)
.await
.with_context(|| {
format!("creating a file {destination_path:?} for a download from {url}")
})?;
futures::io::copy(&mut decompressed_bytes, &mut file)
.await
.with_context(|| format!("extracting {url} to {destination_path:?}"))?;
Ok(())
}
struct HashingWriter<W: AsyncWrite + Unpin> {
writer: W,
hasher: Sha256,
}
impl<W: AsyncWrite + Unpin> AsyncWrite for HashingWriter<W> {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> Poll<std::result::Result<usize, std::io::Error>> {
match Pin::new(&mut self.writer).poll_write(cx, buf) {
Poll::Ready(Ok(n)) => {
self.hasher.update(&buf[..n]);
Poll::Ready(Ok(n))
}
other => other,
}
}
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
Pin::new(&mut self.writer).poll_flush(cx)
}
fn poll_close(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<std::result::Result<(), std::io::Error>> {
Pin::new(&mut self.writer).poll_close(cx)
}
}
@@ -0,0 +1,481 @@
mod async_body;
pub mod github;
pub mod github_download;
pub use anyhow::{Result, anyhow};
pub use async_body::{AsyncBody, Inner};
use derive_more::Deref;
use http::HeaderValue;
pub use http::{self, Method, Request, Response, StatusCode, Uri, request::Builder};
use futures::{
FutureExt as _,
future::{self, BoxFuture},
};
use parking_lot::Mutex;
#[cfg(feature = "test-support")]
use std::fmt;
use std::{any::type_name, sync::Arc};
pub use url::Url;
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
pub enum RedirectPolicy {
#[default]
NoFollow,
FollowLimit(u32),
FollowAll,
}
pub struct FollowRedirects(pub bool);
pub trait HttpRequestExt {
/// Conditionally modify self with the given closure.
fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
where
Self: Sized,
{
if condition { then(self) } else { self }
}
/// Conditionally unwrap and modify self with the given closure, if the given option is Some.
fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
where
Self: Sized,
{
match option {
Some(value) => then(self, value),
None => self,
}
}
/// Whether or not to follow redirects
fn follow_redirects(self, follow: RedirectPolicy) -> Self;
}
impl HttpRequestExt for http::request::Builder {
fn follow_redirects(self, follow: RedirectPolicy) -> Self {
self.extension(follow)
}
}
pub trait HttpClient: 'static + Send + Sync {
fn type_name(&self) -> &'static str;
fn user_agent(&self) -> Option<&HeaderValue>;
fn send(
&self,
req: http::Request<AsyncBody>,
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>;
fn get(
&self,
uri: &str,
body: AsyncBody,
follow_redirects: bool,
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
let request = Builder::new()
.uri(uri)
.follow_redirects(if follow_redirects {
RedirectPolicy::FollowAll
} else {
RedirectPolicy::NoFollow
})
.body(body);
match request {
Ok(request) => self.send(request),
Err(e) => Box::pin(async move { Err(e.into()) }),
}
}
fn post_json(
&self,
uri: &str,
body: AsyncBody,
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
let request = Builder::new()
.uri(uri)
.method(Method::POST)
.header("Content-Type", "application/json")
.body(body);
match request {
Ok(request) => self.send(request),
Err(e) => Box::pin(async move { Err(e.into()) }),
}
}
fn proxy(&self) -> Option<&Url>;
#[cfg(feature = "test-support")]
fn as_fake(&self) -> &FakeHttpClient {
panic!("called as_fake on {}", type_name::<Self>())
}
fn send_multipart_form<'a>(
&'a self,
_url: &str,
_request: reqwest::multipart::Form,
) -> BoxFuture<'a, anyhow::Result<Response<AsyncBody>>> {
future::ready(Err(anyhow!("not implemented"))).boxed()
}
}
/// An [`HttpClient`] that may have a proxy.
#[derive(Deref)]
pub struct HttpClientWithProxy {
#[deref]
client: Arc<dyn HttpClient>,
proxy: Option<Url>,
}
impl HttpClientWithProxy {
/// Returns a new [`HttpClientWithProxy`] with the given proxy URL.
pub fn new(client: Arc<dyn HttpClient>, proxy_url: Option<String>) -> Self {
let proxy_url = proxy_url
.and_then(|proxy| proxy.parse().ok())
.or_else(read_proxy_from_env);
Self::new_url(client, proxy_url)
}
pub fn new_url(client: Arc<dyn HttpClient>, proxy_url: Option<Url>) -> Self {
Self {
client,
proxy: proxy_url,
}
}
}
impl HttpClient for HttpClientWithProxy {
fn send(
&self,
req: Request<AsyncBody>,
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
self.client.send(req)
}
fn user_agent(&self) -> Option<&HeaderValue> {
self.client.user_agent()
}
fn proxy(&self) -> Option<&Url> {
self.proxy.as_ref()
}
fn type_name(&self) -> &'static str {
self.client.type_name()
}
#[cfg(feature = "test-support")]
fn as_fake(&self) -> &FakeHttpClient {
self.client.as_fake()
}
fn send_multipart_form<'a>(
&'a self,
url: &str,
form: reqwest::multipart::Form,
) -> BoxFuture<'a, anyhow::Result<Response<AsyncBody>>> {
self.client.send_multipart_form(url, form)
}
}
/// An [`HttpClient`] that has a base URL.
pub struct HttpClientWithUrl {
base_url: Mutex<String>,
client: HttpClientWithProxy,
}
impl std::ops::Deref for HttpClientWithUrl {
type Target = HttpClientWithProxy;
fn deref(&self) -> &Self::Target {
&self.client
}
}
impl HttpClientWithUrl {
/// Returns a new [`HttpClientWithUrl`] with the given base URL.
pub fn new(
client: Arc<dyn HttpClient>,
base_url: impl Into<String>,
proxy_url: Option<String>,
) -> Self {
let client = HttpClientWithProxy::new(client, proxy_url);
Self {
base_url: Mutex::new(base_url.into()),
client,
}
}
pub fn new_url(
client: Arc<dyn HttpClient>,
base_url: impl Into<String>,
proxy_url: Option<Url>,
) -> Self {
let client = HttpClientWithProxy::new_url(client, 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().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() = base_url;
}
/// 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,
)?)
}
/// Builds a Zed Cloud URL using the given path.
pub fn build_zed_cloud_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://cloud.zed.dev",
"https://staging.zed.dev" => "https://cloud.zed.dev",
"http://localhost:3000" => "http://localhost:8787",
other => other,
};
Ok(Url::parse_with_params(
&format!("{}{}", base_api_url, path),
query,
)?)
}
/// Builds a Zed LLM URL using the given path.
pub fn build_zed_llm_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://cloud.zed.dev",
"https://staging.zed.dev" => "https://llm-staging.zed.dev",
"http://localhost:3000" => "http://localhost:8787",
other => other,
};
Ok(Url::parse_with_params(
&format!("{}{}", base_api_url, path),
query,
)?)
}
}
impl HttpClient for HttpClientWithUrl {
fn send(
&self,
req: Request<AsyncBody>,
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
self.client.send(req)
}
fn user_agent(&self) -> Option<&HeaderValue> {
self.client.user_agent()
}
fn proxy(&self) -> Option<&Url> {
self.client.proxy.as_ref()
}
fn type_name(&self) -> &'static str {
self.client.type_name()
}
#[cfg(feature = "test-support")]
fn as_fake(&self) -> &FakeHttpClient {
self.client.as_fake()
}
fn send_multipart_form<'a>(
&'a self,
url: &str,
request: reqwest::multipart::Form,
) -> BoxFuture<'a, anyhow::Result<Response<AsyncBody>>> {
self.client.send_multipart_form(url, request)
}
}
pub fn read_proxy_from_env() -> Option<Url> {
const ENV_VARS: &[&str] = &[
"ALL_PROXY",
"all_proxy",
"HTTPS_PROXY",
"https_proxy",
"HTTP_PROXY",
"http_proxy",
];
ENV_VARS
.iter()
.find_map(|var| std::env::var(var).ok())
.and_then(|env| env.parse().ok())
}
pub fn read_no_proxy_from_env() -> Option<String> {
const ENV_VARS: &[&str] = &["NO_PROXY", "no_proxy"];
ENV_VARS.iter().find_map(|var| std::env::var(var).ok())
}
pub struct BlockedHttpClient;
impl BlockedHttpClient {
pub fn new() -> Self {
BlockedHttpClient
}
}
impl HttpClient for BlockedHttpClient {
fn send(
&self,
_req: Request<AsyncBody>,
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
Box::pin(async {
Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"BlockedHttpClient disallowed request",
)
.into())
})
}
fn user_agent(&self) -> Option<&HeaderValue> {
None
}
fn proxy(&self) -> Option<&Url> {
None
}
fn type_name(&self) -> &'static str {
type_name::<Self>()
}
#[cfg(feature = "test-support")]
fn as_fake(&self) -> &FakeHttpClient {
panic!("called as_fake on {}", type_name::<Self>())
}
}
#[cfg(feature = "test-support")]
type FakeHttpHandler = Arc<
dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>
+ Send
+ Sync
+ 'static,
>;
#[cfg(feature = "test-support")]
pub struct FakeHttpClient {
handler: Mutex<Option<FakeHttpHandler>>,
user_agent: HeaderValue,
}
#[cfg(feature = "test-support")]
impl FakeHttpClient {
pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
where
Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + 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: Mutex::new(Some(Arc::new(move |req| Box::pin(handler(req))))),
user_agent: HeaderValue::from_static(type_name::<Self>()),
}),
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())
})
}
pub fn replace_handler<Fut, F>(&self, new_handler: F)
where
Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
F: Fn(FakeHttpHandler, Request<AsyncBody>) -> Fut + Send + Sync + 'static,
{
let mut handler = self.handler.lock();
let old_handler = handler.take().unwrap();
*handler = Some(Arc::new(move |req| {
Box::pin(new_handler(old_handler.clone(), req))
}));
}
}
#[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, anyhow::Result<Response<AsyncBody>>> {
((self.handler.lock().as_ref().unwrap())(req)) as _
}
fn user_agent(&self) -> Option<&HeaderValue> {
Some(&self.user_agent)
}
fn proxy(&self) -> Option<&Url> {
None
}
fn type_name(&self) -> &'static str {
type_name::<Self>()
}
fn as_fake(&self) -> &FakeHttpClient {
self
}
}