use std::time::Duration;
use serde::Deserialize;
use serde::{de::DeserializeOwned, Serialize};
use crate::JupiterError;
use reqwest::{Client, Url};
use reqwest::header::{HeaderMap, CONTENT_TYPE};
const BODY_SNIPPET_LIMIT: usize = 4096;
fn body_excerpt(bytes: &[u8]) -> String {
let s = String::from_utf8_lossy(bytes);
if s.len() > BODY_SNIPPET_LIMIT {
format!("{}…", &s[..BODY_SNIPPET_LIMIT])
} else {
s.to_string()
}
}
#[derive(Clone, Debug)]
pub struct JupiterConfig {
pub base_url: Url,
pub timeout: Duration,
pub user_agent: Option<String>,
}
impl Default for JupiterConfig {
fn default() -> Self {
Self {
base_url: Url::parse("https://lite-api.jup.ag").unwrap(),
timeout: Duration::from_secs(30),
user_agent: Some(format!("jupiter-rs/{}", env!("CARGO_PKG_VERSION"))),
}
}
}
impl JupiterConfig {
pub fn builder() -> JupiterConfigBuilder {
JupiterConfigBuilder::default()
}
}
#[derive(Default)]
pub struct JupiterConfigBuilder {
base_url: Option<Url>,
timeout: Option<Duration>,
user_agent: Option<String>,
}
impl JupiterConfigBuilder {
pub fn base_url(mut self, url: &str) -> Self {
self.base_url = Some(Url::parse(url).expect("valid base url"));
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
self.user_agent = Some(ua.into());
self
}
pub fn build(self) -> JupiterConfig {
JupiterConfig {
base_url: self.base_url.unwrap_or_else(|| Url::parse("https://lite-api.jup.ag").unwrap()),
timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
user_agent: self.user_agent.or_else(|| Some(format!("jupiter-rs/{}", env!("CARGO_PKG_VERSION")))),
}
}
}
#[derive(Clone)]
pub struct JupiterClient {
config: JupiterConfig,
client: Client,
}
impl JupiterClient {
pub fn new(config: JupiterConfig) -> Result<Self, JupiterError> {
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json".parse().unwrap());
let mut builder = Client::builder()
.default_headers(headers)
.timeout(config.timeout);
if let Some(ua) = &config.user_agent {
builder = builder.user_agent(ua.clone());
}
let client = builder
.build()
.map_err(|e| JupiterError::Network(format!("failed to build http client: {e}")))?;
Ok(Self {
config,
client,
})
}
#[inline]
fn build_url(&self, path: &str) -> Result<Url, JupiterError> {
self.config
.base_url
.join(path)
.map_err(|e| JupiterError::Internal(format!("join url error: {e}")))
}
async fn parse_json<T: DeserializeOwned>(resp: reqwest::Response) -> Result<T, JupiterError> {
let status = resp.status();
let content_type = resp
.headers()
.get(CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
let bytes = resp.bytes().await.map_err(JupiterError::from)?;
if !status.is_success() {
return Err(JupiterError::Http {
status: status.as_u16(),
body: body_excerpt(&bytes),
content_type,
});
}
let mut de = serde_json::Deserializer::from_slice(&bytes);
match serde_path_to_error::deserialize::<_, T>(&mut de) {
Ok(v) => Ok(v),
Err(err) => {
let path = err.path().to_string();
let message = err.inner().to_string();
Err(JupiterError::Parse {
message,
path,
body: body_excerpt(&bytes),
})
}
}
}
pub(super) async fn get_json<T: DeserializeOwned>(&self, path: &str) -> Result<T, JupiterError> {
let url = self.build_url(path)?;
let resp = self.client.get(url).send().await?;
Self::parse_json(resp).await
}
pub(super) async fn get_json_with_query<T, Q>(&self, path: &str, query: &Q) -> Result<T, JupiterError>
where
T: DeserializeOwned,
Q: Serialize,
{
let url = self.build_url(path)?;
let resp = self.client.get(url).query(query).send().await?;
Self::parse_json(resp).await
}
#[allow(dead_code)]
pub async fn post<T: for<'a> Deserialize<'a>, B: Serialize>(
&self,
path: &str,
body: &B,
) -> Result<T, JupiterError> {
let url = self.build_url(path)?;
let resp = self.client.post(url)
.json(body)
.send()
.await
.map_err(JupiterError::from)?;
Self::parse_json(resp).await
}
}