1use std::time::Duration;
2use serde::Deserialize;
3use serde::{de::DeserializeOwned, Serialize};
4use crate::JupiterError;
5use reqwest::{Client, Url};
6use reqwest::header::{HeaderMap, CONTENT_TYPE};
7
8
9
10
11const BODY_SNIPPET_LIMIT: usize = 4096;
12
13fn body_excerpt(bytes: &[u8]) -> String {
14 let s = String::from_utf8_lossy(bytes);
15 if s.len() > BODY_SNIPPET_LIMIT {
16 format!("{}…", &s[..BODY_SNIPPET_LIMIT])
17 } else {
18 s.to_string()
19 }
20}
21
22
23
24
25#[derive(Clone, Debug)]
26pub struct JupiterConfig {
27 pub base_url: Url,
28 pub timeout: Duration,
29 pub user_agent: Option<String>,
30}
31
32impl Default for JupiterConfig {
33 fn default() -> Self {
34 Self {
35 base_url: Url::parse("https://lite-api.jup.ag").unwrap(),
36 timeout: Duration::from_secs(30),
37 user_agent: Some(format!("jupiter-rs/{}", env!("CARGO_PKG_VERSION"))),
38 }
39 }
40}
41
42impl JupiterConfig {
43 pub fn builder() -> JupiterConfigBuilder {
44 JupiterConfigBuilder::default()
45 }
46}
47
48#[derive(Default)]
49pub struct JupiterConfigBuilder {
50 base_url: Option<Url>,
51 timeout: Option<Duration>,
52 user_agent: Option<String>,
53}
54
55impl JupiterConfigBuilder {
56 pub fn base_url(mut self, url: &str) -> Self {
57 self.base_url = Some(Url::parse(url).expect("valid base url"));
58 self
59 }
60
61 pub fn timeout(mut self, timeout: Duration) -> Self {
62 self.timeout = Some(timeout);
63 self
64 }
65
66 pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
67 self.user_agent = Some(ua.into());
68 self
69 }
70
71 pub fn build(self) -> JupiterConfig {
72 JupiterConfig {
73 base_url: self.base_url.unwrap_or_else(|| Url::parse("https://lite-api.jup.ag").unwrap()),
74 timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
75 user_agent: self.user_agent.or_else(|| Some(format!("jupiter-rs/{}", env!("CARGO_PKG_VERSION")))),
76 }
77 }
78}
79
80#[derive(Clone)]
81pub struct JupiterClient {
82 config: JupiterConfig,
83 client: Client,
84}
85
86impl JupiterClient {
87 pub fn new(config: JupiterConfig) -> Result<Self, JupiterError> {
88 let mut headers = HeaderMap::new();
89 headers.insert("Content-Type", "application/json".parse().unwrap());
90
91 let mut builder = Client::builder()
92 .default_headers(headers)
93 .timeout(config.timeout);
94
95 if let Some(ua) = &config.user_agent {
96 builder = builder.user_agent(ua.clone());
97 }
98
99 let client = builder
100 .build()
101 .map_err(|e| JupiterError::Network(format!("failed to build http client: {e}")))?;
102
103 Ok(Self {
104 config,
105 client,
106 })
107 }
108
109 #[inline]
110 fn build_url(&self, path: &str) -> Result<Url, JupiterError> {
111 self.config
112 .base_url
113 .join(path)
114 .map_err(|e| JupiterError::Internal(format!("join url error: {e}")))
115 }
116
117 async fn parse_json<T: DeserializeOwned>(resp: reqwest::Response) -> Result<T, JupiterError> {
118 let status = resp.status();
119 let content_type = resp
120 .headers()
121 .get(CONTENT_TYPE)
122 .and_then(|v| v.to_str().ok())
123 .map(|s| s.to_string());
124
125 let bytes = resp.bytes().await.map_err(JupiterError::from)?;
126
127 if !status.is_success() {
128 return Err(JupiterError::Http {
129 status: status.as_u16(),
130 body: body_excerpt(&bytes),
131 content_type,
132 });
133 }
134
135 let mut de = serde_json::Deserializer::from_slice(&bytes);
137 match serde_path_to_error::deserialize::<_, T>(&mut de) {
138 Ok(v) => Ok(v),
139 Err(err) => {
140 let path = err.path().to_string();
141 let message = err.inner().to_string();
142 Err(JupiterError::Parse {
143 message,
144 path,
145 body: body_excerpt(&bytes),
146 })
147 }
148 }
149 }
150
151 pub(super) async fn get_json<T: DeserializeOwned>(&self, path: &str) -> Result<T, JupiterError> {
152 let url = self.build_url(path)?;
153 let resp = self.client.get(url).send().await?;
154 Self::parse_json(resp).await
155 }
156
157 pub(super) async fn get_json_with_query<T, Q>(&self, path: &str, query: &Q) -> Result<T, JupiterError>
158 where
159 T: DeserializeOwned,
160 Q: Serialize,
161 {
162 let url = self.build_url(path)?;
163 let resp = self.client.get(url).query(query).send().await?;
164 Self::parse_json(resp).await
165 }
166
167 #[allow(dead_code)]
168 pub async fn post<T: for<'a> Deserialize<'a>, B: Serialize>(
169 &self,
170 path: &str,
171 body: &B,
172 ) -> Result<T, JupiterError> {
173 let url = self.build_url(path)?;
174
175 let resp = self.client.post(url)
176 .json(body)
177 .send()
178 .await
179 .map_err(JupiterError::from)?;
180
181 Self::parse_json(resp).await
182 }
183}