Nice cleanup of the http_client lib
This commit is contained in:
parent
871c239ef2
commit
a9e8f2e9cf
15 changed files with 3822 additions and 405 deletions
|
@ -1,5 +0,0 @@
|
|||
[env]
|
||||
RUST_BACKTRACE = "1"
|
||||
|
||||
# [profile.release]
|
||||
# RUSTFLAGS="-Zlocation-detail=none"
|
932
Cargo.lock
generated
932
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
38
Cargo.toml
38
Cargo.toml
|
@ -1,35 +1,9 @@
|
|||
[package]
|
||||
name = "yourcloud"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["apps/yourcloud"]
|
||||
|
||||
[dependencies]
|
||||
axum = "0.7.5"
|
||||
color-eyre = "0.6.3"
|
||||
config = "0.14.0"
|
||||
discord = { git = "https://github.com/SpaceManiac/discord-rs" }
|
||||
redact = { version = "0.1.9", features = ["serde"] }
|
||||
reqwest = { version = "0.12.3", features = ["json", "socks"] }
|
||||
reqwest-middleware = "0.3.0"
|
||||
reqwest-retry = "0.5.0"
|
||||
reqwest-tracing = "0.5.0"
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
serde_json = "1.0.115"
|
||||
serde_with = "3.7.0"
|
||||
tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
urlencoding = "2.1.3"
|
||||
[workspace.dependencies]
|
||||
http_client = { path = "libs/http_client" }
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
||||
[profile.dev.package."*"]
|
||||
[profile.dev.package.backtrace]
|
||||
opt-level = 3
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
|
|
2916
apps/yourcloud/Cargo.lock
generated
Normal file
2916
apps/yourcloud/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
36
apps/yourcloud/Cargo.toml
Normal file
36
apps/yourcloud/Cargo.toml
Normal file
|
@ -0,0 +1,36 @@
|
|||
[package]
|
||||
name = "yourcloud"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
axum = "0.7.5"
|
||||
color-eyre = "0.6.3"
|
||||
config = "0.14.0"
|
||||
discord = { git = "https://github.com/SpaceManiac/discord-rs" }
|
||||
redact = { version = "0.1.9", features = ["serde"] }
|
||||
reqwest = { version = "0.12.3", features = ["json", "socks"] }
|
||||
reqwest-middleware = "0.3.0"
|
||||
reqwest-retry = "0.5.0"
|
||||
reqwest-tracing = "0.5.0"
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
serde_json = "1.0.115"
|
||||
serde_with = "3.7.0"
|
||||
tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
urlencoding = "2.1.3"
|
||||
http_client = { workspace = true }
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
|
@ -5,7 +5,6 @@ mod config;
|
|||
mod minecraft_server_status;
|
||||
mod observe;
|
||||
mod prelude;
|
||||
mod webclient;
|
||||
mod webserver;
|
||||
|
||||
use crate::prelude::*;
|
|
@ -1,8 +1,7 @@
|
|||
use crate::prelude::*;
|
||||
use crate::webclient::Client;
|
||||
use http_client::{Client, HeaderMap, HeaderValue, Url};
|
||||
use serde_with::skip_serializing_none;
|
||||
|
||||
use reqwest::header::{HeaderMap, HeaderValue};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct MinecraftServerStatus {
|
||||
|
@ -96,14 +95,19 @@ impl MinecraftServerStatus {
|
|||
let mut headers = HeaderMap::new();
|
||||
headers.insert("content-type", HeaderValue::from_str("application/json")?);
|
||||
headers.insert("accepts", HeaderValue::from_str("application/json")?);
|
||||
let base_client = Client::builder().default_headers(headers).build()?;
|
||||
let client = Client::try_new(base_client, "https://api.mcsrvstat.us/3")?;
|
||||
|
||||
let client = Client::named("http_client@api.mcsrvstat.us")
|
||||
.with_default_headers(headers)
|
||||
.with_url(Url::parse("https://api.mcsrvstat.us/3")?)
|
||||
.build()?;
|
||||
|
||||
Ok(Self { client })
|
||||
}
|
||||
|
||||
pub async fn status(&self, endpoint: &str) -> Result<ServerStatus> {
|
||||
self.client
|
||||
.get(&format!("/{}", urlencoding::encode(endpoint)))
|
||||
.await
|
||||
Ok(self
|
||||
.client
|
||||
.get_json(&format!("/{}", urlencoding::encode(endpoint)))
|
||||
.await?)
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ pub fn setup_logging() {
|
|||
|
||||
let filter = EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::TRACE.into())
|
||||
.parse_lossy("info,yourcloud=trace");
|
||||
.parse_lossy("trace,yourcloud=trace");
|
||||
|
||||
tracing_subscriber::fmt().with_env_filter(filter).init();
|
||||
}
|
|
@ -49,7 +49,7 @@ async fn minecraft_server_status() -> WebserverResult<impl IntoResponse> {
|
|||
let mut results = Vec::with_capacity(reqs.len());
|
||||
for r in reqs {
|
||||
let client = c.clone();
|
||||
debug!("Fetching server status for jql {}", r);
|
||||
debug!("Fetching minecraft server status for {}", r);
|
||||
futures.push(tokio::spawn(async move { client.status(r).await }));
|
||||
}
|
||||
|
15
libs/http_client/Cargo.toml
Normal file
15
libs/http_client/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "http_client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.11.26", features = ["json", "socks"] }
|
||||
reqwest-middleware = "0.2.5"
|
||||
reqwest-retry = "0.4.0"
|
||||
reqwest-tracing = "0.4.8"
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
serde_json = "1.0.114"
|
||||
thiserror = "1.0.63"
|
||||
tracing = "0.1.40"
|
||||
url = "2.5.2"
|
192
libs/http_client/src/lib.rs
Normal file
192
libs/http_client/src/lib.rs
Normal file
|
@ -0,0 +1,192 @@
|
|||
pub use reqwest::header::{HeaderMap, HeaderValue};
|
||||
pub use reqwest::{Body, ClientBuilder, Method, Response, Url};
|
||||
pub use reqwest_middleware::ClientBuilder as MiddlewareClientBuilder;
|
||||
pub use reqwest_middleware::Middleware;
|
||||
pub use reqwest_middleware::{ClientWithMiddleware, Extension, RequestBuilder};
|
||||
pub use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
|
||||
pub use reqwest_tracing::{OtelName, TracingMiddleware};
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::sync::Arc;
|
||||
use tracing::instrument;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
client: ClientWithMiddleware,
|
||||
base_url: Option<Url>,
|
||||
}
|
||||
|
||||
pub struct Builder {
|
||||
middleware: Option<Box<Vec<Arc<dyn Middleware>>>>,
|
||||
default_headers: Option<HeaderMap>,
|
||||
base_url: Option<Url>,
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for Builder {
|
||||
fn default() -> Self {
|
||||
let mut middleware: Box<Vec<Arc<dyn Middleware>>> = Box::new(vec![]);
|
||||
|
||||
let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3);
|
||||
let retry_middleware = RetryTransientMiddleware::new_with_policy(retry_policy);
|
||||
middleware.push(Arc::new(retry_middleware));
|
||||
|
||||
middleware.push(Arc::new(TracingMiddleware::default()));
|
||||
|
||||
Builder {
|
||||
name: None,
|
||||
base_url: None,
|
||||
default_headers: None,
|
||||
middleware: Some(middleware),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
pub fn new() -> Builder {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn named(name: &str) -> Self {
|
||||
Self::default().with_name(name)
|
||||
}
|
||||
|
||||
pub fn with_name(mut self, name: &str) -> Self {
|
||||
self.name = Some(name.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_url(mut self, base_url: Url) -> Self {
|
||||
self.base_url = Some(base_url);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn try_with_url(self, base_url: &str) -> Result<Self> {
|
||||
Ok(self.with_url(Url::parse(base_url)?))
|
||||
}
|
||||
|
||||
pub fn with_default_headers(mut self, default_headers: HeaderMap) -> Self {
|
||||
self.default_headers = Some(default_headers);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(mut self) -> Result<Client> {
|
||||
let mut builder = reqwest::Client::builder();
|
||||
|
||||
if let Some(headers) = self.default_headers {
|
||||
builder = builder.default_headers(headers);
|
||||
}
|
||||
|
||||
let mut middleware_builder = reqwest_middleware::ClientBuilder::new(builder.build()?);
|
||||
|
||||
if let Some(middleware) = self.middleware {
|
||||
for middleware in middleware.into_iter() {
|
||||
middleware_builder = middleware_builder.with_arc(middleware);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(otel_name) = self.name {
|
||||
middleware_builder =
|
||||
middleware_builder.with_init(Extension(OtelName(otel_name.into())));
|
||||
}
|
||||
|
||||
let client = middleware_builder.build();
|
||||
|
||||
if let Some(ref mut base_url) = self.base_url {
|
||||
base_url.set_path(&format!("{}{}", base_url.path().trim_end_matches('/'), "/"));
|
||||
}
|
||||
|
||||
Ok(Client {
|
||||
client,
|
||||
base_url: self.base_url,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("reqwest error: {0}")]
|
||||
Reqwest(#[from] reqwest::Error),
|
||||
#[error("reqwest_middleware error: {0}")]
|
||||
ReqwestMiddleware(#[from] reqwest_middleware::Error),
|
||||
#[error("reqwest_middleware error: {0}")]
|
||||
UrlParse(#[from] url::ParseError),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// A good-enough-out-of-the-box, batteries-included async HTTP client with
|
||||
/// enough escape hatches in case you need to get wild. Includes retries and
|
||||
/// tracing instrumentation.
|
||||
///
|
||||
/// The intent of HttpClient is to be able to painlessly build out wrappers
|
||||
/// around HTTP API endpoints.
|
||||
impl Client {
|
||||
pub fn builder() -> Builder {
|
||||
Builder::default()
|
||||
}
|
||||
|
||||
pub fn named(name: &str) -> Builder {
|
||||
Builder::named(name)
|
||||
}
|
||||
|
||||
/// Takes a path segment and properly joins it relative to `base_url`
|
||||
/// If the client has None base_url, then the path will be parsed as a Url
|
||||
/// and the result returned.
|
||||
#[instrument]
|
||||
pub fn build_url(&self, path: &str) -> Result<Url> {
|
||||
if let Some(url) = &self.base_url {
|
||||
url.join(path.trim_start_matches('/')).map_err(|e| e.into())
|
||||
} else {
|
||||
Ok(Url::parse(path)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub fn build_request(&self, method: Method, path: &str) -> Result<RequestBuilder> {
|
||||
Ok(self.client.request(method, self.build_url(path)?))
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn send_with_body<S: Into<Body> + std::fmt::Debug>(
|
||||
&self,
|
||||
method: Method,
|
||||
path: &str,
|
||||
body: S,
|
||||
) -> Result<Response> {
|
||||
Ok(self.build_request(method, path)?.body(body).send().await?)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn post<S: Into<Body> + std::fmt::Debug>(
|
||||
&self,
|
||||
path: &str,
|
||||
body: S,
|
||||
) -> Result<Response> {
|
||||
Ok(self.send_with_body(Method::POST, path, body).await?)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn put<S: Into<Body> + std::fmt::Debug>(
|
||||
&self,
|
||||
path: &str,
|
||||
body: S,
|
||||
) -> Result<Response> {
|
||||
Ok(self.send_with_body(Method::PUT, path, body).await?)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn get(&self, rel_url_path: &str) -> Result<Response> {
|
||||
Ok(self
|
||||
.build_request(Method::GET, rel_url_path)?
|
||||
.send()
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn get_json<T: DeserializeOwned>(&self, rel_url_path: &str) -> Result<T> {
|
||||
Ok(self.get(rel_url_path).await?.json().await?)
|
||||
}
|
||||
|
||||
// this is a lib, so add new stuff sparingly!
|
||||
// prefer building atop this instead of adding stuff here
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
use reqwest::{Client as RClient, Method, Url};
|
||||
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, RequestBuilder};
|
||||
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
|
||||
use reqwest_tracing::TracingMiddleware;
|
||||
use serde::de;
|
||||
|
||||
pub struct Client {
|
||||
client: ClientWithMiddleware,
|
||||
base_url: Url,
|
||||
}
|
||||
|
||||
pub trait ResourceRequest {
|
||||
async fn res<T: de::DeserializeOwned>(self) -> Result<T>;
|
||||
}
|
||||
|
||||
impl ResourceRequest for RequestBuilder {
|
||||
async fn res<T: de::DeserializeOwned>(self) -> Result<T> {
|
||||
let body = self.send().await?.text().await?;
|
||||
debug!(body);
|
||||
serde_json::from_str(&body).map_err(|e| e.into())
|
||||
|
||||
// self.send().await?.json().await.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn builder() -> reqwest::ClientBuilder {
|
||||
RClient::builder()
|
||||
}
|
||||
|
||||
pub fn try_new(client: RClient, base_url: &str) -> Result<Self> {
|
||||
let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3);
|
||||
|
||||
let client = ClientBuilder::new(client)
|
||||
.with(TracingMiddleware::default())
|
||||
.with(RetryTransientMiddleware::new_with_policy(retry_policy))
|
||||
.build();
|
||||
|
||||
// force trailing slash
|
||||
let mut base_url = Url::parse(base_url)?;
|
||||
base_url.set_path(&format!("{}{}", base_url.path().trim_end_matches('/'), "/"));
|
||||
|
||||
Ok(Self { client, base_url })
|
||||
}
|
||||
|
||||
/// Takes a path segment and properly joins it relative to `base_url`
|
||||
pub fn url(&self, path: &str) -> Result<Url> {
|
||||
self.base_url
|
||||
.join(path.trim_start_matches('/'))
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
pub fn build(&self, method: Method, rel_url_path: &str) -> Result<RequestBuilder> {
|
||||
Ok(self.client.request(method, self.url(rel_url_path)?))
|
||||
}
|
||||
|
||||
pub async fn request<T: de::DeserializeOwned>(
|
||||
&self,
|
||||
method: Method,
|
||||
rel_url_path: &str,
|
||||
) -> Result<T> {
|
||||
self.build(method, rel_url_path)?.res().await
|
||||
}
|
||||
|
||||
pub async fn get<T: de::DeserializeOwned>(&self, rel_url_path: &str) -> Result<T> {
|
||||
self.request::<T>(Method::GET, rel_url_path).await
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue