jquants_api_client/
api.rs

1//! This module contains all the API models.
2//! The models are used to serialize and deserialize the data that is sent to and from the API.
3
4pub mod breakdown_trading_data;
5pub mod cash_dividend_data;
6pub mod daily_stock_prices;
7pub mod earnings_calendar;
8pub mod financial_statement_details;
9pub mod financial_statements;
10pub mod futures_prices;
11pub mod index_option_prices;
12pub mod indicies;
13pub mod listed_issue_info;
14pub mod morning_session_stock_prices;
15pub mod options_prices;
16pub mod shared;
17pub mod short_sale_by_sector;
18pub mod topic_prices;
19pub mod trading_by_type_of_investors;
20pub mod trading_calendar;
21pub mod weekly_margin_trading_outstandings;
22
23use shared::{
24    auth::{get_id_token_from_api, get_refresh_token_from_api},
25    responses::error_response::JQuantsErrorResponse,
26};
27use std::{fmt, sync::Arc};
28use tokio::sync::RwLock;
29
30use crate::error::JQuantsError;
31use chrono::{DateTime, Local};
32use reqwest::{Client, RequestBuilder};
33use serde::{de::DeserializeOwned, Serialize};
34
35const BASE_URL: &str = "https://api.jquants.com/v1";
36/// Concatenate the base URL and the path.
37///
38/// `path` does not need to include a leading `/`.
39///
40/// # Example
41///
42/// ```ignore
43/// let path = "token/auth_refresh";
44/// let url = build_url(path);
45/// assert_eq!(url, "https://api.jquants.com/v1/token/auth_refresh");
46/// ```
47fn build_url(path: &str) -> String {
48    format!("{}/{}", BASE_URL, path)
49}
50
51/// J-Quants API client trait
52pub trait JQuantsPlanClient: Clone {
53    /// Create a new client from an API client.
54    fn new(api_client: JQuantsApiClient) -> Self;
55
56    /// Create a new client from a refresh token.
57    fn new_from_refresh_token(refresh_token: String) -> Self {
58        let api_client = JQuantsApiClient::new_from_refresh_token(refresh_token);
59        Self::new(api_client)
60    }
61
62    /// Create a new client from an account.
63    fn new_from_account(
64        mailaddress: &str,
65        password: &str,
66    ) -> impl std::future::Future<Output = Result<Self, JQuantsError>> + Send {
67        async {
68            let api_client = JQuantsApiClient::new_from_account(mailaddress, password).await?;
69            Ok(Self::new(api_client))
70        }
71    }
72
73    /// Get the API client.
74    fn get_api_client(&self) -> &JQuantsApiClient;
75
76    /// Get a current refresh token.
77    fn get_current_refresh_token(&self) -> impl std::future::Future<Output = String> + Send {
78        let api_client = self.get_api_client().clone();
79        async move {
80            api_client
81                .inner
82                .token_set
83                .read()
84                .await
85                .refresh_token
86                .clone()
87        }
88    }
89
90    /// Get a new refresh token from an account.
91    /// But don't update the ID token in the client.
92    ///
93    /// Use `refresh_refresh_token` if you want to update the refresh token in the client.
94    fn get_refresh_token_from_api(
95        &self,
96        mail_address: &str,
97        password: &str,
98    ) -> impl std::future::Future<Output = Result<String, JQuantsError>> + Send {
99        let api_client = self.get_api_client().clone();
100        async move { get_refresh_token_from_api(&api_client.inner.client, mail_address, password).await }
101    }
102
103    /// Get a new ID token from a refresh token.
104    /// But don't update the ID token in the client.
105    ///
106    /// Use `refresh_id_token` if you want to update the ID token in the client.
107    fn get_id_token_from_api(
108        &self,
109        refresh_token: &str,
110    ) -> impl std::future::Future<Output = Result<String, JQuantsError>> + Send {
111        let api_client = self.get_api_client().clone();
112        async move { get_id_token_from_api(&api_client.inner.client, refresh_token).await }
113    }
114
115    /// Renew the refresh token in the client.
116    fn reset_refresh_token(
117        &self,
118        mail_address: &str,
119        password: &str,
120    ) -> impl std::future::Future<Output = Result<(), JQuantsError>> + Send {
121        let api_client = self.get_api_client().clone();
122        async move {
123            api_client
124                .inner
125                .reset_refresh_token(mail_address, password)
126                .await
127        }
128    }
129
130    /// Renew the ID token in the client.
131    fn reset_id_token(&self) -> impl std::future::Future<Output = Result<(), JQuantsError>> + Send {
132        let api_client = self.get_api_client().clone();
133        async move { api_client.inner.reset_id_token().await }
134    }
135
136    /// Reauthenticate with a new refresh token and a new id token.
137    fn reauthenticate(
138        &self,
139        mail_address: &str,
140        password: &str,
141    ) -> impl std::future::Future<Output = Result<(), JQuantsError>> + Send {
142        let api_client = self.get_api_client().clone();
143        async move { api_client.inner.reset_tokens(mail_address, password).await }
144    }
145}
146
147/// J-Quants API client
148#[derive(Clone)]
149pub struct JQuantsApiClient {
150    inner: Arc<JQuantsApiClientRef>,
151}
152impl JQuantsApiClient {
153    /// Create a new client from a refresh token.
154    fn new_from_refresh_token(refresh_token: String) -> Self {
155        Self {
156            inner: Arc::new(JQuantsApiClientRef::new_from_refresh_token(refresh_token)),
157        }
158    }
159
160    /// Create a new client from an account.
161    async fn new_from_account(mailaddress: &str, password: &str) -> Result<Self, JQuantsError> {
162        let client_ref = JQuantsApiClientRef::new_from_account(mailaddress, password).await?;
163        Ok(Self {
164            inner: Arc::new(client_ref),
165        })
166    }
167}
168
169/// J-Quants API client
170///
171/// See: [API Reference](https://jpx.gitbook.io/j-quants-en)
172pub(crate) struct JQuantsApiClientRef {
173    /// HTTP client
174    client: Client,
175    /// Refresh token and ID token
176    token_set: Arc<RwLock<TokenSet>>,
177}
178
179impl JQuantsApiClientRef {
180    /// Create a new client from a refresh token.
181    fn new_from_refresh_token(refresh_token: String) -> Self {
182        Self {
183            client: Client::new(),
184            token_set: Arc::new(RwLock::new(TokenSet {
185                refresh_token,
186                id_token: None,
187            })),
188        }
189    }
190
191    /// Create a new client from an account.
192    async fn new_from_account(mailaddress: &str, password: &str) -> Result<Self, JQuantsError> {
193        let client = Client::new();
194        let refresh_token = get_refresh_token_from_api(&client, mailaddress, password).await?;
195        let new_id_token = get_id_token_from_api(&client, &refresh_token).await?;
196
197        let id_token_wrapper = IdTokenWrapper::new(new_id_token);
198
199        Ok(Self {
200            client,
201            token_set: Arc::new(RwLock::new(TokenSet {
202                refresh_token,
203                id_token: Some(id_token_wrapper),
204            })),
205        })
206    }
207
208    /// Get a new refresh token from an account.
209    async fn reset_refresh_token(
210        &self,
211        mail_address: &str,
212        password: &str,
213    ) -> Result<(), JQuantsError> {
214        tracing::debug!("Starting reset a refresh token process.");
215
216        match get_refresh_token_from_api(&self.client, mail_address, password).await {
217            Ok(new_refresh_token) => {
218                let mut token_set_write = self.token_set.write().await;
219                token_set_write.refresh_token = new_refresh_token;
220                tracing::debug!("Refresh token refreshed successfully.");
221                Ok(())
222            }
223            Err(e) => {
224                tracing::error!("Failed to refresh a refresh token: {:?}", e);
225                Err(e)
226            }
227        }
228    }
229
230    /// Get a new ID token from a refresh token.
231    async fn reset_id_token(&self) -> Result<(), JQuantsError> {
232        tracing::debug!("Starting reset a refresh id process.");
233
234        let refresh_token = { self.token_set.read().await.refresh_token.clone() };
235        match get_id_token_from_api(&self.client, &refresh_token).await {
236            Ok(new_id_token) => {
237                let mut token_set_write = self.token_set.write().await;
238                token_set_write.id_token = Some(IdTokenWrapper::new(new_id_token));
239                tracing::debug!("ID token refreshed successfully.");
240                Ok(())
241            }
242            Err(e) => {
243                tracing::error!("Failed to refresh ID token: {:?}", e);
244                Err(e)
245            }
246        }
247    }
248
249    /// Reset the refresh token if needed.
250    async fn reset_id_token_if_needed(&self) -> Result<(), JQuantsError> {
251        let needs_refresh = {
252            let token_set = self.token_set.read().await;
253            match &token_set.id_token {
254                Some(token) => !token.is_valid(),
255                None => true,
256            }
257        };
258
259        if needs_refresh {
260            tracing::debug!("ID token is invalid or expired. Attempting to refresh.");
261            self.reset_id_token().await
262        } else {
263            tracing::debug!("ID token is still valid.");
264            Ok(())
265        }
266    }
267
268    /// Reauthenticate with a new refresh token and a new id token.
269    async fn reset_tokens(&self, mail_address: &str, password: &str) -> Result<(), JQuantsError> {
270        tracing::debug!("Starting re-authentication process.");
271
272        // 再認証して新しいrefresh_tokenとid_tokenを取得
273        let new_refresh_token = get_refresh_token_from_api(&self.client, mail_address, password)
274            .await
275            .map_err(|e| {
276                tracing::error!("Failed to obtain new refresh token: {:?}", e);
277                e
278            })?;
279        tracing::debug!("Successfully obtained new refresh token.");
280
281        let new_id_token = get_id_token_from_api(&self.client, &new_refresh_token)
282            .await
283            .map_err(|e| {
284                tracing::error!("Failed to obtain new ID token: {:?}", e);
285                e
286            })?;
287        tracing::debug!("Successfully obtained new ID token.");
288
289        let expires_at = Local::now() + chrono::Duration::hours(24);
290        let new_id_token_wrapper = Some(IdTokenWrapper {
291            id_token: new_id_token,
292            expires_at,
293        });
294        {
295            let mut token_set_write = self.token_set.write().await;
296            token_set_write.refresh_token = new_refresh_token;
297            token_set_write.id_token = new_id_token_wrapper;
298        }
299
300        tracing::debug!("Re-authentication process process completed successfully.");
301        Ok(())
302    }
303
304    /// Send a GET request to the API.
305    /// The request is authenticated with the ID token.
306    /// If the ID token is expired, it will be refreshed.
307    /// If the refresh token is expired, it will return an error.
308    async fn get<T: DeserializeOwned + fmt::Debug>(
309        &self,
310        path: &str,
311        params: impl Serialize,
312    ) -> Result<T, JQuantsError> {
313        let url = format!("{BASE_URL}/{}", path);
314        let request = self.client.get(&url).query(&params);
315
316        self.common_send_and_refresh_token_if_needed::<T>(request)
317            .await
318    }
319
320    /// Sends a common request and authentication if needed.
321    async fn common_send_and_refresh_token_if_needed<T: DeserializeOwned + fmt::Debug>(
322        &self,
323        request: RequestBuilder,
324    ) -> Result<T, JQuantsError> {
325        self.reset_id_token_if_needed().await?;
326
327        self.common_send(request).await
328    }
329
330    /// Send a request and parse the response.
331    async fn common_send<T: DeserializeOwned + fmt::Debug>(
332        &self,
333        request: RequestBuilder,
334    ) -> Result<T, JQuantsError> {
335        let id_token = {
336            self.token_set
337                .read()
338                .await
339                .id_token
340                .as_ref()
341                .ok_or_else(|| {
342                    tracing::error!("ID token not found.");
343                    JQuantsError::BugError("ID token not found.".to_string())
344                })?
345                .id_token
346                .clone()
347        };
348        let request = request.header("Authorization", &format!("Bearer {id_token}"));
349
350        if let Some(url) = request
351            .try_clone()
352            .and_then(|req| req.build().ok().map(|r| r.url().clone()))
353        {
354            tracing::debug!("Sending API request to URL: {url}");
355        } else {
356            tracing::debug!("Sending API request.");
357        }
358
359        let response = request.send().await?;
360        let status = response.status();
361        let text = response.text().await.unwrap_or_default();
362        tracing::debug!("Received response with status: {}", status);
363
364        if status.is_success() {
365            match serde_json::from_str::<T>(&text) {
366                Ok(data) => {
367                    tracing::debug!("Successfully parsed response.");
368                    Ok(data)
369                }
370                Err(_) => {
371                    tracing::error!("Failed to parse response");
372                    Err(JQuantsError::InvalidResponseFormat {
373                        status_code: status.as_u16(),
374                        body: text,
375                    })
376                }
377            }
378        } else {
379            match serde_json::from_str::<JQuantsErrorResponse>(&text) {
380                Ok(error_response) => match status {
381                    reqwest::StatusCode::UNAUTHORIZED => {
382                        tracing::warn!(
383                            "Received UNAUTHORIZED error. Status code: {}",
384                            status.as_u16()
385                        );
386                        Err(JQuantsError::IdTokenInvalidOrExpired {
387                            body: error_response,
388                            status_code: status.as_u16(),
389                        })
390                    }
391                    _ => {
392                        tracing::error!("API error occurred. Status code: {}", status.as_u16());
393                        Err(JQuantsError::ApiError {
394                            body: error_response,
395                            status_code: status.as_u16(),
396                        })
397                    }
398                },
399                Err(_) => {
400                    tracing::error!("Invalid response format. Status code: {}", status.as_u16());
401                    Err(JQuantsError::InvalidResponseFormat {
402                        status_code: status.as_u16(),
403                        body: text,
404                    })
405                }
406            }
407        }
408    }
409}
410
411/// Token set
412///
413/// The refresh token is valid for one week and the ID token is valid for 24 hours.
414pub(crate) struct TokenSet {
415    /// Refresh token
416    /// Use this token to refresh the ID token.
417    refresh_token: String,
418    /// ID token
419    id_token: Option<IdTokenWrapper>,
420}
421
422/// ID Token wrapper
423///
424/// The ID token is valid for 24 hours.
425pub(crate) struct IdTokenWrapper {
426    /// ID Token
427    id_token: String,
428    /// ID Token expiration time
429    expires_at: DateTime<Local>,
430}
431impl IdTokenWrapper {
432    /// Create a new ID token wrapper.
433    fn new(id_token: String) -> Self {
434        let expires_at = Local::now() + chrono::Duration::hours(24);
435        IdTokenWrapper {
436            id_token,
437            expires_at,
438        }
439    }
440
441    /// Check if the ID token is valid.
442    /// The ID token is valid for 24 hours.
443    ///
444    /// [Docs](https://jpx.gitbook.io/j-quants-en/api-reference/idtoken#attention)
445    fn is_valid(&self) -> bool {
446        Local::now() < self.expires_at
447    }
448}
449
450/// Mask the ID token for security reasons.
451/// If you want to display the ID token, do so at your own risk.
452impl fmt::Debug for IdTokenWrapper {
453    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
454        let len = self.id_token.len();
455        let masking_id_token = "*".repeat(len);
456
457        f.debug_struct("IdTokenWrapper")
458            .field("id_token", &masking_id_token)
459            .field("expires_at", &self.expires_at)
460            .finish()
461    }
462}