Skip to main content

jupiter_sdk/
client.rs

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        // 使用 serde_path_to_error 捕获精确路径
136        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}