From a29bf38dcf9fc906aec9b69fbf2d1fa508168f8d Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Sat, 4 Oct 2025 17:19:46 +0900 Subject: [PATCH 01/18] new bins --- libft-api/bin/campus_users.rs | 5 +- libft-api/bin/get_user_ext.rs | 93 +++++++++++++++++++---------------- 2 files changed, 51 insertions(+), 47 deletions(-) diff --git a/libft-api/bin/campus_users.rs b/libft-api/bin/campus_users.rs index a99281d..b7a9ccb 100644 --- a/libft-api/bin/campus_users.rs +++ b/libft-api/bin/campus_users.rs @@ -81,10 +81,7 @@ async fn main() -> Result<(), Box> { info!("{}", result.len()); } - let file_path = format!( - "/Users/hdoo/works/gsia/codes/libft-api/libft-api/bin/piscine/third_cohort/first_round/progress_{}.csv", - Utc::now().format("%Y-%m-%d_%H-%M-%S") - ); + let file_path = format!("./progress_{}.csv", Utc::now().format("%Y-%m-%d_%H-%M-%S")); let mut file = std::fs::File::create(&file_path).expect("Failed to create output file"); diff --git a/libft-api/bin/get_user_ext.rs b/libft-api/bin/get_user_ext.rs index abd7c04..336e2a6 100644 --- a/libft-api/bin/get_user_ext.rs +++ b/libft-api/bin/get_user_ext.rs @@ -1,35 +1,51 @@ -use std::{collections::HashMap, io::Write, ops::ControlFlow, sync::Arc, time::Duration}; +use std::{ops::ControlFlow, sync::Arc, time::Duration}; -use chrono::Utc; -use libft_api::{campus_id::*, prelude::*, FT_PISCINE_CURSUS_ID}; -use rvstruct::ValueStruct; +use libft_api::prelude::*; use tokio::{sync::Semaphore, task::JoinSet, time::sleep}; -use tracing::info; #[tokio::main] async fn main() -> Result<(), Box> { tracing_subscriber::fmt::init(); - let thread_num = 8; + let thread_num = 4; let permit = Arc::new(Semaphore::new(thread_num)); let ids = [ - 212531, 212530, 212529, 212528, 212527, 212526, 212525, 212524, 212523, 212522, 212521, - 212520, 212519, 212518, 212517, 212516, 212515, 212514, 212513, 212512, 212511, 212510, - 212509, 212508, 212507, 212506, 212505, 212504, 212503, 212502, 212501, 212500, 212499, - 212498, 212497, 212496, 212495, 212494, 212493, 212492, 212491, 212490, 212489, 212488, - 212487, 212486, 212485, 212484, 212483, 212482, 212481, 212480, 212479, 212478, 212477, - 212476, 212475, 212474, 212473, 212472, 212471, 212470, 212469, 212468, 212467, 212466, - 212465, 212464, 212463, 212462, 212461, 212460, 212459, 212458, 212457, 212456, 212455, - 212454, 212453, 212452, 212638, 212637, 212629, 212628, 212627, 212626, 212625, 212624, - 212623, 212622, 212621, 212620, 212619, 212618, 212617, 212616, 212615, 212614, 212613, - 212612, 212611, 212610, 212609, 212608, 212607, 212606, 212605, 212604, 212603, 212602, - 212601, 212600, 212599, 212598, 212597, 212596, 212595, 212594, 212593, 212592, 212591, - 212590, 212589, 212588, 212587, 212586, 212585, 212584, 212583, 212582, 212581, 212580, - 212579, 212578, 212577, 212576, 212575, 212574, 212573, 212572, 212571, 212570, 212569, - 212568, 212567, 212566, 212565, 212564, 212563, 212562, 212561, 212560, 212559, 212558, - 212557, 212556, 212555, 212554, 212553, 212552, 212551, 212550, 212549, 212548, 212547, - 212546, 212545, 212544, 212543, 212542, 212541, 212540, 212539, 212538, 212537, 212536, - 212535, 212534, 212533, 212532, + 172410, 197482, 190887, 172305, 172353, 197422, 197429, 190783, 197456, 190848, + 190815, + // 172394, 174189, 190846, 174084, 190820, 172357, 190800, 197497, 172418, 172352, 172349, + // 197528, 190909, 174169, 197496, 174101, 197397, 174128, 174104, 174127, 174112, 197454, + // 174184, 197455, 197495, 197484, 172327, 197507, 190797, 197498, 197444, 174097, 190898, + // 172325, 174113, 172307, 174153, 172346, 172356, 190862, 197402, 174156, 190839, 197518, + // 197483, 174185, 174152, 174145, 197459, 197504, 174131, 190847, 197523, 197521, 197511, + // 197406, 197403, 172364, 197486, 172362, 190795, 190802, 197525, 174188, 197457, 190806, + // 174089, 174135, 174129, 197400, 190817, 174081, 174147, 197489, 172308, 197463, 190913, + // 197437, 197605, 172400, 197516, 190885, 197449, 174161, 174186, 174110, 197439, 190838, + // 172329, 190870, 172370, 174085, 174111, 190849, 172416, 190876, 197606, 197519, 174138, + // 174149, 172413, 190845, 197527, 190895, 174168, 174137, 172414, 190832, 197537, 172375, + // 197441, 174151, 190808, 197472, 172390, 197520, 190843, 172348, 172392, 190896, 172389, + // 197448, 197417, 174139, 190907, 172335, 174095, 197494, 190910, 190816, 197445, 197541, + // 174130, 174150, 190823, 197467, 190821, 190784, 190926, 174142, 197421, 197420, 174093, + // 197435, 197453, 197530, 174102, 190886, 190861, 174103, 197447, 174123, 174099, 174096, + // 174178, 172350, 197543, 197474, 174117, 172402, 172324, 172367, 190790, 197490, 190803, + // 174133, 197529, 190855, 197428, 197542, 197499, 190837, 190865, 174154, 197547, 197501, + // 190812, 190818, 197418, 172310, 190836, 197540, 172342, 190869, 197407, 197533, 190911, + // 197487, 172318, 190903, 190831, 190937, 174109, 174115, 190854, 190866, 174181, 190813, + // 174091, 172361, 172344, 190785, 197505, 197532, 197531, 172309, 172323, 174157, 197514, + // 190791, 174105, 190810, 174183, 190794, 197395, 197458, 197481, 190905, 197412, 174086, + // 197548, 197536, 172351, 190829, 174165, 197503, 172385, 172404, 197526, 172365, 197399, + // 197538, 172401, 197409, 174119, 174083, 174177, 197539, 197432, 190874, 190844, 172319, + // 174141, 190786, 174087, 172378, 190883, 172396, 174160, 190884, 174092, 174132, 197442, + // 197398, 174190, 190853, 172330, 197413, 197469, 174094, 172366, 172368, 172322, 197427, + // 174120, 197408, 197425, 172360, 197434, 172399, 173488, 151095, 159380, 84509, 212592, + // 212527, 212590, 212600, 212458, 212489, 212601, 212464, 212628, 212493, 212582, 212591, + // 212469, 212456, 212608, 212615, 212498, 212625, 212562, 212512, 212612, 212468, 212571, + // 212471, 212606, 212560, 212525, 212501, 212572, 212587, 212452, 212460, 212496, 212557, + // 212476, 212529, 212534, 212586, 212543, 212602, 212567, 212524, 212477, 212481, 212561, + // 212473, 212495, 212522, 212570, 212517, 212538, 212539, 212459, 212462, 212544, 212482, + // 212558, 212559, 212457, 212472, 212548, 212553, 212609, 212583, 212535, 212518, 212467, + // 212521, 212545, 212533, 212568, 212595, 212505, 212465, 212503, 212499, 212514, 212624, + // 212466, 212454, 212549, 212540, 212487, 212555, 212497, 212556, 212623, 212494, 212530, + // 212581, 212502, 212510, 212546, 212579, ] .map(FtUserId::new); @@ -38,7 +54,6 @@ async fn main() -> Result<(), Box> { let permit = Arc::clone(&permit); users_task.spawn(async move { let _permit = permit.acquire().await.unwrap(); - let mut page = 1; loop { if let ControlFlow::Break(result) = get_user_info(id).await { break result; @@ -47,32 +62,23 @@ async fn main() -> Result<(), Box> { }); } - let file_path = format!( - "/Users/hdoo/works/gsia/codes/gs_stat_bins/data/piscine/third_cohort/first_round/users_{}.csv", - Utc::now().format("%Y-%m-%d_%H-%M-%S") - ); - - let mut file = std::fs::File::create(&file_path).expect("Failed to create output file"); - - file.write_all("user_id|login|level\n".as_bytes())?; + let mut users = Vec::new(); while let Some(Ok(Some(user))) = users_task.join_next().await { - let level = match user.cursus_users { - Some(cursus_users) => cursus_users - .into_iter() - .find(|cursus| cursus.cursus_id.0 == 9) - .map(|user| user.level), - None => None, - }; - writeln!(file, "{:?}|{:?}|{:?}", user.id, user.login, level) - .expect("Failed to write record"); + users.push(user); + } + + if let Err(e) = std::fs::write( + "users.json", + serde_json::to_string_pretty(&users).expect("restore from des"), + ) { + tracing::error!("{e}"); } - println!("Output written to: {}", file_path); Ok(()) } -async fn get_user_info(id: FtUserId) -> ControlFlow> { +async fn get_user_info(id: FtUserId) -> ControlFlow> { let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); @@ -86,6 +92,7 @@ async fn get_user_info(id: FtUserId) -> ControlFlow> { Ok(res) => ControlFlow::Break(Some(res.user)), Err(e) => match e { FtClientError::RateLimitError(ft_rate_limit_error) => { + tracing::error!("{ft_rate_limit_error}"); sleep(Duration::new(1, 42)).await; ControlFlow::Continue(()) } From 5847da08ca7a70b0309079f3fd5155193d4bc204 Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Sat, 4 Oct 2025 17:20:52 +0900 Subject: [PATCH 02/18] Merge FtUser --- libft-api/Cargo.toml | 4 +++ libft-api/src/api/user/users_id.rs | 42 +----------------------------- libft-api/src/common/client.rs | 1 + libft-api/src/models/user.rs | 29 ++++++++++++++------- 4 files changed, 25 insertions(+), 51 deletions(-) diff --git a/libft-api/Cargo.toml b/libft-api/Cargo.toml index 78f1d83..bc0fa1a 100644 --- a/libft-api/Cargo.toml +++ b/libft-api/Cargo.toml @@ -46,6 +46,10 @@ path="bin/user_subscribe.rs" name="teams" path="bin/teams.rs" +[[bin]] +name="get_user_ext" +path="bin/get_user_ext.rs" + [[bin]] name="exam_resubscribe" path="bin/exam_resubscribe.rs" diff --git a/libft-api/src/api/user/users_id.rs b/libft-api/src/api/user/users_id.rs index f65cdd7..206a227 100644 --- a/libft-api/src/api/user/users_id.rs +++ b/libft-api/src/api/user/users_id.rs @@ -3,46 +3,6 @@ use serde::{Deserialize, Serialize}; use crate::{prelude::*, to_param}; -#[derive(Debug, Serialize, Deserialize, Builder)] -pub struct FtUserExt { - pub id: Option, - pub email: Option, - pub login: Option, - pub first_name: Option, - pub last_name: Option, - pub url: Option, - pub phone: Option, - pub displayname: Option, - pub kind: Option, - #[serde(rename = "active?")] - pub active: Option, - #[serde(rename = "alumni?")] - pub alumni: Option, - pub alumnized_at: Option, - pub anonymize_date: Option, - pub correction_point: Option, - pub created_at: Option, - pub data_erasure_date: Option, - pub image: Option, - pub location: Option, - pub pool_month: Option, - pub pool_year: Option, - pub staff: Option, - pub updated_at: Option, - pub usual_first_name: Option, - pub usual_full_name: Option, - pub wallet: Option, - pub cursus_users: Option>, - pub projects_users: Option>, - pub languages_users: Option>, - pub achievements: Option>, - pub campus: Option>, - pub campus_users: Option>, - pub titles: Option>, - pub titles_users: Option>, - pub roles: Option>, -} - #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiUsersIdRequest { pub id: FtUserIdentifier, @@ -56,7 +16,7 @@ pub struct FtApiUsersIdRequest { #[derive(Debug, Serialize, Deserialize, Builder)] #[serde(transparent)] pub struct FtApiUsersIdResponse { - pub user: FtUserExt, + pub user: FtUser, } impl FtClientSession<'_, FCHC> diff --git a/libft-api/src/common/client.rs b/libft-api/src/common/client.rs index bb1d617..01e6304 100644 --- a/libft-api/src/common/client.rs +++ b/libft-api/src/common/client.rs @@ -176,6 +176,7 @@ where } } + #[must_use] pub fn open_session<'a>(&'a self, token: &'a FtApiToken) -> FtClientSession<'a, FCHC> { // TODO: Add tracer for LOGGING // let http_session_span = span!(Level::DEBUG, "Ft API request",); diff --git a/libft-api/src/models/user.rs b/libft-api/src/models/user.rs index e174ddf..9bb80a3 100644 --- a/libft-api/src/models/user.rs +++ b/libft-api/src/models/user.rs @@ -2,35 +2,44 @@ use rsb_derive::Builder; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; -use crate::{FtDateTimeFixedOffset, FtDateTimeUtc, FtHost, FtImage, FtUrl}; +use crate::prelude::*; #[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize, Builder)] pub struct FtUser { - pub id: Option, - pub email: Option, - pub login: Option, - pub first_name: Option, - pub last_name: Option, - pub url: Option, - pub phone: Option, - pub displayname: Option, - pub kind: Option, + pub achievements: Option>, #[serde(rename = "active?")] pub active: Option, #[serde(rename = "alumni?")] pub alumni: Option, pub alumnized_at: Option, pub anonymize_date: Option, + pub campus_users: Option>, + pub campus: Option>, pub correction_point: Option, pub created_at: Option, + pub cursus_users: Option>, pub data_erasure_date: Option, + pub displayname: Option, + pub email: Option, + pub first_name: Option, + pub id: Option, pub image: Option, + pub kind: Option, + pub languages_users: Option>, + pub last_name: Option, pub location: Option, + pub login: Option, + pub phone: Option, pub pool_month: Option, pub pool_year: Option, + pub projects_users: Option>, + pub roles: Option>, #[serde(rename = "staff?")] pub staff: Option, + pub titles_users: Option>, + pub titles: Option>, pub updated_at: Option, + pub url: Option, pub usual_first_name: Option, pub usual_full_name: Option, pub wallet: Option, From e07fe4469971affc0f31e79f251e576ab930b983 Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Fri, 10 Oct 2025 17:02:35 +0900 Subject: [PATCH 03/18] refactor(api)!: remove lifetime from `FtClientSession` The `FtClientSession` no longer borrows the `FtApiToken`, but instead takes ownership of it. This simplifies the API by removing the lifetime parameter `'a` from `FtClientSession` and its related `impl` blocks. This change makes it easier to work with sessions, especially in asynchronous contexts where managing lifetimes can be complex. All method calls have been updated to reflect this new ownership model. BREAKING CHANGE: The signature of `FtClient::open_session` has changed from `open_session<'a>(&'a self, token: &'a FtApiToken) -> FtClientSession<'a, FCHC>` to `open_session(&self, token: FtApiToken) -> FtClientSession<'_, FCHC>`. Consumers of the library must now pass the `FtApiToken` by value. --- libft-api/bin/blackholed.rs | 290 ++++++++++-------- libft-api/bin/campus_users.rs | 8 +- libft-api/bin/evaluation.rs | 8 +- libft-api/bin/exam_resubscribe.rs | 2 +- libft-api/bin/final_score.rs | 6 +- libft-api/bin/get_user_ext.rs | 2 +- libft-api/bin/journals.rs | 2 +- libft-api/bin/location_stat.rs | 4 +- libft-api/bin/locations.rs | 2 +- libft-api/bin/project_stats.rs | 2 +- libft-api/bin/teams.rs | 4 +- libft-api/bin/user_creation.rs | 6 +- libft-api/bin/user_subscribe.rs | 2 +- libft-api/src/api/campus/campus_id.rs | 4 +- .../src/api/campus/campus_id_journals.rs | 4 +- .../src/api/campus/campus_id_locations.rs | 4 +- libft-api/src/api/campus/campus_id_users.rs | 4 +- libft-api/src/api/campus/campus_users.rs | 2 +- .../src/api/cursus/cursus_id_projects.rs | 4 +- libft-api/src/api/exam/exams.rs | 4 +- libft-api/src/api/group/groups.rs | 6 +- libft-api/src/api/project/project_data.rs | 4 +- libft-api/src/api/project/projects.rs | 4 +- .../src/api/project/projects_id_teams.rs | 4 +- .../project_sessions_id_scale_teams.rs | 4 +- .../project_sessions_id_teams.rs | 6 +- .../src/api/project_user/projects_users.rs | 4 +- libft-api/src/api/scale_team/scale_teams.rs | 4 +- libft-api/src/api/user/users.rs | 6 +- libft-api/src/api/user/users_id.rs | 2 +- .../users_id_correction_point_historics.rs | 4 +- .../user/users_id_correction_points_add.rs | 4 +- .../src/api/user/users_id_cursus_users.rs | 8 +- libft-api/src/api/user/users_id_locations.rs | 6 +- .../src/api/user/users_id_locations_stats.rs | 2 +- .../src/api/user/users_id_projects_users.rs | 4 +- libft-api/src/api/user/users_id_teams.rs | 4 +- libft-api/src/auth.rs | 2 + libft-api/src/common/client.rs | 19 +- 39 files changed, 239 insertions(+), 222 deletions(-) diff --git a/libft-api/bin/blackholed.rs b/libft-api/bin/blackholed.rs index d0b0c3a..1d34337 100644 --- a/libft-api/bin/blackholed.rs +++ b/libft-api/bin/blackholed.rs @@ -1,24 +1,79 @@ use std::{ops::ControlFlow, sync::Arc, time::Duration}; use libft_api::{campus_id::*, prelude::*}; -use tokio::{sync::Semaphore, task::JoinSet, time::sleep}; +use tokio::{task::JoinSet, time::sleep}; use tracing::{info, info_span}; #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); info_span!("main"); - let thread_num = 6; - let mut handles = JoinSet::new(); + let client = Arc::new(FtClient::new(FtClientReqwestConnector::new())); - let permit = Arc::new(Semaphore::new(thread_num)); - for mut page in 1..=thread_num { - let permit = Arc::clone(&permit); + let client_clone = Arc::clone(&client); + + // this will get all the data from given parameter. + // and make profer thread from rate limit. + { + let mut handles = JoinSet::new(); handles.spawn(async move { - let _permit = permit.acquire().await.unwrap(); let mut result = Vec::new(); + let client = &client_clone; + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) + .await + .unwrap(); + let session = Arc::new(client.open_session(token)); loop { - if let ControlFlow::Break(()) = users(&mut result, thread_num, &mut page).await { + if let ControlFlow::Break(()) = { + let result = &mut result; + let session_clone = Arc::clone(&session); + async move { + let res = + session_clone + .users(FtApiUsersRequest::new().with_per_page(100).with_filter( + vec![ + FtFilterOption::new( + FtFilterField::PrimaryCampusId, + vec![ + RABAT.to_string(), + // ISKANDARPUTERI.to_string(), + // MILANO.to_string(), + // NABLUS.to_string(), + // LUANDA.to_string(), + // WARSAW.to_string(), + // ANTANANARIVO.to_string(), + ], + ), + FtFilterOption::new( + FtFilterField::Kind, + vec!["student".to_string()], + ), + ], + )) + .await; + match res { + Ok(res) => { + if res.users.is_empty() { + return ControlFlow::Break(()); + } + result.extend(res.users); + info!("{}", result.len()); + // *page += thread_num; + } + Err(FtClientError::RateLimitError(_)) => { + eprintln!("rate limit, try again."); + sleep(Duration::new(1, 42)).await + } + Err(e) => { + eprintln!("other error: {e}"); + return ControlFlow::Break(()); + } + } + ControlFlow::Continue(()) + } + } + .await + { break result .into_iter() .filter_map(|user| user.id) @@ -26,93 +81,57 @@ async fn main() { } } }); - } - - let mut ids = Vec::new(); - while let Some(Ok(res)) = handles.join_next().await { - ids.extend(res); - } - info!("{:#?}", ids); - - let mut handles = JoinSet::new(); - - let mut result = Vec::new(); - for id in ids { - let permit = Arc::clone(&permit); - handles.spawn(async move { - let _permit = permit.acquire().await.unwrap(); - let mut result = Vec::new(); - let mut page = 1; - loop { - if let ControlFlow::Break(()) = cursus_users(&mut result, &id, &mut page).await { - break result; - } - } - }); - } - - while let Some(Ok(res)) = handles.join_next().await { - result.extend(res); - info!("{}", result.len()); - } - - println!("user_id,login,email,begin_at,end_at,cursus"); - result.into_iter().for_each(|cursus_user| { - println!( - "{:?},{:?},{:?},{:?},{:?},{:?}", - cursus_user.user.id, - cursus_user.user.login, - cursus_user.user.email, - cursus_user.begin_at, - cursus_user.end_at, - cursus_user.cursus.name, - ) - }); -} - -async fn cursus_users( - result: &mut Vec, - id: &FtUserId, - page: &mut i32, -) -> ControlFlow<()> { - let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) - .await - .unwrap(); - let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(&token)); - let res = session - .users_id_cursus_users( - FtApiUsersIdCursusUsersRequest::new(id.clone()) - .with_per_page(100) - .with_page(*page as u16), - ) - .await; - match res { - Ok(res) => { - if res.cursus_user.is_empty() { - return ControlFlow::Break(()); - } - result.extend(res.cursus_user); - *page += 1; - } - Err(FtClientError::RateLimitError(_)) => { - eprintln!("rate limit, try again."); - sleep(Duration::new(1, 42)).await - } - Err(e) => { - eprintln!("other error: {e}"); - return ControlFlow::Break(()); + // reserve size from X-Total + let mut result = Vec::new(); + while let Some(Ok(data)) = handles.join_next().await { + result.extend(data); } } - ControlFlow::Continue(()) } +// async fn cursus_users( +// result: &mut Vec, +// id: &FtUserId, +// page: &mut i32, +// ) -> ControlFlow<()> { +// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) +// .await +// .unwrap(); +// let client = FtClient::new(FtClientReqwestConnector::new()); +// let session = Arc::new(client.open_session(token)); +// let res = session +// .users_id_cursus_users( +// FtApiUsersIdCursusUsersRequest::new(*id) +// .with_per_page(100) +// .with_page(*page as u16), +// ) +// .await; +// match res { +// Ok(res) => { +// if res.cursus_user.is_empty() { +// return ControlFlow::Break(()); +// } +// result.extend(res.cursus_user); +// *page += 1; +// } +// Err(FtClientError::RateLimitError(_)) => { +// eprintln!("rate limit, try again."); +// sleep(Duration::new(1, 42)).await +// } +// Err(e) => { +// eprintln!("other error: {e}"); +// return ControlFlow::Break(()); +// } +// } +// ControlFlow::Continue(()) +// } + // async fn campus_users(thread_num: usize, page: &mut usize) -> ControlFlow> { // let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) // .await // .unwrap(); // let client = FtClient::new(FtClientReqwestConnector::new()); -// let session = Arc::new(client.open_session(&token)); +// let session = Arc::new(client.open_session(token)); // let res = session // .campus_users( // FtApiCampusUsersRequest::new() @@ -146,52 +165,53 @@ async fn cursus_users( // ControlFlow::Continue(()) // } -async fn users(result: &mut Vec, thread_num: usize, page: &mut usize) -> ControlFlow<()> { - let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) - .await - .unwrap(); - let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(&token)); - let res = session - .users( - FtApiUsersRequest::new() - .with_per_page(100) - .with_page(*page as u16) - .with_filter(vec![ - FtFilterOption::new( - FtFilterField::PrimaryCampusId, - vec![ - RABAT.to_string(), - ISKANDARPUTERI.to_string(), - MILANO.to_string(), - NABLUS.to_string(), - LUANDA.to_string(), - WARSAW.to_string(), - ANTANANARIVO.to_string(), - ], - ), - FtFilterOption::new(FtFilterField::Kind, vec!["student".to_string()]), - ]), - ) - .await; - - match res { - Ok(res) => { - if res.users.is_empty() { - return ControlFlow::Break(()); - } - result.extend(res.users); - info!("{}", result.len()); - *page += thread_num; - } - Err(FtClientError::RateLimitError(_)) => { - eprintln!("rate limit, try again."); - sleep(Duration::new(1, 42)).await - } - Err(e) => { - eprintln!("other error: {e}"); - return ControlFlow::Break(()); - } - } - ControlFlow::Continue(()) -} +// async fn users( +// result: &mut Vec, +// client: &Arc>, +// ) -> ControlFlow<()> { +// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) +// .await +// .unwrap(); +// let session = Arc::new(client.open_session(token)); +// let res = session +// .users( +// FtApiUsersRequest::new() +// .with_per_page(100) +// .with_filter(vec![ +// FtFilterOption::new( +// FtFilterField::PrimaryCampusId, +// vec![ +// RABAT.to_string(), +// ISKANDARPUTERI.to_string(), +// MILANO.to_string(), +// NABLUS.to_string(), +// LUANDA.to_string(), +// WARSAW.to_string(), +// ANTANANARIVO.to_string(), +// ], +// ), +// FtFilterOption::new(FtFilterField::Kind, vec!["student".to_string()]), +// ]), +// ) +// .await; +// +// match res { +// Ok(res) => { +// if res.users.is_empty() { +// return ControlFlow::Break(()); +// } +// result.extend(res.users); +// info!("{}", result.len()); +// // *page += thread_num; +// } +// Err(FtClientError::RateLimitError(_)) => { +// eprintln!("rate limit, try again."); +// sleep(Duration::new(1, 42)).await +// } +// Err(e) => { +// eprintln!("other error: {e}"); +// return ControlFlow::Break(()); +// } +// } +// ControlFlow::Continue(()) +// } diff --git a/libft-api/bin/campus_users.rs b/libft-api/bin/campus_users.rs index b7a9ccb..04b303f 100644 --- a/libft-api/bin/campus_users.rs +++ b/libft-api/bin/campus_users.rs @@ -128,10 +128,10 @@ async fn get_projects_users( .await .unwrap(); let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(&token)); + let session = Arc::new(client.open_session(token)); let res = session .users_id_projects_users( - FtApiUsersIdProjectsUsersRequest::new(id.clone()) + FtApiUsersIdProjectsUsersRequest::new(*id) .with_per_page(100) .with_page(*page as u16), ) @@ -162,7 +162,7 @@ async fn get_projects_users( // .await // .unwrap(); // let client = FtClient::new(FtClientReqwestConnector::new()); -// let session = Arc::new(client.open_session(&token)); +// let session = Arc::new(client.open_sssion(token)); // let res = session // .users( // FtApiUsersRequest::new() @@ -175,7 +175,7 @@ async fn get_projects_users( // .with_filter(vec![ // FtFilterOption::new( // FtFilterField::PrimaryCampusId, -// vec![GYEONGSAN.to_string()], +// vec![GYONGSAN.to_string()], // ), // FtFilterOption::new(FtFilterField::Kind, vec!["student".to_string()]), // ]), diff --git a/libft-api/bin/evaluation.rs b/libft-api/bin/evaluation.rs index 156eec7..972959a 100644 --- a/libft-api/bin/evaluation.rs +++ b/libft-api/bin/evaluation.rs @@ -212,10 +212,10 @@ async fn get_evaluation_historics( .await .unwrap(); let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(&token)); + let session = Arc::new(client.open_session(token)); let res = session .users_id_correction_point_historics( - FtApiUsersIdCorrectionPointHistoricsRequest::new(id.clone()) + FtApiUsersIdCorrectionPointHistoricsRequest::new(*id) .with_filter(vec![FtFilterOption::new( FtFilterField::Sum, vec!["-1".to_owned()], @@ -229,7 +229,7 @@ async fn get_evaluation_historics( if res.historics.is_empty() { return ControlFlow::Break(()); } - result.entry(id.clone()).or_default().extend(res.historics); + result.entry(*id).or_default().extend(res.historics); *page += 1; } Err(FtClientError::RateLimitError(_)) => sleep(Duration::new(1, 42)).await, @@ -250,7 +250,7 @@ async fn get_scale_teams( .await .unwrap(); let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(&token)); + let session = Arc::new(client.open_session(token)); let res = session .scale_teams( FtApiScaleTeamsRequest::new() diff --git a/libft-api/bin/exam_resubscribe.rs b/libft-api/bin/exam_resubscribe.rs index 16da4de..767102b 100644 --- a/libft-api/bin/exam_resubscribe.rs +++ b/libft-api/bin/exam_resubscribe.rs @@ -28,7 +28,7 @@ async fn main() { .unwrap(); let client = FtClient::new(FtClientReqwestConnector::new()); - let session = client.open_session(&token); + let session = client.open_session(token); let exam_res = session .exams_users_post( diff --git a/libft-api/bin/final_score.rs b/libft-api/bin/final_score.rs index fa76b16..4191c14 100644 --- a/libft-api/bin/final_score.rs +++ b/libft-api/bin/final_score.rs @@ -53,7 +53,7 @@ async fn main() -> Result<(), Box> { while let Some(Ok(res)) = teams_task.join_next().await { for team in res { tracing::info!("{}", team.id.0); - teams_by_id.entry(team.id.clone()).or_insert(team); + teams_by_id.entry(team.id).or_insert(team); } } @@ -166,7 +166,7 @@ async fn get_user_id_teams( .await .unwrap(); let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(&token)); + let session = Arc::new(client.open_session(token)); let res = session .users_id_teams( FtApiUsersIdTeamsRequest::new(id) @@ -208,7 +208,7 @@ async fn get_scale_teams( .await .unwrap(); let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(&token)); + let session = Arc::new(client.open_session(token)); let res = session .scale_teams( FtApiScaleTeamsRequest::new() diff --git a/libft-api/bin/get_user_ext.rs b/libft-api/bin/get_user_ext.rs index 336e2a6..976548a 100644 --- a/libft-api/bin/get_user_ext.rs +++ b/libft-api/bin/get_user_ext.rs @@ -83,7 +83,7 @@ async fn get_user_info(id: FtUserId) -> ControlFlow> { .await .unwrap(); let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(&token)); + let session = Arc::new(client.open_session(token)); let res = session .users_id(FtApiUsersIdRequest::new(FtUserIdentifier::UserId(id))) .await; diff --git a/libft-api/bin/journals.rs b/libft-api/bin/journals.rs index f6218e6..dbe2c40 100644 --- a/libft-api/bin/journals.rs +++ b/libft-api/bin/journals.rs @@ -19,7 +19,7 @@ async fn main() { .unwrap(); let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(&token)); + let session = Arc::new(client.open_session(token)); let res = session .campus_id_journals( FtApiCampusIdJournalsRequest::new( diff --git a/libft-api/bin/location_stat.rs b/libft-api/bin/location_stat.rs index 8039b41..d20e92c 100644 --- a/libft-api/bin/location_stat.rs +++ b/libft-api/bin/location_stat.rs @@ -120,10 +120,10 @@ // .await // .unwrap(); // let client = FtClient::new(FtClientReqwestConnector::new()); -// let session = Arc::new(client.open_session(&token)); +// let session = Arc::new(client.open_session(token)); // let res = session // .users_id_locations_stats( -// FtApiUsersIdLocationsStatsRequest::new(id.clone()) +// FtApiUsersIdLocationsStatsRequest::new(id) // .with_begin_at("2024-1-1".parse().unwrap()) // .with_end_at("2025-3-1".parse().unwrap()), // ) diff --git a/libft-api/bin/locations.rs b/libft-api/bin/locations.rs index b9e06c2..1abb5c8 100644 --- a/libft-api/bin/locations.rs +++ b/libft-api/bin/locations.rs @@ -10,7 +10,7 @@ async fn main() { .unwrap(); let client = FtClient::new(FtClientReqwestConnector::new()); - let session = client.open_session(&token); + let session = client.open_session(token); let mut page = 1; loop { diff --git a/libft-api/bin/project_stats.rs b/libft-api/bin/project_stats.rs index c2493ac..04cafdb 100644 --- a/libft-api/bin/project_stats.rs +++ b/libft-api/bin/project_stats.rs @@ -10,7 +10,7 @@ async fn main() { .unwrap(); let client = FtClient::new(FtClientReqwestConnector::new()); - let session = client.open_session(&token); + let session = client.open_session(token); let mut page = 1; println!("login|project|final_mark|retriable_at|status|team_mate"); diff --git a/libft-api/bin/teams.rs b/libft-api/bin/teams.rs index 1a433ef..008d8a4 100644 --- a/libft-api/bin/teams.rs +++ b/libft-api/bin/teams.rs @@ -163,7 +163,7 @@ async fn post_scale_team( .await .unwrap(); let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(&token)); + let session = Arc::new(client.open_session(token)); session .scale_teams_multiple_create_post(FtApiScaleTeamsMultipleCreateRequest::new(bodys)) @@ -179,7 +179,7 @@ async fn get_project_teams( .await .unwrap(); let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(&token)); + let session = Arc::new(client.open_session(token)); let res = session .project_sessions_id_teams( FtApiProjectSessionsTeamsRequest::new(project_session_id) diff --git a/libft-api/bin/user_creation.rs b/libft-api/bin/user_creation.rs index 0778f34..ccc5915 100644 --- a/libft-api/bin/user_creation.rs +++ b/libft-api/bin/user_creation.rs @@ -66,7 +66,7 @@ async fn assign_group(id: FtUserId) -> Result Result .unwrap(); let client = FtClient::new(FtClientReqwestConnector::new()); - let session = client.open_session(&token); + let session = client.open_session(token); session .users_post(FtApiUsersPostRequest::new(FtApiUserPostBody { diff --git a/libft-api/bin/user_subscribe.rs b/libft-api/bin/user_subscribe.rs index 8551fe4..c21d3cd 100644 --- a/libft-api/bin/user_subscribe.rs +++ b/libft-api/bin/user_subscribe.rs @@ -43,7 +43,7 @@ async fn main() { .unwrap(); let client = FtClient::new(FtClientReqwestConnector::new()); - let session = client.open_session(&token); + let session = client.open_session(token); for exam_set in exam_sets.iter() { let project_res = session diff --git a/libft-api/src/api/campus/campus_id.rs b/libft-api/src/api/campus/campus_id.rs index 48c2c99..e28341e 100644 --- a/libft-api/src/api/campus/campus_id.rs +++ b/libft-api/src/api/campus/campus_id.rs @@ -23,7 +23,7 @@ pub struct FtApiCampusIdResponse { pub campus: Vec, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -79,7 +79,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); let res = session.campus_id(FtApiCampusIdRequest::new()).await; assert!(res.is_ok()); diff --git a/libft-api/src/api/campus/campus_id_journals.rs b/libft-api/src/api/campus/campus_id_journals.rs index fe90b6b..202164a 100644 --- a/libft-api/src/api/campus/campus_id_journals.rs +++ b/libft-api/src/api/campus/campus_id_journals.rs @@ -27,7 +27,7 @@ pub struct FtApiCampusIdJournalsResponse { pub journals: Vec, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -84,7 +84,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); let res = session .campus_id_journals(FtApiCampusIdJournalsRequest::new( FtCampusId::new(GYEONGSAN), diff --git a/libft-api/src/api/campus/campus_id_locations.rs b/libft-api/src/api/campus/campus_id_locations.rs index 290a6d2..eb85398 100644 --- a/libft-api/src/api/campus/campus_id_locations.rs +++ b/libft-api/src/api/campus/campus_id_locations.rs @@ -25,7 +25,7 @@ pub struct FtApiCampusIdLocationsResponse { pub location: Vec, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -80,7 +80,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); let res = session .campus_id_locations( FtApiCampusIdLocationsRequest::new(FtCampusId::new(GYEONGSAN)).with_per_page(100), diff --git a/libft-api/src/api/campus/campus_id_users.rs b/libft-api/src/api/campus/campus_id_users.rs index d8c0a72..85499d1 100644 --- a/libft-api/src/api/campus/campus_id_users.rs +++ b/libft-api/src/api/campus/campus_id_users.rs @@ -24,7 +24,7 @@ pub struct FtApiCampusIdUsersResponse { pub users: Vec, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -77,7 +77,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); let res = session .campus_id_users(FtApiCampusIdUsersRequest::new(FtCampusId::new(GYEONGSAN))) .await; diff --git a/libft-api/src/api/campus/campus_users.rs b/libft-api/src/api/campus/campus_users.rs index d71cb4c..bab6d3b 100644 --- a/libft-api/src/api/campus/campus_users.rs +++ b/libft-api/src/api/campus/campus_users.rs @@ -76,7 +76,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); let res = session.campus_users(FtApiCampusUsersRequest::new()).await; assert!(res.is_ok()); diff --git a/libft-api/src/api/cursus/cursus_id_projects.rs b/libft-api/src/api/cursus/cursus_id_projects.rs index bc9bf38..10c7273 100644 --- a/libft-api/src/api/cursus/cursus_id_projects.rs +++ b/libft-api/src/api/cursus/cursus_id_projects.rs @@ -24,7 +24,7 @@ pub struct FtApiCursusIdProjectsResponse { pub projects: Vec, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -80,7 +80,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); let res = session .cursus_id_projects(FtApiCursusIdProjectsRequest::new(FtCursusId::new( FT_CURSUS_ID, diff --git a/libft-api/src/api/exam/exams.rs b/libft-api/src/api/exam/exams.rs index f4dbb6b..69a0b9d 100644 --- a/libft-api/src/api/exam/exams.rs +++ b/libft-api/src/api/exam/exams.rs @@ -53,7 +53,7 @@ where /// reqwest::Client::new(), /// )); /// - /// let session = client.open_session(&token); + /// let session = client.open_session(token); /// /// let res = session /// .exams_users_post( @@ -126,7 +126,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); session.exams(FtApiExamsRequest::new()).await.unwrap(); } diff --git a/libft-api/src/api/group/groups.rs b/libft-api/src/api/group/groups.rs index 0c71a49..d5c69ea 100644 --- a/libft-api/src/api/group/groups.rs +++ b/libft-api/src/api/group/groups.rs @@ -36,7 +36,7 @@ pub struct FtApiGroupsResponse { pub groups: Vec, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -74,7 +74,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); let res = session .groups_users_post(FtApiGroupsUsersPostRequest::new(FtApiGroupsUsersPostBody { @@ -97,7 +97,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); session.groups(FtApiGroupsRequest::new()).await.unwrap(); } diff --git a/libft-api/src/api/project/project_data.rs b/libft-api/src/api/project/project_data.rs index 6180f6a..5faf52c 100644 --- a/libft-api/src/api/project/project_data.rs +++ b/libft-api/src/api/project/project_data.rs @@ -24,7 +24,7 @@ pub struct FtApiProjectDataResponse { pub project_data: Vec, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -79,7 +79,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); let res = session.project_data(FtApiProjectDataRequest::new()).await; diff --git a/libft-api/src/api/project/projects.rs b/libft-api/src/api/project/projects.rs index fd941f3..3f9b5da 100644 --- a/libft-api/src/api/project/projects.rs +++ b/libft-api/src/api/project/projects.rs @@ -24,7 +24,7 @@ pub struct FtApiProjectResponse { pub projects: Vec, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -77,7 +77,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); let res = session.projects(FtApiProjectRequest::new()).await; assert!(res.is_ok()); diff --git a/libft-api/src/api/project/projects_id_teams.rs b/libft-api/src/api/project/projects_id_teams.rs index 3bbbc07..c81fc73 100644 --- a/libft-api/src/api/project/projects_id_teams.rs +++ b/libft-api/src/api/project/projects_id_teams.rs @@ -24,7 +24,7 @@ pub struct FtApiProjectsIdTeamsResponse { pub teams: Vec, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -80,7 +80,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); let res = session .projects_id_teams(FtApiProjectsIdTeamsRequest::new(FtProjectId::new(1314))) .await; diff --git a/libft-api/src/api/project_session/project_sessions_id_scale_teams.rs b/libft-api/src/api/project_session/project_sessions_id_scale_teams.rs index d396b83..ebf83b0 100644 --- a/libft-api/src/api/project_session/project_sessions_id_scale_teams.rs +++ b/libft-api/src/api/project_session/project_sessions_id_scale_teams.rs @@ -16,7 +16,7 @@ pub struct FtApiProjectSessionsScaleTeamsRequest { pub project_session_id: FtProjectSessionId, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -50,7 +50,7 @@ mod tests { let req = FtApiProjectSessionsScaleTeamsRequest::new(FtProjectSessionId::new(LIBFT)); - let session = client.open_session(&token); + let session = client.open_session(token); let res = session.project_sessions_scale_teams(req).await; assert!(res.is_ok()); } diff --git a/libft-api/src/api/project_session/project_sessions_id_teams.rs b/libft-api/src/api/project_session/project_sessions_id_teams.rs index dc00dd6..30070ce 100644 --- a/libft-api/src/api/project_session/project_sessions_id_teams.rs +++ b/libft-api/src/api/project_session/project_sessions_id_teams.rs @@ -23,7 +23,7 @@ pub struct FtApiProjectSessionsTeamsRequest { pub per_page: Option, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -82,7 +82,7 @@ mod tests { let reqest = FtApiProjectSessionsTeamsRequest::new(FtProjectSessionId::new(LIBFT)); - let session = client.open_session(&token); + let session = client.open_session(token); let result = session.project_sessions_id_teams(reqest).await; assert!(result.is_ok()); } @@ -97,7 +97,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); let res = session .project_sessions_id_teams( FtApiProjectSessionsTeamsRequest::new(FtProjectSessionId::new(LIBFT)).with_filter( diff --git a/libft-api/src/api/project_user/projects_users.rs b/libft-api/src/api/project_user/projects_users.rs index 0ee20b1..0547f48 100644 --- a/libft-api/src/api/project_user/projects_users.rs +++ b/libft-api/src/api/project_user/projects_users.rs @@ -36,7 +36,7 @@ pub struct FtApiProjectsUsersResponse { pub projects_users: Vec, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -101,7 +101,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); let project_ids = ALL_INNER_SUBJECTS_ID .into_iter() .map(|id| id.to_string()) diff --git a/libft-api/src/api/scale_team/scale_teams.rs b/libft-api/src/api/scale_team/scale_teams.rs index 5b61e28..bf5cb89 100644 --- a/libft-api/src/api/scale_team/scale_teams.rs +++ b/libft-api/src/api/scale_team/scale_teams.rs @@ -35,7 +35,7 @@ pub struct FtApiScaleTeamsMultipleCreateResponse { pub scale_teams: Vec, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -102,7 +102,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); let res = session .scale_teams(FtApiScaleTeamsRequest::new().with_filter(vec![ FtFilterOption::new(FtFilterField::CampusId, vec![GYEONGSAN.to_string()]), diff --git a/libft-api/src/api/user/users.rs b/libft-api/src/api/user/users.rs index 2e69647..4df58f9 100644 --- a/libft-api/src/api/user/users.rs +++ b/libft-api/src/api/user/users.rs @@ -42,7 +42,7 @@ pub struct FtApiUsersResponse { pub users: Vec, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -102,7 +102,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); let res = session.users(FtApiUsersRequest::new()).await; assert!(res.is_ok()); @@ -118,7 +118,7 @@ mod tests { // reqwest::Client::new(), // )); // - // let session = client.open_session(&token); + // let session = client.open_session(token); // let res = session // .users_post(FtApiUsersPostRequest::new(FtApiUserPostBody { // email: "yondoo@42gyeongsan.kr".to_string(), diff --git a/libft-api/src/api/user/users_id.rs b/libft-api/src/api/user/users_id.rs index 206a227..73b16cc 100644 --- a/libft-api/src/api/user/users_id.rs +++ b/libft-api/src/api/user/users_id.rs @@ -76,7 +76,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); /* let res = */ session .users_id(FtApiUsersIdRequest::new(FtUserIdentifier::Login( diff --git a/libft-api/src/api/user/users_id_correction_point_historics.rs b/libft-api/src/api/user/users_id_correction_point_historics.rs index 204a774..9d16cd7 100644 --- a/libft-api/src/api/user/users_id_correction_point_historics.rs +++ b/libft-api/src/api/user/users_id_correction_point_historics.rs @@ -23,7 +23,7 @@ pub struct FtApiUsersIdCorrectionPointHistoricsResponse { pub historics: Vec, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -77,7 +77,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); let res = session .users_id_correction_point_historics(FtApiUsersIdCorrectionPointHistoricsRequest::new( FtUserId::new(TEST_USER_YONDOO_ID), diff --git a/libft-api/src/api/user/users_id_correction_points_add.rs b/libft-api/src/api/user/users_id_correction_points_add.rs index 96f4fa4..b3e62c2 100644 --- a/libft-api/src/api/user/users_id_correction_points_add.rs +++ b/libft-api/src/api/user/users_id_correction_points_add.rs @@ -22,7 +22,7 @@ pub struct FtCorrectionPointsReason(String); #[derive(Debug, Eq, Hash, PartialEq, PartialOrd, Clone, Serialize, Deserialize, ValueStruct)] pub struct FtCorrectionPointsAmount(i32); -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -68,7 +68,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); let res = session .users_id_correction_points_add(FtApiUsersIdCorrectionPointsAddRequest { id: FtUserId::new(crate::info::TEST_USER_YONDOO_ID), diff --git a/libft-api/src/api/user/users_id_cursus_users.rs b/libft-api/src/api/user/users_id_cursus_users.rs index 57a50b8..e7a4453 100644 --- a/libft-api/src/api/user/users_id_cursus_users.rs +++ b/libft-api/src/api/user/users_id_cursus_users.rs @@ -97,7 +97,7 @@ where /// reqwest::Client::new(), /// )); /// - /// let session = client.open_session(&token); + /// let session = client.open_session(token); /// /// let req = FtApiUsersIdCursusUsersRequest::new(FtUserId::new(TEST_USER_YONDOO06_ID)) /// .with_page(1) @@ -158,7 +158,7 @@ where &self, req: FtApiUsersIdCursusUsersPostRequest, ) -> ClientResult { - let url = &format!("users/{}/cursus_users", req.cursus_user.user_id.clone()); + let url = &format!("users/{}/cursus_users", req.cursus_user.user_id); self.http_session_api.http_post(url, &req).await } @@ -179,7 +179,7 @@ mod tests { // reqwest::Client::new(), // )); // - // let session = client.open_session(&token); + // let session = client.open_session(token); // let res = session // .users_id_cursus_users_post(FtApiUsersIdCursusUsersPostRequest::new( // FtApiCursusUsersBody { @@ -208,7 +208,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); let res = session .users_id_cursus_users(FtApiUsersIdCursusUsersRequest::new(FtUserId::new(174_083))) .await; diff --git a/libft-api/src/api/user/users_id_locations.rs b/libft-api/src/api/user/users_id_locations.rs index 9ad93eb..632024d 100644 --- a/libft-api/src/api/user/users_id_locations.rs +++ b/libft-api/src/api/user/users_id_locations.rs @@ -1,7 +1,3 @@ -use std::collections::HashMap; - -use chrono::Days; -use chrono::NaiveDate; use rsb_derive::Builder; use serde::{Deserialize, Serialize}; @@ -77,7 +73,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); let res = session .users_id_locations( FtApiUsersIdLocationsRequest::new(FtUserId::new(TEST_USER_YONDOO_ID)).with_filter( diff --git a/libft-api/src/api/user/users_id_locations_stats.rs b/libft-api/src/api/user/users_id_locations_stats.rs index 2c15e0d..d12bb5c 100644 --- a/libft-api/src/api/user/users_id_locations_stats.rs +++ b/libft-api/src/api/user/users_id_locations_stats.rs @@ -74,7 +74,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); let end_at = Local::now().date_naive(); let begin_at = end_at .checked_sub_days(Days::new(5)) diff --git a/libft-api/src/api/user/users_id_projects_users.rs b/libft-api/src/api/user/users_id_projects_users.rs index 4f3a6b9..d13843f 100644 --- a/libft-api/src/api/user/users_id_projects_users.rs +++ b/libft-api/src/api/user/users_id_projects_users.rs @@ -27,7 +27,7 @@ pub struct FtApiUsersIdProjectsUsersResponse { pub projects_users: Vec, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -86,7 +86,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); let res = session .users_id_projects_users(FtApiUsersIdProjectsUsersRequest::new(FtUserId::new( TEST_USER_YONDOO_ID, diff --git a/libft-api/src/api/user/users_id_teams.rs b/libft-api/src/api/user/users_id_teams.rs index abd3a3c..5283f92 100644 --- a/libft-api/src/api/user/users_id_teams.rs +++ b/libft-api/src/api/user/users_id_teams.rs @@ -26,7 +26,7 @@ pub struct FtApiUsersIdTeamsResponse { pub teams: Vec, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -84,7 +84,7 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); + let session = client.open_session(token); let res = session .users_id_teams(FtApiUsersIdTeamsRequest::new(FtUserId::new( TEST_USER_YONDOO_ID, diff --git a/libft-api/src/auth.rs b/libft-api/src/auth.rs index 684410b..e44cb9d 100644 --- a/libft-api/src/auth.rs +++ b/libft-api/src/auth.rs @@ -9,6 +9,7 @@ use std::{ use chrono::{DateTime, TimeZone, Utc}; use serde::{Deserialize, Serialize}; +// TODO: add scope pub struct AuthInfo { uid: String, secret: String, @@ -27,6 +28,7 @@ impl AuthInfo { } #[inline] + // TODO: replace scope to field 'scope' pub fn get_params(&self) -> [(&str, &str); 4] { [ ("grant_type", "client_credentials"), diff --git a/libft-api/src/common/client.rs b/libft-api/src/common/client.rs index 01e6304..e93ed47 100644 --- a/libft-api/src/common/client.rs +++ b/libft-api/src/common/client.rs @@ -41,7 +41,7 @@ pub struct FtClientHttpSessionApi<'a, FCHC> where FCHC: FtClientHttpConnector + Send, { - token: &'a FtApiToken, + token: FtApiToken, pub client: &'a FtClient, } @@ -176,8 +176,7 @@ where } } - #[must_use] - pub fn open_session<'a>(&'a self, token: &'a FtApiToken) -> FtClientSession<'a, FCHC> { + pub fn open_session(&self, token: FtApiToken) -> FtClientSession { // TODO: Add tracer for LOGGING // let http_session_span = span!(Level::DEBUG, "Ft API request",); @@ -201,7 +200,7 @@ where } } -impl<'a, FCHC> FtClientHttpSessionApi<'a, FCHC> +impl FtClientHttpSessionApi<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -212,7 +211,7 @@ where self.client .http_api .connector - .http_get_uri(full_uri, self.token) + .http_get_uri(full_uri, &self.token) .await } @@ -229,7 +228,7 @@ where self.client .http_api .connector - .http_get(method_relative_uri, self.token, params) + .http_get(method_relative_uri, &self.token, params) .await } @@ -245,7 +244,7 @@ where self.client .http_api .connector - .http_post(method_relative_uri, self.token, request) + .http_post(method_relative_uri, &self.token, request) .await } @@ -257,7 +256,7 @@ where self.client .http_api .connector - .http_post_uri(full_uri, self.token, request) + .http_post_uri(full_uri, &self.token, request) .await } @@ -273,7 +272,7 @@ where self.client .http_api .connector - .http_delete(method_relative_uri, self.token, request) + .http_delete(method_relative_uri, &self.token, request) .await } @@ -285,7 +284,7 @@ where self.client .http_api .connector - .http_delete_uri(full_uri, self.token, request) + .http_delete_uri(full_uri, &self.token, request) .await } } From c288bf0eaf7e35fd3654fc78c5f455dfc43e990a Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Mon, 13 Oct 2025 16:17:44 +0900 Subject: [PATCH 04/18] ratelimiter --- libft-api/bin/blackholed.rs | 185 +++---------------- libft-api/bin/campus_users.rs | 10 +- libft-api/src/common.rs | 3 + libft-api/src/common/client.rs | 31 +++- libft-api/src/common/error.rs | 6 +- libft-api/src/common/ratelimiter.rs | 277 ++++++++++++++++++++++++++++ libft-api/src/connector.rs | 46 ++--- libft-api/src/lib.rs | 4 +- 8 files changed, 367 insertions(+), 195 deletions(-) create mode 100644 libft-api/src/common/ratelimiter.rs diff --git a/libft-api/bin/blackholed.rs b/libft-api/bin/blackholed.rs index 1d34337..5f97bb6 100644 --- a/libft-api/bin/blackholed.rs +++ b/libft-api/bin/blackholed.rs @@ -8,49 +8,46 @@ use tracing::{info, info_span}; async fn main() { tracing_subscriber::fmt::init(); info_span!("main"); - let client = Arc::new(FtClient::new(FtClientReqwestConnector::new())); + let client = Arc::new(FtClient::with_ratelimits( + FtClientReqwestConnector::new(), + 8, + 14000, + )); + let thread_num = 8; + let mut handles = JoinSet::new(); - let client_clone = Arc::clone(&client); - - // this will get all the data from given parameter. - // and make profer thread from rate limit. - { - let mut handles = JoinSet::new(); + for i in 0..8 { + let client = Arc::clone(&client); handles.spawn(async move { let mut result = Vec::new(); - let client = &client_clone; let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); let session = Arc::new(client.open_session(token)); + let mut page = i; loop { + let page = &mut page; if let ControlFlow::Break(()) = { let result = &mut result; let session_clone = Arc::clone(&session); async move { - let res = - session_clone - .users(FtApiUsersRequest::new().with_per_page(100).with_filter( - vec![ + let res = session_clone + .users( + FtApiUsersRequest::new() + .with_page(*page) + .with_per_page(100) + .with_filter(vec![ FtFilterOption::new( FtFilterField::PrimaryCampusId, - vec![ - RABAT.to_string(), - // ISKANDARPUTERI.to_string(), - // MILANO.to_string(), - // NABLUS.to_string(), - // LUANDA.to_string(), - // WARSAW.to_string(), - // ANTANANARIVO.to_string(), - ], + vec![SEOUL.to_string()], ), FtFilterOption::new( FtFilterField::Kind, vec!["student".to_string()], ), - ], - )) - .await; + ]), + ) + .await; match res { Ok(res) => { if res.users.is_empty() { @@ -58,7 +55,7 @@ async fn main() { } result.extend(res.users); info!("{}", result.len()); - // *page += thread_num; + *page += thread_num; } Err(FtClientError::RateLimitError(_)) => { eprintln!("rate limit, try again."); @@ -81,137 +78,11 @@ async fn main() { } } }); - // reserve size from X-Total - let mut result = Vec::new(); - while let Some(Ok(data)) = handles.join_next().await { - result.extend(data); - } } -} - -// async fn cursus_users( -// result: &mut Vec, -// id: &FtUserId, -// page: &mut i32, -// ) -> ControlFlow<()> { -// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) -// .await -// .unwrap(); -// let client = FtClient::new(FtClientReqwestConnector::new()); -// let session = Arc::new(client.open_session(token)); -// let res = session -// .users_id_cursus_users( -// FtApiUsersIdCursusUsersRequest::new(*id) -// .with_per_page(100) -// .with_page(*page as u16), -// ) -// .await; -// match res { -// Ok(res) => { -// if res.cursus_user.is_empty() { -// return ControlFlow::Break(()); -// } -// result.extend(res.cursus_user); -// *page += 1; -// } -// Err(FtClientError::RateLimitError(_)) => { -// eprintln!("rate limit, try again."); -// sleep(Duration::new(1, 42)).await -// } -// Err(e) => { -// eprintln!("other error: {e}"); -// return ControlFlow::Break(()); -// } -// } -// ControlFlow::Continue(()) -// } -// async fn campus_users(thread_num: usize, page: &mut usize) -> ControlFlow> { -// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) -// .await -// .unwrap(); -// let client = FtClient::new(FtClientReqwestConnector::new()); -// let session = Arc::new(client.open_session(token)); -// let res = session -// .campus_users( -// FtApiCampusUsersRequest::new() -// .with_per_page(100) -// .with_page(*page as u16) -// .with_filter(vec![ -// FtFilterOption::new(FtFilterField::CampusId, vec![SINGAPORE.to_string()]), -// FtFilterOption::new(FtFilterField::Status, vec!["student".to_string()]), -// ]), -// ) -// .await; -// let mut result = Vec::new(); -// -// match res { -// Ok(res) => { -// if res.campus_users.is_empty() { -// return ControlFlow::Break(result); -// } -// result.extend(res.campus_users); -// *page += thread_num; -// } -// Err(FtClientError::RateLimitError(_)) => { -// eprintln!("rate limit, try again."); -// sleep(Duration::new(1, 42)).await -// } -// Err(e) => { -// eprintln!("other error: {e}"); -// return ControlFlow::Break(result); -// } -// } -// ControlFlow::Continue(()) -// } - -// async fn users( -// result: &mut Vec, -// client: &Arc>, -// ) -> ControlFlow<()> { -// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) -// .await -// .unwrap(); -// let session = Arc::new(client.open_session(token)); -// let res = session -// .users( -// FtApiUsersRequest::new() -// .with_per_page(100) -// .with_filter(vec![ -// FtFilterOption::new( -// FtFilterField::PrimaryCampusId, -// vec![ -// RABAT.to_string(), -// ISKANDARPUTERI.to_string(), -// MILANO.to_string(), -// NABLUS.to_string(), -// LUANDA.to_string(), -// WARSAW.to_string(), -// ANTANANARIVO.to_string(), -// ], -// ), -// FtFilterOption::new(FtFilterField::Kind, vec!["student".to_string()]), -// ]), -// ) -// .await; -// -// match res { -// Ok(res) => { -// if res.users.is_empty() { -// return ControlFlow::Break(()); -// } -// result.extend(res.users); -// info!("{}", result.len()); -// // *page += thread_num; -// } -// Err(FtClientError::RateLimitError(_)) => { -// eprintln!("rate limit, try again."); -// sleep(Duration::new(1, 42)).await -// } -// Err(e) => { -// eprintln!("other error: {e}"); -// return ControlFlow::Break(()); -// } -// } -// ControlFlow::Continue(()) -// } + // reserve size from X-Total + let mut result = Vec::new(); + while let Some(Ok(data)) = handles.join_next().await { + result.extend(data); + } +} diff --git a/libft-api/bin/campus_users.rs b/libft-api/bin/campus_users.rs index 04b303f..55c66c5 100644 --- a/libft-api/bin/campus_users.rs +++ b/libft-api/bin/campus_users.rs @@ -1,8 +1,7 @@ -use std::{io::Write, ops::ControlFlow, sync::Arc, time::Duration}; - use chrono::Utc; use libft_api::prelude::*; use rvstruct::ValueStruct; +use std::{io::Write, ops::ControlFlow, sync::Arc, time::Duration}; use tokio::{sync::Semaphore, task::JoinSet, time::sleep}; use tracing::info; @@ -11,6 +10,7 @@ async fn main() -> Result<(), Box> { tracing_subscriber::fmt::init(); let thread_num = 4; let permit = Arc::new(Semaphore::new(thread_num)); + // 3기 2차? let ids = [ 212531, 212530, 212529, 212528, 212527, 212526, 212525, 212524, 212523, 212522, 212521, 212520, 212519, 212518, 212517, 212516, 212515, 212514, 212513, 212512, 212511, 212510, @@ -85,10 +85,9 @@ async fn main() -> Result<(), Box> { let mut file = std::fs::File::create(&file_path).expect("Failed to create output file"); - file.write_all( - "user_id,login,project_name,marked_at,created_at,final_mark,updated_at\n".as_bytes(), - )?; + file.write_all(&serde_json::to_vec(&result).unwrap()); + /* for projects_user in result { let (id, login) = { let user = projects_user @@ -114,6 +113,7 @@ async fn main() -> Result<(), Box> { ) .expect("Failed to write record"); } + */ println!("Output written to: {}", file_path); Ok(()) diff --git a/libft-api/src/common.rs b/libft-api/src/common.rs index 1ec009c..90dad11 100644 --- a/libft-api/src/common.rs +++ b/libft-api/src/common.rs @@ -6,3 +6,6 @@ mod error; pub use param::*; mod param; + +pub use ratelimiter::*; +mod ratelimiter; diff --git a/libft-api/src/common/client.rs b/libft-api/src/common/client.rs index e93ed47..3cd9657 100644 --- a/libft-api/src/common/client.rs +++ b/libft-api/src/common/client.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; use url::Url; +use crate::common::ratelimiter::RateLimiter; use crate::{FtApiToken, FtClientError, FtClientReqwestConnector}; pub type ClientResult = std::result::Result; @@ -16,6 +17,7 @@ where FCHC: FtClientHttpConnector + Send, { pub http_api: FtClientHttpApi, + pub ratelimiter: RateLimiter, } #[derive(Clone, Debug)] @@ -29,11 +31,11 @@ where pub struct FtClientHttpApiUri; #[derive(Debug)] -pub struct FtClientSession<'a, SCHC> +pub struct FtClientSession<'a, FCHC> where - SCHC: FtClientHttpConnector + Send, + FCHC: FtClientHttpConnector + Send, { - pub http_session_api: FtClientHttpSessionApi<'a, SCHC>, + pub http_session_api: FtClientHttpSessionApi<'a, FCHC>, } #[derive(Debug)] @@ -58,6 +60,7 @@ pub trait FtClientHttpConnector { &'a self, full_uri: Url, token: &'a FtApiToken, + ratelimiter: &'a RateLimiter, ) -> BoxFuture<'a, ClientResult> where RS: for<'de> serde::de::Deserialize<'de> + Send + 'a; @@ -66,6 +69,7 @@ pub trait FtClientHttpConnector { &'a self, method_relative_uri: &str, token: &'a FtApiToken, + ratelimiter: &'a RateLimiter, params: &'p PT, ) -> BoxFuture<'a, ClientResult> where @@ -78,7 +82,7 @@ pub trait FtClientHttpConnector { .and_then(|url| FtClientHttpApiUri::create_url_with_params(url, params)); match full_uri { - Ok(full_uri) => self.http_get_uri(full_uri, token), + Ok(full_uri) => self.http_get_uri(full_uri, token, ratelimiter), Err(err) => std::future::ready(Err(err)).boxed(), } } @@ -173,10 +177,18 @@ where pub fn new(http_connector: FCHC) -> Self { Self { http_api: FtClientHttpApi::new(Arc::new(http_connector)), + ratelimiter: RateLimiter::new(2, 1200), + } + } + + pub fn with_ratelimits(http_connector: FCHC, secondly: u64, hourly: u64) -> Self { + Self { + http_api: FtClientHttpApi::new(Arc::new(http_connector)), + ratelimiter: RateLimiter::new(secondly, hourly), } } - pub fn open_session(&self, token: FtApiToken) -> FtClientSession { + pub fn open_session(&'_ self, token: FtApiToken) -> FtClientSession<'_, FCHC> { // TODO: Add tracer for LOGGING // let http_session_span = span!(Level::DEBUG, "Ft API request",); @@ -211,7 +223,7 @@ where self.client .http_api .connector - .http_get_uri(full_uri, &self.token) + .http_get_uri(full_uri, &self.token, &self.client.ratelimiter) .await } @@ -228,7 +240,12 @@ where self.client .http_api .connector - .http_get(method_relative_uri, &self.token, params) + .http_get( + method_relative_uri, + &self.token, + &self.client.ratelimiter, + params, + ) .await } diff --git a/libft-api/src/common/error.rs b/libft-api/src/common/error.rs index e5438fb..25c380b 100644 --- a/libft-api/src/common/error.rs +++ b/libft-api/src/common/error.rs @@ -11,11 +11,11 @@ macro_rules! enum_into { ($vis:vis $enum_ty:ident $($enum_item:ident $(,)?)*) => { #[derive(Debug)] $vis enum $enum_ty { - $($enum_item(concat_idents!(Ft,$enum_item))),* + $($enum_item(${concat(Ft,$enum_item)})),* } - $(impl From for $enum_ty { - fn from(err: concat_idents!(Ft,$enum_item)) -> Self { + $(impl From<${concat(Ft,$enum_item)}> for $enum_ty { + fn from(err: ${concat(Ft,$enum_item)}) -> Self { $enum_ty::$enum_item(err) } })* diff --git a/libft-api/src/common/ratelimiter.rs b/libft-api/src/common/ratelimiter.rs new file mode 100644 index 0000000..fcf7aaf --- /dev/null +++ b/libft-api/src/common/ratelimiter.rs @@ -0,0 +1,277 @@ +use reqwest::header::HeaderMap; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, SystemTime}; +use tokio::time::sleep; + +#[derive(Debug, Clone)] +pub struct RateLimiter { + secondly_ratelimit_limit: u64, + hourly_ratelimit_limit: u64, + secondly_ratelimit_remaining: Arc>, + hourly_ratelimit_remaining: Arc>, + + // 429 응답의 `retry-after` 헤더를 처리하기 위한 필드 + retry_after: Arc>, + // 초당 요청 제한 윈도우가 언제 리셋되는지 추적하기 위한 필드 (추가됨) + secondly_window_reset: Arc>, + hourly_window_reset: Arc>, +} + +impl RateLimiter { + pub fn new(per_second_limit: u64, hourly_limit: u64) -> Self { + let now = SystemTime::now(); + Self { + secondly_ratelimit_limit: per_second_limit, + hourly_ratelimit_limit: hourly_limit, + secondly_ratelimit_remaining: Arc::new(Mutex::new(per_second_limit)), + hourly_ratelimit_remaining: Arc::new(Mutex::new(hourly_limit)), + retry_after: Arc::new(Mutex::new(now)), + secondly_window_reset: Arc::new(Mutex::new(now + Duration::from_secs(1))), + hourly_window_reset: Arc::new(Mutex::new(now + Duration::from_secs(3600))), + } + } + + /// API 요청을 보내기 전에 호출하여 ratelimit을 준수하도록 대기합니다. + pub async fn acquire(&self) { + loop { + let now = SystemTime::now(); + + // 1. 'retry-after'에 의한 강제 대기 확인 + // 외부에서 429 응답을 받았을 때 이 값을 업데이트했다고 가정합니다. + let retry_after_time = *self.retry_after.lock().unwrap(); + if now < retry_after_time { + let wait_duration = retry_after_time.duration_since(now).unwrap_or_default(); + sleep(wait_duration).await; + continue; // 대기 후 루프의 처음부터 다시 확인 + } + + // Mutex Guard가 await 지점까지 살아남지 않도록 범위를 제한합니다. + let wait_duration = { + // 2. 초당 요청 제한 확인 (Fixed Window) + let mut remaining_guard_sec = self.secondly_ratelimit_remaining.lock().unwrap(); + let mut remaining_guard_hour = self.hourly_ratelimit_remaining.lock().unwrap(); + let mut reset_guard_sec = self.secondly_window_reset.lock().unwrap(); + let mut reset_guard_hour = self.hourly_window_reset.lock().unwrap(); + + if now >= *reset_guard_sec { + *remaining_guard_sec = self.secondly_ratelimit_limit; + *reset_guard_sec = now + Duration::from_secs(1); + } + + if now >= *reset_guard_hour { + *remaining_guard_hour = self.hourly_ratelimit_limit; + *reset_guard_hour = now + Duration::from_secs(3600); + } + + if *remaining_guard_hour > 0 { + *remaining_guard_hour -= 1; + if *remaining_guard_sec > 0 { + *remaining_guard_sec -= 1; + break; + } else { + reset_guard_sec.duration_since(now).unwrap_or_default() + } + } else { + reset_guard_hour.duration_since(now).unwrap_or_default() + } + }; + + // wait_duration이 0보다 크면 대기합니다. + if !wait_duration.is_zero() { + sleep(wait_duration).await; + } + // 대기 후, 다른 스레드가 먼저 토큰을 가져갔을 수 있으므로 loop를 다시 돕니다. + } + } + + pub fn update_from_headers(&self, headers: &HeaderMap) { + let parse_header = |name: &str| -> Option { + headers + .get(name) + .and_then(|v| v.to_str().ok()) + .and_then(|s| s.parse::().ok()) + }; + + if let Some(remaining) = parse_header("x-secondly-ratelimit-remaining") { + *self.secondly_ratelimit_remaining.lock().unwrap() = remaining; + } + + if let Some(remaining) = parse_header("x-hourly-ratelimit-remaining") { + *self.hourly_ratelimit_remaining.lock().unwrap() = remaining; + } + + if let Some(retry_seconds) = parse_header("retry-after") { + let wait_duration = Duration::from_secs(retry_seconds); + let new_retry_time = SystemTime::now() + wait_duration; + *self.retry_after.lock().unwrap() = new_retry_time; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{Duration, Instant}; + + /// 테스트 1: 제한 횟수 내에서는 대기 없이 바로 통과하는지 확인 + #[tokio::test] + async fn test_can_acquire_within_limit() { + let limiter = RateLimiter::new(5, 100); // 초당 5회 + let start = Instant::now(); + + for _ in 0..5 { + limiter.acquire().await; + } + + let elapsed = start.elapsed(); + println!("Elapsed time for 5 acquires: {:?}", elapsed); + + // 5번의 호출은 sleep 없이 즉시 처리되어야 하므로 매우 짧은 시간 안에 끝나야 합니다. + assert!(elapsed < Duration::from_millis(100)); + } + + /// 테스트 2: 초당 제한을 초과하면 1초 이상 대기하는지 확인 + #[tokio::test] + async fn test_waits_when_per_second_limit_exceeded() { + let limiter = RateLimiter::new(3, 100); // 초당 3회 + let start = Instant::now(); + + // 4번 호출 -> 3번은 즉시, 1번은 1초 뒤에 실행되어야 함 + for _ in 0..4 { + limiter.acquire().await; + } + + let elapsed = start.elapsed(); + println!( + "Elapsed time for 4 acquires with 3/sec limit: {:?}", + elapsed + ); + + // 총 소요 시간은 최소 1초 이상이어야 합니다. + assert!(elapsed >= Duration::from_secs(1)); + // 너무 오래 기다리는 것도 아니어야 합니다. (네트워크 지연 등 감안) + assert!(elapsed < Duration::from_secs(2)); + } + + /// 테스트 3: 1초가 지나면 제한이 초기화되는지 확인 + #[tokio::test] + async fn test_limit_resets_after_one_second() { + let limiter = RateLimiter::new(2, 100); // 초당 2회 + + // 1. 먼저 제한을 모두 소진 + limiter.acquire().await; + limiter.acquire().await; + + // 2. 1초보다 길게 대기하여 윈도우를 리셋 + sleep(Duration::from_millis(1100)).await; + + // 3. 리셋된 후에는 다시 즉시 통과해야 함 + let start = Instant::now(); + limiter.acquire().await; + limiter.acquire().await; + let elapsed = start.elapsed(); + + println!("Elapsed time for 2 acquires after reset: {:?}", elapsed); + + // 리셋된 후의 호출은 다시 빨라야 합니다. + assert!(elapsed < Duration::from_millis(100)); + } + + /// 테스트 4: 여러 태스크에서 동시에 접근해도 안전하게 동작하는지 확인 + #[tokio::test] + async fn test_concurrent_acquires_are_safe() { + // Arc를 통해 여러 태스크가 RateLimiter를 공유 + let limiter = Arc::new(RateLimiter::new(8, 100)); // 초당 5회 + let mut tasks = vec![]; + + let start = Instant::now(); + + // 10개의 태스크를 생성하여 동시에 acquire 호출 + for i in 0..64 { + let limiter_clone = Arc::clone(&limiter); + tasks.push(tokio::spawn(async move { + println!("Task {} acquiring...", i); + limiter_clone.acquire().await; + println!("Task {} acquired.", i); + })); + } + + // 모든 태스크가 끝날 때까지 대기 + futures::future::join_all(tasks).await; + + let elapsed = start.elapsed(); + println!( + "Elapsed time for 64 concurrent acquires with 8/sec limit: {:?}", + elapsed + ); + + assert!(elapsed >= Duration::from_secs(7)); + assert!(elapsed < Duration::from_secs(8)); + } + + use reqwest::header::HeaderValue; + /// 테스트 5: 정상 응답(200 OK) 헤더를 받았을 때 상태가 올바르게 업데이트되는지 확인 + #[test] + fn test_update_from_successful_response() { + let limiter = RateLimiter::new(2, 1200); // 초기값: 초당 2, 시간당 1200 + + // 시뮬레이션할 헤더 생성 + let mut headers = HeaderMap::new(); + headers.insert( + "x-secondly-ratelimit-remaining", + HeaderValue::from_static("0"), + ); + headers.insert( + "x-hourly-ratelimit-remaining", + HeaderValue::from_static("1194"), + ); + + // 상태 업데이트 + limiter.update_from_headers(&headers); + + // 상태 검증 + let secondly_remaining = *limiter.secondly_ratelimit_remaining.lock().unwrap(); + let hourly_remaining = *limiter.hourly_ratelimit_remaining.lock().unwrap(); + + assert_eq!( + secondly_remaining, 0, + "Secondly remaining should be updated to 0" + ); + assert_eq!( + hourly_remaining, 1194, + "Hourly remaining should be updated to 1194" + ); + } + + /// 테스트 6: 속도 제한 응답(429) 헤더를 받았을 때 retry_after가 올바르게 설정되는지 확인 + #[test] + fn test_update_from_ratelimited_response() { + let limiter = RateLimiter::new(2, 1200); + let before_update = SystemTime::now(); + + // 429 응답 시뮬레이션 헤더 + let mut headers = HeaderMap::new(); + headers.insert("retry-after", HeaderValue::from_static("1")); + + // 상태 업데이트 + limiter.update_from_headers(&headers); + + // 상태 검증 + let retry_time = *limiter.retry_after.lock().unwrap(); + + // retry_time이 업데이트 이전 시간보다 미래인지 확인 + assert!( + retry_time > before_update, + "retry_after time should be in the future" + ); + + // 대기 시간이 약 1초인지 확인 (실행 시간 오차 감안) + let wait_duration = retry_time.duration_since(before_update).unwrap(); + assert!( + wait_duration >= Duration::from_millis(900) + && wait_duration <= Duration::from_millis(1100), + "The wait duration should be approximately 1 second. Actual: {:?}", + wait_duration + ); + } +} diff --git a/libft-api/src/connector.rs b/libft-api/src/connector.rs index 5bdb76f..e510e02 100644 --- a/libft-api/src/connector.rs +++ b/libft-api/src/connector.rs @@ -11,6 +11,7 @@ use url::Url; use crate::{ map_serde_error, ClientResult, FtApiToken, FtClientError, FtClientHttpApiUri, FtClientHttpConnector, FtEnvelopeMessage, FtHttpError, FtRateLimitError, FtReqwestError, + RateLimiter, }; pub struct FtClientReqwestConnector { @@ -57,10 +58,14 @@ impl FtClientReqwestConnector { &'a self, reqwest: RequestBuilder, url: Url, + ratelimiter: Option<&'a RateLimiter>, ) -> ClientResult where RS: for<'de> serde::de::Deserialize<'de>, { + if let Some(ratelimiter) = ratelimiter { + ratelimiter.acquire().await; + } let url_str = url.to_string(); info!(ft_url = url_str, "Sending HTTP request to"); let http_res = reqwest @@ -68,19 +73,26 @@ impl FtClientReqwestConnector { .await .map_err(|error| FtReqwestError { error })?; let http_status = http_res.status(); - let http_headers = http_res.headers().clone(); + let http_headers = http_res.headers(); + if let Some(ratelimiter) = ratelimiter { + ratelimiter.update_from_headers(http_headers); + } debug!("headers: {:#?}", http_headers); let http_content_type = http_headers.get(header::CONTENT_TYPE); + let http_retry_after = http_headers + .get(header::RETRY_AFTER) + .and_then(|ra| ra.to_str().ok().and_then(|s| s.parse().ok())) + .map(Duration::from_secs); + let http_content_is_json = matches!( + http_content_type.map(|content_type| content_type.to_str()), + Some(Ok("application/json; charset=utf-8")) + ); let http_body_str = http_res .text() .await .map_err(|error| FtReqwestError { error })?; info!(ft_url = url_str, "Received HTTP response {}", http_status); - let http_content_is_json = matches!( - http_content_type.map(|content_type| content_type.to_str()), - Some(Ok("application/json; charset=utf-8")) - ); match http_status { StatusCode::OK if http_content_is_json => { @@ -102,12 +114,7 @@ impl FtClientReqwestConnector { Err(FtClientError::RateLimitError( FtRateLimitError::new() - .opt_retry_after( - http_headers - .get(header::RETRY_AFTER) - .and_then(|ra| ra.to_str().ok().and_then(|s| s.parse().ok())) - .map(Duration::from_secs), - ) + .opt_retry_after(http_retry_after) .opt_code(ft_message.error) .opt_warnings(ft_message.warnings) .with_http_response_body(http_body_str), @@ -115,12 +122,7 @@ impl FtClientReqwestConnector { } StatusCode::TOO_MANY_REQUESTS => Err(FtClientError::RateLimitError( FtRateLimitError::new() - .opt_retry_after( - http_headers - .get(header::RETRY_AFTER) - .and_then(|ra| ra.to_str().ok().and_then(|s| s.parse().ok())) - .map(Duration::from_secs), - ) + .opt_retry_after(http_retry_after) .with_http_response_body(http_body_str), )), _ => Err(FtClientError::HttpError( @@ -139,6 +141,7 @@ impl FtClientHttpConnector for FtClientReqwestConnector { &'a self, full_uri: url::Url, token: &'a FtApiToken, + ratelimiter: &'a RateLimiter, ) -> futures::prelude::future::BoxFuture<'a, ClientResult> where RS: for<'de> serde::de::Deserialize<'de> + Send + 'a, @@ -150,7 +153,8 @@ impl FtClientHttpConnector for FtClientReqwestConnector { .get(full_uri.clone()) .header(AUTHORIZATION, token.get_token_value()); - self.send_http_request(request, full_uri).await + self.send_http_request(request, full_uri, Some(ratelimiter)) + .await } .boxed() } @@ -173,7 +177,7 @@ impl FtClientHttpConnector for FtClientReqwestConnector { .header(AUTHORIZATION, token.get_token_value()) .json(&request_body); - self.send_http_request(request, full_uri).await + self.send_http_request(request, full_uri, None).await } .boxed() } @@ -196,7 +200,7 @@ impl FtClientHttpConnector for FtClientReqwestConnector { .header(AUTHORIZATION, token.get_token_value()) .json(&request_body); - self.send_http_request(request, full_uri).await + self.send_http_request(request, full_uri, None).await } .boxed() } @@ -219,7 +223,7 @@ impl FtClientHttpConnector for FtClientReqwestConnector { .header(AUTHORIZATION, token.get_token_value()) .json(&request_body); - self.send_http_request(request, full_uri).await + self.send_http_request(request, full_uri, None).await } .boxed() } diff --git a/libft-api/src/lib.rs b/libft-api/src/lib.rs index 212941e..f7d1c31 100644 --- a/libft-api/src/lib.rs +++ b/libft-api/src/lib.rs @@ -1,5 +1,5 @@ -#![warn(clippy::pedantic)] -#![feature(concat_idents)] +// #![warn(clippy::pedantic)] +#![feature(macro_metavar_expr_concat)] pub use api::*; mod api; From d769b42e4f3ec02b2088e7cc9c50d0138f39360f Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Tue, 14 Oct 2025 02:17:09 +0900 Subject: [PATCH 05/18] pagination wrapper --- Cargo.lock | 507 ++++++++++++++++++-------- libft-api-derive/Cargo.toml | 7 +- libft-api-derive/src/lib.rs | 50 ++- libft-api/Cargo.toml | 17 +- libft-api/bin/blackholed.rs | 105 ++---- libft-api/src/api.rs | 5 + libft-api/src/api/campus/campus_id.rs | 2 + libft-api/src/api/user/users.rs | 13 +- libft-api/src/common.rs | 3 + libft-api/src/common/client.rs | 23 +- libft-api/src/common/paginator.rs | 71 ++++ libft-api/src/common/ratelimiter.rs | 88 +++-- libft-api/src/connector.rs | 14 +- 13 files changed, 603 insertions(+), 302 deletions(-) create mode 100644 libft-api/src/common/paginator.rs diff --git a/Cargo.lock b/Cargo.lock index 4a3c318..bb0da8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,12 +17,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -56,7 +50,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -100,17 +94,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-targets", + "windows-link 0.2.1", ] [[package]] @@ -131,9 +124,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "darling" -version = "0.20.10" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ "darling_core", "darling_macro", @@ -141,27 +134,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.10" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.90", + "syn 2.0.106", ] [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.90", + "syn 2.0.106", ] [[package]] @@ -182,9 +175,15 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.106", ] +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -239,9 +238,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -302,7 +301,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.106", ] [[package]] @@ -431,19 +430,21 @@ checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "hyper" -version = "1.5.2" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -484,21 +485,28 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ + "base64", "bytes", "futures-channel", + "futures-core", "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -639,7 +647,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.106", ] [[package]] @@ -650,9 +658,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -691,12 +699,33 @@ dependencies = [ "serde", ] +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itoa" version = "1.0.14" @@ -705,9 +734,9 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" -version = "0.3.76" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ "once_cell", "wasm-bindgen", @@ -721,9 +750,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.168" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libft-api" @@ -732,6 +761,7 @@ dependencies = [ "chrono", "futures", "lazy_static", + "libft-api-derive", "reqwest", "rsb_derive", "rvstruct", @@ -748,6 +778,11 @@ dependencies = [ [[package]] name = "libft-api-derive" version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] [[package]] name = "linux-raw-sys" @@ -828,12 +863,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", + "windows-sys 0.60.2", ] [[package]] @@ -889,7 +923,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.106", ] [[package]] @@ -910,12 +944,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "parking_lot" version = "0.12.3" @@ -936,14 +964,14 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" @@ -971,18 +999,18 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.37" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] @@ -996,17 +1024,36 @@ dependencies = [ "bitflags", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "reqwest" -version = "0.12.9" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64", "bytes", "encoding_rs", "futures-core", - "futures-util", "h2", "http", "http-body", @@ -1015,28 +1062,26 @@ dependencies = [ "hyper-rustls", "hyper-tls", "hyper-util", - "ipnet", "js-sys", "log", "mime", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", "tokio-native-tls", + "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows-registry", ] [[package]] @@ -1097,15 +1142,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.10.1" @@ -1123,6 +1159,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "rvs_derive" version = "0.3.2" @@ -1158,6 +1200,30 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1189,34 +1255,45 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.216" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.216" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.106", ] [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -1242,17 +1319,18 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.11.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +checksum = "6093cd8c01b25262b84927e0f7151692158fab02d961e04c979d3903eba7ecc5" dependencies = [ "base64", "chrono", "hex", "indexmap 1.9.3", "indexmap 2.7.0", - "serde", - "serde_derive", + "schemars 0.9.0", + "schemars 1.0.4", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -1260,14 +1338,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.11.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +checksum = "a7e6c180db0816026a61afa1cff5344fb7ebded7e4d3062772179f2501481c27" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.106", ] [[package]] @@ -1311,12 +1389,12 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.8" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -1356,9 +1434,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.90" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -1382,7 +1460,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.106", ] [[package]] @@ -1472,32 +1550,34 @@ dependencies = [ [[package]] name = "tokio" -version = "1.42.0" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", + "slab", "socket2", "tokio-macros", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.106", ] [[package]] @@ -1533,6 +1613,45 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -1558,7 +1677,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.106", ] [[package]] @@ -1584,9 +1703,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "nu-ansi-term", "sharded-slab", @@ -1616,9 +1735,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -1667,34 +1786,36 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.99" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.99" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.49" +version = "0.4.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" dependencies = [ "cfg-if", "js-sys", @@ -1705,9 +1826,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.99" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1715,92 +1836,84 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" -version = "0.3.76" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] -name = "winapi" -version = "0.3.9" +name = "windows-core" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "windows-targets 0.52.6", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "windows-link" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets", -] +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-registry" -version = "0.2.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ + "windows-link 0.1.3", "windows-result", "windows-strings", - "windows-targets", ] [[package]] name = "windows-result" -version = "0.2.0" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-targets", + "windows-link 0.1.3", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-result", - "windows-targets", + "windows-link 0.1.3", ] [[package]] @@ -1809,7 +1922,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -1818,7 +1931,16 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -1827,14 +1949,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -1843,48 +1982,96 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "write16" version = "1.0.0" @@ -1917,7 +2104,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.106", "synstructure", ] @@ -1938,7 +2125,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.106", "synstructure", ] @@ -1967,5 +2154,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.106", ] diff --git a/libft-api-derive/Cargo.toml b/libft-api-derive/Cargo.toml index 43b5788..e5c7cac 100644 --- a/libft-api-derive/Cargo.toml +++ b/libft-api-derive/Cargo.toml @@ -3,5 +3,10 @@ name = "libft-api-derive" version = "0.1.0" edition = "2021" -[dependencies] +[lib] +proc-macro = true +[dependencies] +syn = { version = "2.0.106", features = ["full"]} +quote = "1.0.41" +proc-macro2 = "1.0.101" diff --git a/libft-api-derive/src/lib.rs b/libft-api-derive/src/lib.rs index b93cf3f..6d16c50 100644 --- a/libft-api-derive/src/lib.rs +++ b/libft-api-derive/src/lib.rs @@ -1,14 +1,44 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Data, DeriveInput, Fields, GenericArgument, PathArguments, Type}; -#[cfg(test)] -mod tests { - use super::*; +#[proc_macro_derive(HasVector)] +pub fn has_vec_derive(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let struct_name = &ast.ident; + let (field_name, vec_inner_type) = find_vec_field(&ast.data); + let gen = quote! { + impl HasVec<#vec_inner_type> for #struct_name { + fn get_vec(&self) -> &Vec<#vec_inner_type> { + &self.#field_name + } + fn take_vec(self) -> Vec<#vec_inner_type> { + self.#field_name + } + } + }; + gen.into() +} - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); +fn find_vec_field(data: &Data) -> (proc_macro2::Ident, &Type) { + if let Data::Struct(s) = data { + if let Fields::Named(fields) = &s.fields { + for field in &fields.named { + if let Type::Path(type_path) = &field.ty { + if let Some(last_segment) = type_path.path.segments.last() { + if last_segment.ident == "Vec" { + if let PathArguments::AngleBracketed(args) = &last_segment.arguments { + if let Some(GenericArgument::Type(inner_type)) = args.args.first() { + return (field.ident.clone().unwrap(), inner_type); + } + } + } + } + } + } + } } + panic!("HasVec derive macro requires a field of type Vec"); } diff --git a/libft-api/Cargo.toml b/libft-api/Cargo.toml index bc0fa1a..88de5d0 100644 --- a/libft-api/Cargo.toml +++ b/libft-api/Cargo.toml @@ -59,17 +59,18 @@ path="bin/exam_resubscribe.rs" # path="bin/location_stat.rs" [dependencies] -serde = { version = "1.0.216", features = ["derive"] } -serde_with = { version = "3.11.0", features = ["macros"] } -serde_json = { version = "1.0.133", features = ["std"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_with = { version = "3.15.0", features = ["macros"] } +serde_json = { version = "1.0.145", features = ["std"] } serde_plain = "1.0.2" -reqwest = { version = "0.12.9", features = ["json"] } +reqwest = { version = "0.12.24", features = ["json"] } rvstruct = "0.3.2" -tokio = { version = "1.42.0", features = ["full", "tracing"] } -chrono = { version = "0.4.39", features = ["serde"] } +tokio = { version = "1.47.1", features = ["full", "tracing"] } +chrono = { version = "0.4.42", features = ["serde"] } rsb_derive = "0.5.1" -url = { version = "2.5.4", features = ["serde"] } +url = { version = "2.5.7", features = ["serde"] } futures = { version = "0.3.31", features = ["alloc"] } lazy_static = "1.5.0" tracing = "0.1.41" -tracing-subscriber = "0.3.19" +tracing-subscriber = "0.3.20" +libft-api-derive = {path = "../libft-api-derive"} diff --git a/libft-api/bin/blackholed.rs b/libft-api/bin/blackholed.rs index 5f97bb6..07c122a 100644 --- a/libft-api/bin/blackholed.rs +++ b/libft-api/bin/blackholed.rs @@ -1,8 +1,9 @@ -use std::{ops::ControlFlow, sync::Arc, time::Duration}; +use std::{io::Write, sync::Arc}; +use futures::FutureExt; use libft_api::{campus_id::*, prelude::*}; -use tokio::{task::JoinSet, time::sleep}; -use tracing::{info, info_span}; +use tokio::task::JoinSet; +use tracing::info_span; #[tokio::main] async fn main() { @@ -16,73 +17,39 @@ async fn main() { let thread_num = 8; let mut handles = JoinSet::new(); - for i in 0..8 { - let client = Arc::clone(&client); - handles.spawn(async move { - let mut result = Vec::new(); - let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) - .await - .unwrap(); - let session = Arc::new(client.open_session(token)); - let mut page = i; - loop { - let page = &mut page; - if let ControlFlow::Break(()) = { - let result = &mut result; - let session_clone = Arc::clone(&session); - async move { - let res = session_clone - .users( - FtApiUsersRequest::new() - .with_page(*page) - .with_per_page(100) - .with_filter(vec![ - FtFilterOption::new( - FtFilterField::PrimaryCampusId, - vec![SEOUL.to_string()], - ), - FtFilterOption::new( - FtFilterField::Kind, - vec!["student".to_string()], - ), - ]), - ) - .await; - match res { - Ok(res) => { - if res.users.is_empty() { - return ControlFlow::Break(()); - } - result.extend(res.users); - info!("{}", result.len()); - *page += thread_num; - } - Err(FtClientError::RateLimitError(_)) => { - eprintln!("rate limit, try again."); - sleep(Duration::new(1, 42)).await - } - Err(e) => { - eprintln!("other error: {e}"); - return ControlFlow::Break(()); - } - } - ControlFlow::Continue(()) - } - } + let request_builder: ReqFn = |session, page| { + async move { + session + .users( + FtApiUsersRequest::new() + .with_page(page) + .with_per_page(100) + .with_filter(vec![ + FtFilterOption::new( + FtFilterField::PrimaryCampusId, + vec![SEOUL.to_string()], + ), + FtFilterOption::new(FtFilterField::Kind, vec!["student".to_string()]), + ]), + ) .await - { - break result - .into_iter() - .filter_map(|user| user.id) - .collect::>(); - } - } - }); - } + } + .boxed() + }; - // reserve size from X-Total - let mut result = Vec::new(); - while let Some(Ok(data)) = handles.join_next().await { - result.extend(data); + for i in 1..=thread_num { + let client = Arc::clone(&client); + handles.spawn(async move { scroller(&client, thread_num, i, request_builder).await }); + } + let mut all = Vec::::new(); + while let Some(res) = handles.join_next().await { + match res { + Ok(v) => all.extend(v), + Err(e) => tracing::error!("task failed: {e}"), + } } + + let mut file = std::fs::File::create("campus_users.json").unwrap(); + file.write_all(serde_json::to_string_pretty(&all).unwrap().as_bytes()) + .unwrap(); } diff --git a/libft-api/src/api.rs b/libft-api/src/api.rs index 7f07168..9644f8b 100644 --- a/libft-api/src/api.rs +++ b/libft-api/src/api.rs @@ -9,3 +9,8 @@ mod scale_team; mod user; pub mod prelude; + +pub trait HasVec { + fn get_vec(&self) -> &Vec; + fn take_vec(self) -> Vec; +} diff --git a/libft-api/src/api/campus/campus_id.rs b/libft-api/src/api/campus/campus_id.rs index e28341e..9730741 100644 --- a/libft-api/src/api/campus/campus_id.rs +++ b/libft-api/src/api/campus/campus_id.rs @@ -1,3 +1,5 @@ +use crate::HasVec; +use libft_api_derive::HasVector; use rsb_derive::Builder; use serde::{Deserialize, Serialize}; diff --git a/libft-api/src/api/user/users.rs b/libft-api/src/api/user/users.rs index 4df58f9..54b5afa 100644 --- a/libft-api/src/api/user/users.rs +++ b/libft-api/src/api/user/users.rs @@ -1,7 +1,7 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{prelude::*, to_param}; +use crate::{api::HasVec, prelude::*, to_param}; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiUsersPostRequest { @@ -26,7 +26,7 @@ pub struct FtApiUsersRequest { pub sort: Option>, pub range: Option>, pub filter: Option>, - pub page: Option, + pub page: Option, pub per_page: Option, } @@ -42,6 +42,15 @@ pub struct FtApiUsersResponse { pub users: Vec, } +impl HasVec for FtApiUsersResponse { + fn get_vec(&self) -> &Vec { + &self.users + } + fn take_vec(self) -> Vec { + self.users + } +} + impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, diff --git a/libft-api/src/common.rs b/libft-api/src/common.rs index 90dad11..f07d143 100644 --- a/libft-api/src/common.rs +++ b/libft-api/src/common.rs @@ -9,3 +9,6 @@ mod param; pub use ratelimiter::*; mod ratelimiter; + +pub use paginator::*; +mod paginator; diff --git a/libft-api/src/common/client.rs b/libft-api/src/common/client.rs index 3cd9657..c6ccd4b 100644 --- a/libft-api/src/common/client.rs +++ b/libft-api/src/common/client.rs @@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; use url::Url; -use crate::common::ratelimiter::RateLimiter; -use crate::{FtApiToken, FtClientError, FtClientReqwestConnector}; +use crate::common::ratelimiter::HeaderMetaData; +use crate::{FtApiToken, FtClientError, FtClientReqwestConnector, RateLimiter}; pub type ClientResult = std::result::Result; @@ -17,7 +17,7 @@ where FCHC: FtClientHttpConnector + Send, { pub http_api: FtClientHttpApi, - pub ratelimiter: RateLimiter, + pub meta: HeaderMetaData, } #[derive(Clone, Debug)] @@ -60,7 +60,7 @@ pub trait FtClientHttpConnector { &'a self, full_uri: Url, token: &'a FtApiToken, - ratelimiter: &'a RateLimiter, + ratelimiter: &'a HeaderMetaData, ) -> BoxFuture<'a, ClientResult> where RS: for<'de> serde::de::Deserialize<'de> + Send + 'a; @@ -69,7 +69,7 @@ pub trait FtClientHttpConnector { &'a self, method_relative_uri: &str, token: &'a FtApiToken, - ratelimiter: &'a RateLimiter, + ratelimiter: &'a HeaderMetaData, params: &'p PT, ) -> BoxFuture<'a, ClientResult> where @@ -177,14 +177,14 @@ where pub fn new(http_connector: FCHC) -> Self { Self { http_api: FtClientHttpApi::new(Arc::new(http_connector)), - ratelimiter: RateLimiter::new(2, 1200), + meta: HeaderMetaData::new(RateLimiter::new(2, 1200)), } } pub fn with_ratelimits(http_connector: FCHC, secondly: u64, hourly: u64) -> Self { Self { http_api: FtClientHttpApi::new(Arc::new(http_connector)), - ratelimiter: RateLimiter::new(secondly, hourly), + meta: HeaderMetaData::new(RateLimiter::new(secondly, hourly)), } } @@ -223,7 +223,7 @@ where self.client .http_api .connector - .http_get_uri(full_uri, &self.token, &self.client.ratelimiter) + .http_get_uri(full_uri, &self.token, &self.client.meta) .await } @@ -240,12 +240,7 @@ where self.client .http_api .connector - .http_get( - method_relative_uri, - &self.token, - &self.client.ratelimiter, - params, - ) + .http_get(method_relative_uri, &self.token, &self.client.meta, params) .await } diff --git a/libft-api/src/common/paginator.rs b/libft-api/src/common/paginator.rs new file mode 100644 index 0000000..263f6d2 --- /dev/null +++ b/libft-api/src/common/paginator.rs @@ -0,0 +1,71 @@ +use std::{ops::ControlFlow, sync::Arc, time::Duration}; + +use crate::prelude::*; +use crate::HasVec; +use futures::future::BoxFuture; +use tokio::time::sleep; + +pub type ReqFn = for<'a> fn( + Arc>, + usize, +) -> BoxFuture<'a, ClientResult>; + +pub async fn scroller<'a, T, RS, RQ>( + client: &'a FtClient, + thread_num: usize, + initial_page: usize, + request_builder: RQ, +) -> Vec +where + RS: for<'de> serde::de::Deserialize<'de> + HasVec, + RQ: Fn( + Arc>, + usize, + ) -> BoxFuture<'a, ClientResult>, +{ + let mut result = Vec::new(); + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) + .await + .unwrap(); + let session = Arc::new(client.open_session(token)); + // let total_page = *client.meta.total_page.lock().unwrap(); + let request = Arc::new(request_builder); + + let mut page = initial_page; + loop { + let page = &mut page; + let request = Arc::clone(&request); + if let ControlFlow::Break(()) = { + let result = &mut result; + let session_clone = Arc::clone(&session); + async move { + let res = request(session_clone, *page).await; + match res { + Ok(res) => { + if *client.meta.total_page.lock().unwrap() as usize <= *page + || res.get_vec().is_empty() + { + return ControlFlow::Break(()); + } + + result.extend(res.take_vec()); + *page += thread_num; + } + Err(FtClientError::RateLimitError(_)) => { + tracing::warn!("rate limit, try again."); + sleep(Duration::new(1, 42)).await + } + Err(e) => { + eprintln!("other error: {e}"); + return ControlFlow::Break(()); + } + } + ControlFlow::Continue(()) + } + } + .await + { + break result; + } + } +} diff --git a/libft-api/src/common/ratelimiter.rs b/libft-api/src/common/ratelimiter.rs index fcf7aaf..8a951d4 100644 --- a/libft-api/src/common/ratelimiter.rs +++ b/libft-api/src/common/ratelimiter.rs @@ -3,9 +3,54 @@ use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; use tokio::time::sleep; +#[derive(Debug, Clone)] +pub struct HeaderMetaData { + pub ratelimiter: RateLimiter, + pub total_page: Arc>, +} + +impl HeaderMetaData { + pub fn new(ratelimiter: RateLimiter) -> Self { + Self { + ratelimiter, + total_page: Arc::new(Mutex::new(0)), + } + } + + pub fn update_from_headers(&self, headers: &HeaderMap) { + let parse_header = |name: &str| -> Option { + headers + .get(name) + .and_then(|v| v.to_str().ok()) + .and_then(|s| s.parse::().ok()) + }; + + if let Some(total) = parse_header("x-total") { + *self.total_page.lock().unwrap() = total; + } + if let Some(remaining) = parse_header("x-secondly-ratelimit-remaining") { + *self + .ratelimiter + .secondly_ratelimit_remaining + .lock() + .unwrap() = remaining; + } + + if let Some(remaining) = parse_header("x-hourly-ratelimit-remaining") { + *self.ratelimiter.hourly_ratelimit_remaining.lock().unwrap() = remaining; + } + + if let Some(retry_seconds) = parse_header("retry-after") { + let wait_duration = Duration::from_secs(retry_seconds); + let new_retry_time = SystemTime::now() + wait_duration; + *self.ratelimiter.retry_after.lock().unwrap() = new_retry_time; + } + } +} + #[derive(Debug, Clone)] pub struct RateLimiter { - secondly_ratelimit_limit: u64, + pub secondly_ratelimit_limit: u64, hourly_ratelimit_limit: u64, secondly_ratelimit_remaining: Arc>, hourly_ratelimit_remaining: Arc>, @@ -83,29 +128,6 @@ impl RateLimiter { // 대기 후, 다른 스레드가 먼저 토큰을 가져갔을 수 있으므로 loop를 다시 돕니다. } } - - pub fn update_from_headers(&self, headers: &HeaderMap) { - let parse_header = |name: &str| -> Option { - headers - .get(name) - .and_then(|v| v.to_str().ok()) - .and_then(|s| s.parse::().ok()) - }; - - if let Some(remaining) = parse_header("x-secondly-ratelimit-remaining") { - *self.secondly_ratelimit_remaining.lock().unwrap() = remaining; - } - - if let Some(remaining) = parse_header("x-hourly-ratelimit-remaining") { - *self.hourly_ratelimit_remaining.lock().unwrap() = remaining; - } - - if let Some(retry_seconds) = parse_header("retry-after") { - let wait_duration = Duration::from_secs(retry_seconds); - let new_retry_time = SystemTime::now() + wait_duration; - *self.retry_after.lock().unwrap() = new_retry_time; - } - } } #[cfg(test)] @@ -213,7 +235,7 @@ mod tests { /// 테스트 5: 정상 응답(200 OK) 헤더를 받았을 때 상태가 올바르게 업데이트되는지 확인 #[test] fn test_update_from_successful_response() { - let limiter = RateLimiter::new(2, 1200); // 초기값: 초당 2, 시간당 1200 + let meta = HeaderMetaData::new(RateLimiter::new(2, 1200)); // 시뮬레이션할 헤더 생성 let mut headers = HeaderMap::new(); @@ -227,11 +249,15 @@ mod tests { ); // 상태 업데이트 - limiter.update_from_headers(&headers); + meta.update_from_headers(&headers); // 상태 검증 - let secondly_remaining = *limiter.secondly_ratelimit_remaining.lock().unwrap(); - let hourly_remaining = *limiter.hourly_ratelimit_remaining.lock().unwrap(); + let secondly_remaining = *meta + .ratelimiter + .secondly_ratelimit_remaining + .lock() + .unwrap(); + let hourly_remaining = *meta.ratelimiter.hourly_ratelimit_remaining.lock().unwrap(); assert_eq!( secondly_remaining, 0, @@ -246,7 +272,7 @@ mod tests { /// 테스트 6: 속도 제한 응답(429) 헤더를 받았을 때 retry_after가 올바르게 설정되는지 확인 #[test] fn test_update_from_ratelimited_response() { - let limiter = RateLimiter::new(2, 1200); + let meta = HeaderMetaData::new(RateLimiter::new(2, 1200)); let before_update = SystemTime::now(); // 429 응답 시뮬레이션 헤더 @@ -254,10 +280,10 @@ mod tests { headers.insert("retry-after", HeaderValue::from_static("1")); // 상태 업데이트 - limiter.update_from_headers(&headers); + meta.update_from_headers(&headers); // 상태 검증 - let retry_time = *limiter.retry_after.lock().unwrap(); + let retry_time = *meta.ratelimiter.retry_after.lock().unwrap(); // retry_time이 업데이트 이전 시간보다 미래인지 확인 assert!( diff --git a/libft-api/src/connector.rs b/libft-api/src/connector.rs index e510e02..a3cba86 100644 --- a/libft-api/src/connector.rs +++ b/libft-api/src/connector.rs @@ -11,7 +11,7 @@ use url::Url; use crate::{ map_serde_error, ClientResult, FtApiToken, FtClientError, FtClientHttpApiUri, FtClientHttpConnector, FtEnvelopeMessage, FtHttpError, FtRateLimitError, FtReqwestError, - RateLimiter, + HeaderMetaData, }; pub struct FtClientReqwestConnector { @@ -58,13 +58,13 @@ impl FtClientReqwestConnector { &'a self, reqwest: RequestBuilder, url: Url, - ratelimiter: Option<&'a RateLimiter>, + meta: Option<&'a HeaderMetaData>, ) -> ClientResult where RS: for<'de> serde::de::Deserialize<'de>, { - if let Some(ratelimiter) = ratelimiter { - ratelimiter.acquire().await; + if let Some(meta) = meta { + meta.ratelimiter.acquire().await; } let url_str = url.to_string(); info!(ft_url = url_str, "Sending HTTP request to"); @@ -74,8 +74,8 @@ impl FtClientReqwestConnector { .map_err(|error| FtReqwestError { error })?; let http_status = http_res.status(); let http_headers = http_res.headers(); - if let Some(ratelimiter) = ratelimiter { - ratelimiter.update_from_headers(http_headers); + if let Some(meta) = meta { + meta.update_from_headers(http_headers); } debug!("headers: {:#?}", http_headers); let http_content_type = http_headers.get(header::CONTENT_TYPE); @@ -141,7 +141,7 @@ impl FtClientHttpConnector for FtClientReqwestConnector { &'a self, full_uri: url::Url, token: &'a FtApiToken, - ratelimiter: &'a RateLimiter, + ratelimiter: &'a HeaderMetaData, ) -> futures::prelude::future::BoxFuture<'a, ClientResult> where RS: for<'de> serde::de::Deserialize<'de> + Send + 'a, From db4b19a21a905bde0b4027e847981111104525a4 Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Tue, 14 Oct 2025 09:52:30 +0900 Subject: [PATCH 06/18] refactor(derive): improve error handling for HasVector macro Replaced `panic!` with `syn::Error` in the `HasVector` procedural macro to provide more descriptive and well-located compile-time errors. The macro will now emit specific errors if the derive is used on non-struct types, structs without named fields, or structs that do not contain exactly one `Vec` field. This improves the developer experience by providing clear feedback instead of a macro panic. --- libft-api-derive/src/lib.rs | 89 +++++++++++++++++++++++++++---------- 1 file changed, 66 insertions(+), 23 deletions(-) diff --git a/libft-api-derive/src/lib.rs b/libft-api-derive/src/lib.rs index 6d16c50..ad5a25d 100644 --- a/libft-api-derive/src/lib.rs +++ b/libft-api-derive/src/lib.rs @@ -2,43 +2,86 @@ extern crate proc_macro; use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, Data, DeriveInput, Fields, GenericArgument, PathArguments, Type}; +use syn::{ + parse_macro_input, spanned::Spanned, Data, DeriveInput, Error, Fields, GenericArgument, + PathArguments, Type, +}; #[proc_macro_derive(HasVector)] pub fn has_vec_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); + expand_has_vec(ast) + .unwrap_or_else(|e| e.to_compile_error()) + .into() +} + +fn expand_has_vec(ast: DeriveInput) -> Result { let struct_name = &ast.ident; - let (field_name, vec_inner_type) = find_vec_field(&ast.data); - let gen = quote! { - impl HasVec<#vec_inner_type> for #struct_name { - fn get_vec(&self) -> &Vec<#vec_inner_type> { - &self.#field_name + + let field = match &ast.data { + Data::Struct(s) => match &s.fields { + Fields::Named(named) => { + named.named.iter().find(|f| is_vec(&f.ty)).ok_or_else(|| { + Error::new( + s.fields.span(), + "HasVector requires exactly one named field of type Vec", + ) + })? } - fn take_vec(self) -> Vec<#vec_inner_type> { - self.#field_name + _ => { + return Err(Error::new( + s.fields.span(), + "HasVector currently supports only named-field structs", + )) } + }, + _ => { + return Err(Error::new( + ast.span(), + "HasVector can only be derived for structs", + )) } }; - gen.into() + + let field_ident = field + .ident + .clone() + .ok_or_else(|| Error::new(field.span(), "expected a named field"))?; + + let inner_ty = extract_vec_inner_ty(&field.ty).ok_or_else(|| { + Error::new( + field.ty.span(), + "field must be exactly Vec (no aliases or refs)", + ) + })?; + + Ok(quote! { + impl HasVec<#inner_ty> for #struct_name { + fn get_vec(&self) -> &Vec<#inner_ty> { &self.#field_ident } + fn take_vec(self) -> Vec<#inner_ty> { self.#field_ident } + } + }) +} + +fn is_vec(ty: &Type) -> bool { + matches!( + ty, + Type::Path(tp) + if tp.path.segments.last().map(|s| s.ident == "Vec").unwrap_or(false) + ) } -fn find_vec_field(data: &Data) -> (proc_macro2::Ident, &Type) { - if let Data::Struct(s) = data { - if let Fields::Named(fields) = &s.fields { - for field in &fields.named { - if let Type::Path(type_path) = &field.ty { - if let Some(last_segment) = type_path.path.segments.last() { - if last_segment.ident == "Vec" { - if let PathArguments::AngleBracketed(args) = &last_segment.arguments { - if let Some(GenericArgument::Type(inner_type)) = args.args.first() { - return (field.ident.clone().unwrap(), inner_type); - } - } - } +fn extract_vec_inner_ty(ty: &Type) -> Option { + if let Type::Path(tp) = ty { + if let Some(seg) = tp.path.segments.last() { + if seg.ident == "Vec" { + if let PathArguments::AngleBracketed(args) = &seg.arguments { + if let Some(GenericArgument::Type(inner)) = args.args.first() { + return Some(inner.clone()); } } } } } - panic!("HasVec derive macro requires a field of type Vec"); + None } From 91d3d614a728feb3e3775b10b0b92de73555eb04 Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Tue, 14 Oct 2025 09:53:07 +0900 Subject: [PATCH 07/18] refactor(api)!: introduce HasVector derive to reduce boilerplate This commit introduces a `HasVector` derive macro to automatically implement the `HasVec` trait for API response structs. This significantly reduces boilerplate code by removing the need for manual implementations across numerous response types. Key changes include: - Creation and application of the `HasVector` derive macro. - Removal of manual `HasVec` trait implementations. - Consolidation of common imports into a `prelude` module for cleaner code. - Introduction of a `QueryParam` type alias to simplify function signatures in `common/param.rs`. BREAKING CHANGE: The `AccessTokenScope` enum has been removed from the public API as it was unused. --- libft-api/bin/campus_users.rs | 1 - libft-api/bin/teams.rs | 2 +- libft-api/src/api/campus/campus_id.rs | 10 ++-------- libft-api/src/api/campus/campus_id_journals.rs | 9 +++------ libft-api/src/api/campus/campus_id_locations.rs | 10 +++------- libft-api/src/api/campus/campus_id_users.rs | 10 +++------- libft-api/src/api/cursus/cursus_id_projects.rs | 9 +++------ libft-api/src/api/exam/exams.rs | 9 +++------ libft-api/src/api/group/groups.rs | 7 +++---- libft-api/src/api/project/projects.rs | 9 +++------ .../project_sessions_id_scale_teams.rs | 8 +++----- .../project_session/project_sessions_id_teams.rs | 9 +++------ libft-api/src/api/project_user/projects_users.rs | 6 +++--- libft-api/src/api/scale_team/scale_teams.rs | 11 ++++++----- libft-api/src/api/user/users.rs | 14 +++----------- .../user/users_id_correction_point_historics.rs | 9 +++------ .../src/api/user/users_id_correction_points_add.rs | 2 +- libft-api/src/api/user/users_id_cursus_users.rs | 5 +++-- libft-api/src/api/user/users_id_locations.rs | 5 +++-- libft-api/src/api/user/users_id_locations_stats.rs | 3 +-- libft-api/src/api/user/users_id_projects_users.rs | 12 ++++-------- libft-api/src/api/user/users_id_teams.rs | 11 ++++------- libft-api/src/auth.rs | 11 ----------- libft-api/src/common/param.rs | 14 +++++--------- 24 files changed, 66 insertions(+), 130 deletions(-) diff --git a/libft-api/bin/campus_users.rs b/libft-api/bin/campus_users.rs index 55c66c5..a2f0ba4 100644 --- a/libft-api/bin/campus_users.rs +++ b/libft-api/bin/campus_users.rs @@ -1,6 +1,5 @@ use chrono::Utc; use libft_api::prelude::*; -use rvstruct::ValueStruct; use std::{io::Write, ops::ControlFlow, sync::Arc, time::Duration}; use tokio::{sync::Semaphore, task::JoinSet, time::sleep}; use tracing::info; diff --git a/libft-api/bin/teams.rs b/libft-api/bin/teams.rs index 008d8a4..e6f1d8b 100644 --- a/libft-api/bin/teams.rs +++ b/libft-api/bin/teams.rs @@ -1,4 +1,4 @@ -use std::{io::Write, sync::Arc}; +use std::sync::Arc; use chrono::{TimeDelta, TimeZone, Utc}; use ft_project_session_ids::c_piscine::C_PISCINE_RUSH_02; diff --git a/libft-api/src/api/campus/campus_id.rs b/libft-api/src/api/campus/campus_id.rs index 9730741..0bb9292 100644 --- a/libft-api/src/api/campus/campus_id.rs +++ b/libft-api/src/api/campus/campus_id.rs @@ -1,14 +1,8 @@ -use crate::HasVec; +use crate::{prelude::*, to_param, HasVec}; use libft_api_derive::HasVector; use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{ - convert_filter_option_to_tuple, convert_range_option_to_tuple, to_param, ClientResult, - FtCampus, FtCampusId, FtClientHttpConnector, FtClientSession, FtFilterOption, FtRangeOption, - FtSortOption, -}; - #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiCampusIdRequest { pub campus_id: Option, @@ -19,7 +13,7 @@ pub struct FtApiCampusIdRequest { pub per_page: Option, } -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiCampusIdResponse { pub campus: Vec, diff --git a/libft-api/src/api/campus/campus_id_journals.rs b/libft-api/src/api/campus/campus_id_journals.rs index 202164a..0b9ec88 100644 --- a/libft-api/src/api/campus/campus_id_journals.rs +++ b/libft-api/src/api/campus/campus_id_journals.rs @@ -2,11 +2,8 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; use tracing::debug; -use crate::{ - convert_filter_option_to_tuple, convert_range_option_to_tuple, to_param, ClientResult, - FtCampusId, FtClientHttpConnector, FtClientSession, FtFilterOption, FtJournal, FtRangeOption, - FtSortOption, FtUserId, -}; +use crate::{prelude::*, to_param, HasVec}; +use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiCampusIdJournalsRequest { @@ -21,7 +18,7 @@ pub struct FtApiCampusIdJournalsRequest { pub per_page: Option, } -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiCampusIdJournalsResponse { pub journals: Vec, diff --git a/libft-api/src/api/campus/campus_id_locations.rs b/libft-api/src/api/campus/campus_id_locations.rs index eb85398..77e9425 100644 --- a/libft-api/src/api/campus/campus_id_locations.rs +++ b/libft-api/src/api/campus/campus_id_locations.rs @@ -1,13 +1,9 @@ +use crate::{prelude::*, to_param, HasVec}; +use libft_api_derive::HasVector; use rsb_derive::Builder; use serde::{Deserialize, Serialize}; use tracing::debug; -use crate::{ - convert_filter_option_to_tuple, convert_range_option_to_tuple, to_param, ClientResult, - FtCampusId, FtClientHttpConnector, FtClientSession, FtFilterOption, FtLocation, FtRangeOption, - FtSortOption, FtUserId, -}; - #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiCampusIdLocationsRequest { pub user_id: Option, @@ -19,7 +15,7 @@ pub struct FtApiCampusIdLocationsRequest { pub per_page: Option, } -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiCampusIdLocationsResponse { pub location: Vec, diff --git a/libft-api/src/api/campus/campus_id_users.rs b/libft-api/src/api/campus/campus_id_users.rs index 85499d1..070b885 100644 --- a/libft-api/src/api/campus/campus_id_users.rs +++ b/libft-api/src/api/campus/campus_id_users.rs @@ -1,12 +1,8 @@ +use crate::{prelude::*, to_param, HasVec}; +use libft_api_derive::HasVector; use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{ - convert_filter_option_to_tuple, convert_range_option_to_tuple, to_param, ClientResult, - FtCampusId, FtClientHttpConnector, FtClientSession, FtFilterOption, FtRangeOption, - FtSortOption, FtUser, FtUserId, -}; - #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiCampusIdUsersRequest { pub campus_id: FtCampusId, @@ -18,7 +14,7 @@ pub struct FtApiCampusIdUsersRequest { pub per_page: Option, } -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiCampusIdUsersResponse { pub users: Vec, diff --git a/libft-api/src/api/cursus/cursus_id_projects.rs b/libft-api/src/api/cursus/cursus_id_projects.rs index 10c7273..a4f30b3 100644 --- a/libft-api/src/api/cursus/cursus_id_projects.rs +++ b/libft-api/src/api/cursus/cursus_id_projects.rs @@ -1,11 +1,8 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{ - convert_filter_option_to_tuple, convert_range_option_to_tuple, to_param, ClientResult, - FtClientHttpConnector, FtClientSession, FtCursusId, FtFilterOption, FtProject, FtProjectId, - FtRangeOption, FtSortOption, -}; +use crate::{prelude::*, to_param, HasVec}; +use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiCursusIdProjectsRequest { @@ -18,7 +15,7 @@ pub struct FtApiCursusIdProjectsRequest { pub per_page: Option, } -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiCursusIdProjectsResponse { pub projects: Vec, diff --git a/libft-api/src/api/exam/exams.rs b/libft-api/src/api/exam/exams.rs index 69a0b9d..d4cc573 100644 --- a/libft-api/src/api/exam/exams.rs +++ b/libft-api/src/api/exam/exams.rs @@ -1,11 +1,8 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{ - convert_filter_option_to_tuple, convert_range_option_to_tuple, to_param, ClientResult, - FtClientHttpConnector, FtClientSession, FtExam, FtExamId, FtExamUser, FtFilterOption, - FtRangeOption, FtSortOption, FtUserId, -}; +use crate::{prelude::*, to_param, HasVec}; +use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiExamsRequest { @@ -26,7 +23,7 @@ pub struct FtApiExamsUsersPostBody { pub user_id: FtUserId, } -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiExamsResponse { pub exams: Vec, diff --git a/libft-api/src/api/group/groups.rs b/libft-api/src/api/group/groups.rs index d5c69ea..8c18e86 100644 --- a/libft-api/src/api/group/groups.rs +++ b/libft-api/src/api/group/groups.rs @@ -1,9 +1,8 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{ - to_param, ClientResult, FtClientHttpConnector, FtClientSession, FtGroup, FtGroupId, FtUserId, -}; +use crate::{prelude::*, to_param, HasVec}; +use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiGroupsRequest { @@ -30,7 +29,7 @@ pub struct FtApiGroupsUsersPostResponse { pub group: FtGroup, } -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiGroupsResponse { pub groups: Vec, diff --git a/libft-api/src/api/project/projects.rs b/libft-api/src/api/project/projects.rs index 3f9b5da..f83a181 100644 --- a/libft-api/src/api/project/projects.rs +++ b/libft-api/src/api/project/projects.rs @@ -1,11 +1,8 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{ - convert_filter_option_to_tuple, convert_range_option_to_tuple, to_param, ClientResult, - FtClientHttpConnector, FtClientSession, FtCursusId, FtFilterOption, FtProject, FtProjectId, - FtRangeOption, FtSortOption, -}; +use crate::{prelude::*, to_param, HasVec}; +use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiProjectRequest { @@ -18,7 +15,7 @@ pub struct FtApiProjectRequest { pub per_page: Option, } -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiProjectResponse { pub projects: Vec, diff --git a/libft-api/src/api/project_session/project_sessions_id_scale_teams.rs b/libft-api/src/api/project_session/project_sessions_id_scale_teams.rs index ebf83b0..0ebd984 100644 --- a/libft-api/src/api/project_session/project_sessions_id_scale_teams.rs +++ b/libft-api/src/api/project_session/project_sessions_id_scale_teams.rs @@ -1,11 +1,9 @@ +use crate::{prelude::*, HasVec}; +use libft_api_derive::HasVector; use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{ - ClientResult, FtClientHttpConnector, FtClientSession, FtProjectSessionId, FtScaleTeam, -}; - -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiProjectSessionsScaleTeamsResponse { pub scale_teams: Vec, diff --git a/libft-api/src/api/project_session/project_sessions_id_teams.rs b/libft-api/src/api/project_session/project_sessions_id_teams.rs index 30070ce..e588b1c 100644 --- a/libft-api/src/api/project_session/project_sessions_id_teams.rs +++ b/libft-api/src/api/project_session/project_sessions_id_teams.rs @@ -1,13 +1,10 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{ - convert_filter_option_to_tuple, convert_range_option_to_tuple, to_param, ClientResult, - FtClientHttpConnector, FtClientSession, FtFilterOption, FtProjectSessionId, FtRangeOption, - FtSortOption, FtTeam, -}; +use crate::{prelude::*, to_param, HasVec}; +use libft_api_derive::HasVector; -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiProjectSessionsTeamsResponse { pub teams: Vec, diff --git a/libft-api/src/api/project_user/projects_users.rs b/libft-api/src/api/project_user/projects_users.rs index 0547f48..9970954 100644 --- a/libft-api/src/api/project_user/projects_users.rs +++ b/libft-api/src/api/project_user/projects_users.rs @@ -1,8 +1,8 @@ +use crate::{prelude::*, to_param, HasVec}; +use libft_api_derive::HasVector; use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{prelude::*, to_param}; - #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiProjectsUsersPostRequest { pub projects_user: FtApiProjectsUsersPostBody, @@ -30,7 +30,7 @@ pub struct FtApiProjectsUsersRequest { pub per_page: Option, } -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiProjectsUsersResponse { pub projects_users: Vec, diff --git a/libft-api/src/api/scale_team/scale_teams.rs b/libft-api/src/api/scale_team/scale_teams.rs index bf5cb89..3cce4fa 100644 --- a/libft-api/src/api/scale_team/scale_teams.rs +++ b/libft-api/src/api/scale_team/scale_teams.rs @@ -1,7 +1,8 @@ +use crate::{prelude::*, to_param, HasVec}; +use libft_api_derive::HasVector; use rsb_derive::Builder; -use serde::{Deserialize, Serialize}; -use crate::{prelude::*, to_param}; +use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiScaleTeamsRequest { @@ -12,7 +13,7 @@ pub struct FtApiScaleTeamsRequest { pub per_page: Option, } -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiScaleTeamsResponse { pub scale_teams: Vec, @@ -29,7 +30,7 @@ pub struct FtApiScaleTeamsMultipleCreateBody { pub team_id: FtTeamId, } -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiScaleTeamsMultipleCreateResponse { pub scale_teams: Vec, @@ -103,7 +104,7 @@ mod tests { )); let session = client.open_session(token); - let res = session + let _ = session .scale_teams(FtApiScaleTeamsRequest::new().with_filter(vec![ FtFilterOption::new(FtFilterField::CampusId, vec![GYEONGSAN.to_string()]), FtFilterOption::new( diff --git a/libft-api/src/api/user/users.rs b/libft-api/src/api/user/users.rs index 54b5afa..f63a657 100644 --- a/libft-api/src/api/user/users.rs +++ b/libft-api/src/api/user/users.rs @@ -1,7 +1,8 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{api::HasVec, prelude::*, to_param}; +use crate::{prelude::*, to_param, HasVec}; +use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiUsersPostRequest { @@ -36,21 +37,12 @@ pub struct FtApiUserPostsResponse { pub user: FtUser, } -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiUsersResponse { pub users: Vec, } -impl HasVec for FtApiUsersResponse { - fn get_vec(&self) -> &Vec { - &self.users - } - fn take_vec(self) -> Vec { - self.users - } -} - impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, diff --git a/libft-api/src/api/user/users_id_correction_point_historics.rs b/libft-api/src/api/user/users_id_correction_point_historics.rs index 9d16cd7..5687db9 100644 --- a/libft-api/src/api/user/users_id_correction_point_historics.rs +++ b/libft-api/src/api/user/users_id_correction_point_historics.rs @@ -1,11 +1,8 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{ - convert_filter_option_to_tuple, convert_range_option_to_tuple, to_param, ClientResult, - FtClientHttpConnector, FtClientSession, FtCorrectionPointHistory, FtFilterOption, - FtRangeOption, FtSortOption, FtUserId, -}; +use crate::{prelude::*, to_param, HasVec}; +use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiUsersIdCorrectionPointHistoricsRequest { @@ -17,7 +14,7 @@ pub struct FtApiUsersIdCorrectionPointHistoricsRequest { pub per_page: Option, } -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiUsersIdCorrectionPointHistoricsResponse { pub historics: Vec, diff --git a/libft-api/src/api/user/users_id_correction_points_add.rs b/libft-api/src/api/user/users_id_correction_points_add.rs index b3e62c2..6fc00a2 100644 --- a/libft-api/src/api/user/users_id_correction_points_add.rs +++ b/libft-api/src/api/user/users_id_correction_points_add.rs @@ -2,7 +2,7 @@ use rsb_derive::Builder; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; -use crate::{ClientResult, FtClientHttpConnector, FtClientSession, FtUser, FtUserId}; +use crate::prelude::*; #[derive(Debug, Serialize, Deserialize, Builder)] #[serde(transparent)] diff --git a/libft-api/src/api/user/users_id_cursus_users.rs b/libft-api/src/api/user/users_id_cursus_users.rs index e7a4453..58e1158 100644 --- a/libft-api/src/api/user/users_id_cursus_users.rs +++ b/libft-api/src/api/user/users_id_cursus_users.rs @@ -1,7 +1,8 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{prelude::*, to_param}; +use crate::{prelude::*, to_param, HasVec}; +use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiUsersIdCursusUsersRequest { @@ -26,7 +27,7 @@ pub struct FtApiCursusUsersBody { pub has_coalition: bool, } -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiUsersIdCursusUsersResponse { pub cursus_user: Vec, diff --git a/libft-api/src/api/user/users_id_locations.rs b/libft-api/src/api/user/users_id_locations.rs index 632024d..0eb2fb4 100644 --- a/libft-api/src/api/user/users_id_locations.rs +++ b/libft-api/src/api/user/users_id_locations.rs @@ -1,7 +1,8 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{prelude::*, to_param}; +use crate::{prelude::*, to_param, HasVec}; +use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiUsersIdLocationsRequest { @@ -13,7 +14,7 @@ pub struct FtApiUsersIdLocationsRequest { pub per_page: Option, } -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiUsersIdLocationsResponse { pub locations: Vec, diff --git a/libft-api/src/api/user/users_id_locations_stats.rs b/libft-api/src/api/user/users_id_locations_stats.rs index d12bb5c..aaa9fc1 100644 --- a/libft-api/src/api/user/users_id_locations_stats.rs +++ b/libft-api/src/api/user/users_id_locations_stats.rs @@ -5,8 +5,7 @@ use chrono::NaiveDate; use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::to_param; -use crate::{ClientResult, FtClientHttpConnector, FtClientSession, FtUserId}; +use crate::{prelude::*, to_param}; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiUsersIdLocationsStatsRequest { diff --git a/libft-api/src/api/user/users_id_projects_users.rs b/libft-api/src/api/user/users_id_projects_users.rs index d13843f..7995560 100644 --- a/libft-api/src/api/user/users_id_projects_users.rs +++ b/libft-api/src/api/user/users_id_projects_users.rs @@ -1,13 +1,9 @@ +use crate::{prelude::*, to_param, HasVec}; +use libft_api_derive::HasVector; use rsb_derive::Builder; use serde::{Deserialize, Serialize}; use tracing::info; -use crate::{ - convert_filter_option_to_tuple, convert_range_option_to_tuple, to_param, ClientResult, - FtClientHttpConnector, FtClientSession, FtCursusId, FtFilterOption, FtProjectId, - FtProjectSessionId, FtProjectsUser, FtRangeOption, FtSortOption, FtUserId, -}; - #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiUsersIdProjectsUsersRequest { pub cursus_id: Option, @@ -21,7 +17,7 @@ pub struct FtApiUsersIdProjectsUsersRequest { pub per_page: Option, } -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiUsersIdProjectsUsersResponse { pub projects_users: Vec, @@ -87,7 +83,7 @@ mod tests { )); let session = client.open_session(token); - let res = session + let _ = session .users_id_projects_users(FtApiUsersIdProjectsUsersRequest::new(FtUserId::new( TEST_USER_YONDOO_ID, ))) diff --git a/libft-api/src/api/user/users_id_teams.rs b/libft-api/src/api/user/users_id_teams.rs index 5283f92..4c31fdf 100644 --- a/libft-api/src/api/user/users_id_teams.rs +++ b/libft-api/src/api/user/users_id_teams.rs @@ -1,11 +1,8 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{ - convert_filter_option_to_tuple, convert_range_option_to_tuple, to_param, ClientResult, - FtClientHttpConnector, FtClientSession, FtCursusId, FtFilterOption, FtProjectId, - FtProjectSessionId, FtRangeOption, FtSortOption, FtTeam, FtUserId, -}; +use crate::{prelude::*, to_param, HasVec}; +use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiUsersIdTeamsRequest { @@ -20,7 +17,7 @@ pub struct FtApiUsersIdTeamsRequest { pub per_page: Option, } -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiUsersIdTeamsResponse { pub teams: Vec, @@ -85,7 +82,7 @@ mod tests { )); let session = client.open_session(token); - let res = session + let _ = session .users_id_teams(FtApiUsersIdTeamsRequest::new(FtUserId::new( TEST_USER_YONDOO_ID, ))) diff --git a/libft-api/src/auth.rs b/libft-api/src/auth.rs index e44cb9d..86df310 100644 --- a/libft-api/src/auth.rs +++ b/libft-api/src/auth.rs @@ -55,17 +55,6 @@ impl FtApiToken { } } -#[derive(Debug, PartialEq, PartialOrd, Clone, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -enum AccessTokenScope { - Public, - Projects, - Profile, - Elearning, - Tig, - Forum, -} - #[derive(Debug, PartialEq, PartialOrd, Clone, Serialize, Deserialize)] enum AccessTokenType { #[serde(rename = "bearer")] diff --git a/libft-api/src/common/param.rs b/libft-api/src/common/param.rs index 5606f25..6a34e36 100644 --- a/libft-api/src/common/param.rs +++ b/libft-api/src/common/param.rs @@ -141,6 +141,8 @@ impl ToQueryParam for FtRangeField { } } +type QueryParam = Result)>, Box>; + /// Converts a list of options into query parameter tuples. /// /// # Arguments @@ -154,9 +156,7 @@ impl ToQueryParam for FtRangeField { /// # Errors /// /// Returns an error if converting a field to a query key fails. -pub fn convert_options_to_tuple( - options: Vec<(T, Vec)>, -) -> Result)>, Box> { +pub fn convert_options_to_tuple(options: Vec<(T, Vec)>) -> QueryParam { options .into_iter() .map(|(field, values)| { @@ -180,9 +180,7 @@ pub fn convert_options_to_tuple( /// # Errors /// /// Returns an error if converting a field to a query key fails. -pub fn convert_filter_option_to_tuple( - filter_options: Vec, -) -> Result)>, Box> { +pub fn convert_filter_option_to_tuple(filter_options: Vec) -> QueryParam { let options = filter_options .into_iter() .map(|option| (option.field, option.value)) @@ -199,9 +197,7 @@ pub fn convert_filter_option_to_tuple( /// # Errors /// /// Returns an error if converting a field to a query key fails. -pub fn convert_range_option_to_tuple( - range_options: Vec, -) -> Result)>, Box> { +pub fn convert_range_option_to_tuple(range_options: Vec) -> QueryParam { let options = range_options .into_iter() .map(|option| (option.range, option.value)) From 9f5d84e6d748acc9b7bbdaab0a9267e5a1cb77f4 Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Tue, 14 Oct 2025 11:13:06 +0900 Subject: [PATCH 08/18] refactor(ratelimiter): overhaul for correctness and performance This commit completely refactors the `RateLimiter` to improve its correctness, performance, and testability. The previous implementation used multiple `Arc>` for each state variable, leading to complex locking logic within the `acquire` loop. This could cause contention and made reasoning about the state difficult. The new design consolidates all state into a single `Inner` struct protected by one `Arc>`. The `acquire` method now uses a short-lived lock to decide on an action (Permit, Sleep, or Recheck) and performs any waiting *outside* the lock, preventing long-held locks. Key changes: - Replaced multiple mutexes with a single mutex over a state struct. - Switched from `SystemTime` to monotonic `tokio::time::Instant` for duration calculations. - Redesigned `acquire` to separate decision-making (under lock) from waiting (outside lock). - Rewrote all tests to use `tokio::time::pause()` and `advance()` for deterministic, reliable execution without real-time delays. - Added comprehensive tests for concurrency, header updates, and edge cases like `retry-after`. --- libft-api/src/common/ratelimiter.rs | 499 +++++++++++++++++----------- 1 file changed, 296 insertions(+), 203 deletions(-) diff --git a/libft-api/src/common/ratelimiter.rs b/libft-api/src/common/ratelimiter.rs index 8a951d4..b100013 100644 --- a/libft-api/src/common/ratelimiter.rs +++ b/libft-api/src/common/ratelimiter.rs @@ -1,7 +1,7 @@ use reqwest::header::HeaderMap; use std::sync::{Arc, Mutex}; -use std::time::{Duration, SystemTime}; -use tokio::time::sleep; +use std::time::Duration; +use tokio::time::{sleep_until, Instant}; #[derive(Debug, Clone)] pub struct HeaderMetaData { @@ -18,286 +18,379 @@ impl HeaderMetaData { } pub fn update_from_headers(&self, headers: &HeaderMap) { - let parse_header = |name: &str| -> Option { + let parse_u64 = |name: &str| -> Option { headers .get(name) - .and_then(|v| v.to_str().ok()) - .and_then(|s| s.parse::().ok()) + .and_then(|v| v.to_str().ok())? + .parse() + .ok() }; - if let Some(total) = parse_header("x-total") { + if let Some(total) = parse_u64("x-total") { *self.total_page.lock().unwrap() = total; } - if let Some(remaining) = parse_header("x-secondly-ratelimit-remaining") { - *self - .ratelimiter - .secondly_ratelimit_remaining - .lock() - .unwrap() = remaining; - } - - if let Some(remaining) = parse_header("x-hourly-ratelimit-remaining") { - *self.ratelimiter.hourly_ratelimit_remaining.lock().unwrap() = remaining; - } - - if let Some(retry_seconds) = parse_header("retry-after") { - let wait_duration = Duration::from_secs(retry_seconds); - let new_retry_time = SystemTime::now() + wait_duration; - *self.ratelimiter.retry_after.lock().unwrap() = new_retry_time; - } + self.ratelimiter.update_from_headers(headers); } } +#[derive(Debug)] +struct Inner { + sec_limit: u64, + hour_limit: u64, + sec_remaining: u64, + hour_remaining: u64, + sec_reset: Instant, + hour_reset: Instant, + retry_after_until: Option, +} + #[derive(Debug, Clone)] pub struct RateLimiter { - pub secondly_ratelimit_limit: u64, - hourly_ratelimit_limit: u64, - secondly_ratelimit_remaining: Arc>, - hourly_ratelimit_remaining: Arc>, - - // 429 응답의 `retry-after` 헤더를 처리하기 위한 필드 - retry_after: Arc>, - // 초당 요청 제한 윈도우가 언제 리셋되는지 추적하기 위한 필드 (추가됨) - secondly_window_reset: Arc>, - hourly_window_reset: Arc>, + inner: Arc>, } impl RateLimiter { pub fn new(per_second_limit: u64, hourly_limit: u64) -> Self { - let now = SystemTime::now(); + let now = Instant::now(); + let inner = Inner { + sec_limit: per_second_limit, + hour_limit: hourly_limit, + sec_remaining: per_second_limit, + hour_remaining: hourly_limit, + sec_reset: now + Duration::from_secs(1), + hour_reset: now + Duration::from_secs(3600), + retry_after_until: None, + }; Self { - secondly_ratelimit_limit: per_second_limit, - hourly_ratelimit_limit: hourly_limit, - secondly_ratelimit_remaining: Arc::new(Mutex::new(per_second_limit)), - hourly_ratelimit_remaining: Arc::new(Mutex::new(hourly_limit)), - retry_after: Arc::new(Mutex::new(now)), - secondly_window_reset: Arc::new(Mutex::new(now + Duration::from_secs(1))), - hourly_window_reset: Arc::new(Mutex::new(now + Duration::from_secs(3600))), + inner: Arc::new(Mutex::new(inner)), } } - /// API 요청을 보내기 전에 호출하여 ratelimit을 준수하도록 대기합니다. - pub async fn acquire(&self) { - loop { - let now = SystemTime::now(); - - // 1. 'retry-after'에 의한 강제 대기 확인 - // 외부에서 429 응답을 받았을 때 이 값을 업데이트했다고 가정합니다. - let retry_after_time = *self.retry_after.lock().unwrap(); - if now < retry_after_time { - let wait_duration = retry_after_time.duration_since(now).unwrap_or_default(); - sleep(wait_duration).await; - continue; // 대기 후 루프의 처음부터 다시 확인 - } + /// 헤더 기반 갱신: 한 번만 락 잡고 끝냄 + pub fn update_from_headers(&self, headers: &HeaderMap) { + let parse_u64 = |name: &str| -> Option { + headers + .get(name) + .and_then(|v| v.to_str().ok())? + .parse() + .ok() + }; - // Mutex Guard가 await 지점까지 살아남지 않도록 범위를 제한합니다. - let wait_duration = { - // 2. 초당 요청 제한 확인 (Fixed Window) - let mut remaining_guard_sec = self.secondly_ratelimit_remaining.lock().unwrap(); - let mut remaining_guard_hour = self.hourly_ratelimit_remaining.lock().unwrap(); - let mut reset_guard_sec = self.secondly_window_reset.lock().unwrap(); - let mut reset_guard_hour = self.hourly_window_reset.lock().unwrap(); - - if now >= *reset_guard_sec { - *remaining_guard_sec = self.secondly_ratelimit_limit; - *reset_guard_sec = now + Duration::from_secs(1); - } + let mut st = self.inner.lock().unwrap(); - if now >= *reset_guard_hour { - *remaining_guard_hour = self.hourly_ratelimit_limit; - *reset_guard_hour = now + Duration::from_secs(3600); - } + if let Some(rem) = parse_u64("x-secondly-ratelimit-remaining") { + // 서버가 알려준 값으로 덮어써서 동기화 + st.sec_remaining = rem.min(st.sec_limit); + } + if let Some(rem) = parse_u64("x-hourly-ratelimit-remaining") { + st.hour_remaining = rem.min(st.hour_limit); + } + if let Some(secs) = parse_u64("retry-after") { + st.retry_after_until = Some(Instant::now() + Duration::from_secs(secs)); + } + } - if *remaining_guard_hour > 0 { - *remaining_guard_hour -= 1; - if *remaining_guard_sec > 0 { - *remaining_guard_sec -= 1; - break; + /// 요청 전 호출: 락은 매우 짧게만 잡고, 대기는 락 밖에서 수행 + pub async fn acquire(&self) { + loop { + // 락을 짧게 잡아서 '무엇을 할지'만 결정하고 곧바로 풀기 + let decision = { + let mut st = self.inner.lock().unwrap(); + let now = Instant::now(); + + // 1) Retry-After가 남아있으면 그 시각까지 잔다 + if let Some(deadline) = st.retry_after_until { + if now < deadline { + Control::Sleep(deadline) } else { - reset_guard_sec.duration_since(now).unwrap_or_default() + st.retry_after_until = None; // 만료됨 + Control::Recheck } } else { - reset_guard_hour.duration_since(now).unwrap_or_default() + // 2) 윈도 리셋 + if now >= st.sec_reset { + st.sec_remaining = st.sec_limit; + st.sec_reset = now + Duration::from_secs(1); + } + if now >= st.hour_reset { + st.hour_remaining = st.hour_limit; + st.hour_reset = now + Duration::from_secs(3600); + } + + // 3) 토큰 소비 가능? + if st.sec_remaining > 0 && st.hour_remaining > 0 { + st.sec_remaining -= 1; + st.hour_remaining -= 1; + Control::Permit + } else { + // 부족한 쪽의 리셋 시각까지 잔다 + let next = if st.sec_remaining == 0 { + st.sec_reset + } else { + st.hour_reset + }; + Control::Sleep(next) + } } }; - // wait_duration이 0보다 크면 대기합니다. - if !wait_duration.is_zero() { - sleep(wait_duration).await; + match decision { + Control::Permit => return, // 바로 진행 + Control::Sleep(deadline) => sleep_until(deadline).await, + Control::Recheck => {} // 즉시 루프 재검사 } - // 대기 후, 다른 스레드가 먼저 토큰을 가져갔을 수 있으므로 loop를 다시 돕니다. } } } +enum Control { + Permit, + Sleep(Instant), + Recheck, +} + #[cfg(test)] mod tests { use super::*; - use std::time::{Duration, Instant}; + use reqwest::header::{HeaderMap, HeaderValue}; + use tokio::time as ttime; + use ttime::{Duration, Instant}; + + // ---- 위에 붙여둔 with_windows 도우미가 이 모듈 안에 함께 있어야 합니다. ---- + #[cfg(any(test, feature = "test_helpers"))] + impl RateLimiter { + fn with_windows( + per_second_limit: u64, + hourly_limit: u64, + sec_window: std::time::Duration, + hour_window: std::time::Duration, + ) -> Self { + let now = Instant::now(); + let inner = Inner { + sec_limit: per_second_limit, + hour_limit: hourly_limit, + sec_remaining: per_second_limit, + hour_remaining: hourly_limit, + sec_reset: now + Duration::from_secs_f64(sec_window.as_secs_f64()), + hour_reset: now + Duration::from_secs_f64(hour_window.as_secs_f64()), + retry_after_until: None, + }; + Self { + inner: std::sync::Arc::new(std::sync::Mutex::new(inner)), + } + } + } - /// 테스트 1: 제한 횟수 내에서는 대기 없이 바로 통과하는지 확인 - #[tokio::test] + /// 제한 이내에서는 대기 없이 통과 + #[tokio::test(start_paused = true)] async fn test_can_acquire_within_limit() { - let limiter = RateLimiter::new(5, 100); // 초당 5회 - let start = Instant::now(); - + let limiter = + RateLimiter::with_windows(5, 100, Duration::from_secs(1), Duration::from_secs(3600)); + let t0 = Instant::now(); for _ in 0..5 { limiter.acquire().await; } - - let elapsed = start.elapsed(); - println!("Elapsed time for 5 acquires: {:?}", elapsed); - - // 5번의 호출은 sleep 없이 즉시 처리되어야 하므로 매우 짧은 시간 안에 끝나야 합니다. - assert!(elapsed < Duration::from_millis(100)); + // 가상 시간은 전진하지 않음(슬립이 없었단 뜻) + assert_eq!(Instant::now() - t0, Duration::from_millis(0)); } - /// 테스트 2: 초당 제한을 초과하면 1초 이상 대기하는지 확인 - #[tokio::test] + /// 초당 제한 초과 시 다음 윈도우까지 정확히 대기 + #[tokio::test(start_paused = true)] async fn test_waits_when_per_second_limit_exceeded() { - let limiter = RateLimiter::new(3, 100); // 초당 3회 - let start = Instant::now(); - - // 4번 호출 -> 3번은 즉시, 1번은 1초 뒤에 실행되어야 함 - for _ in 0..4 { + let limiter = + RateLimiter::with_windows(3, 100, Duration::from_secs(1), Duration::from_secs(3600)); + // 3개는 즉시 + for _ in 0..3 { limiter.acquire().await; } + // 4번째는 1초 뒤 + let j = tokio::spawn({ + let l = limiter.clone(); + async move { l.acquire().await } + }); - let elapsed = start.elapsed(); - println!( - "Elapsed time for 4 acquires with 3/sec limit: {:?}", - elapsed - ); + ttime::advance(Duration::from_millis(999)).await; + assert!(!j.is_finished(), "아직 1초 미만이므로 완료되면 안됨"); - // 총 소요 시간은 최소 1초 이상이어야 합니다. - assert!(elapsed >= Duration::from_secs(1)); - // 너무 오래 기다리는 것도 아니어야 합니다. (네트워크 지연 등 감안) - assert!(elapsed < Duration::from_secs(2)); + ttime::advance(Duration::from_millis(1)).await; // 총 1s + j.await.unwrap(); } - /// 테스트 3: 1초가 지나면 제한이 초기화되는지 확인 - #[tokio::test] - async fn test_limit_resets_after_one_second() { - let limiter = RateLimiter::new(2, 100); // 초당 2회 - - // 1. 먼저 제한을 모두 소진 + /// 짧은 윈도우(200ms)에서 리셋 확인 + #[tokio::test(start_paused = true)] + async fn test_limit_resets_after_short_window() { + let limiter = RateLimiter::with_windows( + 2, + 100, + Duration::from_millis(200), + Duration::from_secs(3600), + ); limiter.acquire().await; limiter.acquire().await; - // 2. 1초보다 길게 대기하여 윈도우를 리셋 - sleep(Duration::from_millis(1100)).await; + let j = tokio::spawn({ + let l = limiter.clone(); + async move { l.acquire().await } + }); - // 3. 리셋된 후에는 다시 즉시 통과해야 함 - let start = Instant::now(); - limiter.acquire().await; - limiter.acquire().await; - let elapsed = start.elapsed(); - - println!("Elapsed time for 2 acquires after reset: {:?}", elapsed); + ttime::advance(Duration::from_millis(199)).await; + assert!(!j.is_finished(), "아직 200ms 전이므로 대기해야 함"); - // 리셋된 후의 호출은 다시 빨라야 합니다. - assert!(elapsed < Duration::from_millis(100)); + ttime::advance(Duration::from_millis(1)).await; // 200ms 도달 + j.await.unwrap(); } - /// 테스트 4: 여러 태스크에서 동시에 접근해도 안전하게 동작하는지 확인 - #[tokio::test] - async fn test_concurrent_acquires_are_safe() { - // Arc를 통해 여러 태스크가 RateLimiter를 공유 - let limiter = Arc::new(RateLimiter::new(8, 100)); // 초당 5회 - let mut tasks = vec![]; - - let start = Instant::now(); - - // 10개의 태스크를 생성하여 동시에 acquire 호출 - for i in 0..64 { - let limiter_clone = Arc::clone(&limiter); - tasks.push(tokio::spawn(async move { - println!("Task {} acquiring...", i); - limiter_clone.acquire().await; - println!("Task {} acquired.", i); - })); - } + /// 동시 접근 시 초당 한 배치씩 처리되는지(배칭) 확인 + #[tokio::test(start_paused = true)] + async fn test_concurrent_acquires_batching() { + let limiter = + RateLimiter::with_windows(8, 1000, Duration::from_secs(1), Duration::from_secs(3600)); - // 모든 태스크가 끝날 때까지 대기 - futures::future::join_all(tasks).await; + let mut handles = Vec::new(); + for _ in 0..32 { + let l = limiter.clone(); + handles.push(tokio::spawn(async move { l.acquire().await })); + } - let elapsed = start.elapsed(); - println!( - "Elapsed time for 64 concurrent acquires with 8/sec limit: {:?}", - elapsed + // 스케줄링 기회 부여 + tokio::task::yield_now().await; + assert_eq!( + handles.iter().filter(|h| h.is_finished()).count(), + 8, + "첫 8개는 즉시 통과" ); - assert!(elapsed >= Duration::from_secs(7)); - assert!(elapsed < Duration::from_secs(8)); + for i in 1..=3 { + ttime::advance(Duration::from_secs(1)).await; + tokio::task::yield_now().await; + assert_eq!( + handles.iter().filter(|h| h.is_finished()).count(), + 8 * (i + 1), + "매 1초마다 8개씩 완료되어야 함" + ); + } + + for h in handles { + h.await.unwrap(); + } } - use reqwest::header::HeaderValue; - /// 테스트 5: 정상 응답(200 OK) 헤더를 받았을 때 상태가 올바르게 업데이트되는지 확인 - #[test] - fn test_update_from_successful_response() { - let meta = HeaderMetaData::new(RateLimiter::new(2, 1200)); + /// retry-after 헤더가 다음 acquire를 정확히 지연 + #[tokio::test(start_paused = true)] + async fn test_retry_after_delays_acquire() { + let limiter = + RateLimiter::with_windows(5, 100, Duration::from_secs(1), Duration::from_secs(3600)); + let mut headers = HeaderMap::new(); + headers.insert("retry-after", HeaderValue::from_static("2")); + limiter.update_from_headers(&headers); + + let j = tokio::spawn({ + let l = limiter.clone(); + async move { l.acquire().await } + }); + + ttime::advance(Duration::from_secs(1)).await; + assert!(!j.is_finished(), "2초 이전이므로 대기 중이어야 함"); + + ttime::advance(Duration::from_secs(1)).await; // 총 2s + j.await.unwrap(); + } + + /// 서버가 secondly remaining=0을 보냈을 때 즉시 대기 시작하는지 + #[tokio::test(start_paused = true)] + async fn test_header_remaining_zero_enforces_wait() { + let limiter = RateLimiter::with_windows( + 2, + 100, + Duration::from_millis(300), + Duration::from_secs(3600), + ); - // 시뮬레이션할 헤더 생성 let mut headers = HeaderMap::new(); headers.insert( "x-secondly-ratelimit-remaining", HeaderValue::from_static("0"), ); - headers.insert( - "x-hourly-ratelimit-remaining", - HeaderValue::from_static("1194"), - ); + limiter.update_from_headers(&headers); - // 상태 업데이트 - meta.update_from_headers(&headers); + let j = tokio::spawn({ + let l = limiter.clone(); + async move { l.acquire().await } + }); - // 상태 검증 - let secondly_remaining = *meta - .ratelimiter - .secondly_ratelimit_remaining - .lock() - .unwrap(); - let hourly_remaining = *meta.ratelimiter.hourly_ratelimit_remaining.lock().unwrap(); + ttime::advance(Duration::from_millis(299)).await; + assert!(!j.is_finished()); + ttime::advance(Duration::from_millis(1)).await; // 300ms 경과 + j.await.unwrap(); + } - assert_eq!( - secondly_remaining, 0, - "Secondly remaining should be updated to 0" - ); - assert_eq!( - hourly_remaining, 1194, - "Hourly remaining should be updated to 1194" - ); + /// (테스트용) 시간당 윈도우를 2초로 줄여서 hour limit 동작 확인 + #[tokio::test(start_paused = true)] + async fn test_hourly_window_respected_with_short_window() { + // 초당은 넉넉(100), 시간당은 3, hour_window 2초 + let limiter = + RateLimiter::with_windows(100, 3, Duration::from_millis(50), Duration::from_secs(2)); + + for _ in 0..3 { + limiter.acquire().await; // 시간당 3개 소진 + } + + let j = tokio::spawn({ + let l = limiter.clone(); + async move { l.acquire().await } // 4번째 -> hour reset까지 대기 + }); + + ttime::advance(Duration::from_secs(1)).await; + assert!(!j.is_finished(), "아직 hour 윈도우(2s) 전"); + + ttime::advance(Duration::from_secs(1)).await; // 총 2s -> 리셋 + j.await.unwrap(); } - /// 테스트 6: 속도 제한 응답(429) 헤더를 받았을 때 retry_after가 올바르게 설정되는지 확인 - #[test] - fn test_update_from_ratelimited_response() { - let meta = HeaderMetaData::new(RateLimiter::new(2, 1200)); - let before_update = SystemTime::now(); + /// retry-after > per-second reset: 더 강한 제약이 우선하는지 확인 + #[tokio::test(start_paused = true)] + async fn test_interleaved_retry_after_and_window() { + // 초당 2개, 300ms 윈도우 + let limiter = RateLimiter::with_windows( + 2, + 100, + Duration::from_millis(300), + Duration::from_secs(3600), + ); + + // 첫 토큰 소비 + limiter.acquire().await; - // 429 응답 시뮬레이션 헤더 + // 서버가 1초 retry-after 지시 let mut headers = HeaderMap::new(); headers.insert("retry-after", HeaderValue::from_static("1")); + limiter.update_from_headers(&headers); + + // 다음 acquire는 retry-after 끝까지 대기해야 함 + let j = tokio::spawn({ + let l = limiter.clone(); + async move { l.acquire().await } + }); + + // per-second 윈도우가 먼저 지나가도… + ttime::advance(Duration::from_millis(300)).await; + tokio::task::yield_now().await; + assert!(!j.is_finished(), "윈도우 리셋이 와도 retry-after가 우선"); + + // retry-after 종료 시 진행 + ttime::advance(Duration::from_millis(700)).await; // 총 1s + j.await.unwrap(); + } - // 상태 업데이트 + /// HeaderMetaData가 x-total을 반영하는지(부가 메타 확인) + #[test] + fn test_header_metadata_updates_total_page() { + let meta = HeaderMetaData::new(RateLimiter::new(5, 100)); + let mut headers = HeaderMap::new(); + headers.insert("x-total", HeaderValue::from_static("42")); meta.update_from_headers(&headers); - // 상태 검증 - let retry_time = *meta.ratelimiter.retry_after.lock().unwrap(); - - // retry_time이 업데이트 이전 시간보다 미래인지 확인 - assert!( - retry_time > before_update, - "retry_after time should be in the future" - ); - - // 대기 시간이 약 1초인지 확인 (실행 시간 오차 감안) - let wait_duration = retry_time.duration_since(before_update).unwrap(); - assert!( - wait_duration >= Duration::from_millis(900) - && wait_duration <= Duration::from_millis(1100), - "The wait duration should be approximately 1 second. Actual: {:?}", - wait_duration - ); + let total = *meta.total_page.lock().unwrap(); + assert_eq!(total, 42); } } From 668fe44d4fe1e1458f0178fb0688d933eab623d4 Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Tue, 14 Oct 2025 12:00:07 +0900 Subject: [PATCH 09/18] refactor(client)!: introduce token caching and improve ergonomics This commit refactors the client and authentication flow to improve ergonomics and performance. A token caching mechanism has been introduced via `FtApiToken::try_get`. This function attempts to retrieve a valid token from a temporary file, falling back to fetching a new one if the cached token is invalid or non-existent. This reduces the number of authentication requests. The `journals.rs` example binary has been updated to use a new `scroller` utility, simplifying pagination logic. Additionally, extensive documentation has been added across the `auth`, `client`, and `connector` modules to improve developer experience. BREAKING CHANGE: The `page` field in `FtApiCampusIdJournalsRequest` has been changed from `Option` to `Option` to support a larger number of pages. --- libft-api/Cargo.toml | 2 +- libft-api/bin/journals.rs | 90 ++++++------------- .../src/api/campus/campus_id_journals.rs | 23 ++++- libft-api/src/api/group/groups.rs | 44 ++++----- .../user/users_id_correction_points_add.rs | 2 +- libft-api/src/auth.rs | 32 +++++++ libft-api/src/common/client.rs | 15 ++++ libft-api/src/connector.rs | 7 ++ 8 files changed, 125 insertions(+), 90 deletions(-) diff --git a/libft-api/Cargo.toml b/libft-api/Cargo.toml index 88de5d0..c1cdf09 100644 --- a/libft-api/Cargo.toml +++ b/libft-api/Cargo.toml @@ -65,7 +65,7 @@ serde_json = { version = "1.0.145", features = ["std"] } serde_plain = "1.0.2" reqwest = { version = "0.12.24", features = ["json"] } rvstruct = "0.3.2" -tokio = { version = "1.47.1", features = ["full", "tracing"] } +tokio = { version = "1.47.1", features = ["full", "tracing", "test-util"] } chrono = { version = "0.4.42", features = ["serde"] } rsb_derive = "0.5.1" url = { version = "2.5.7", features = ["serde"] } diff --git a/libft-api/bin/journals.rs b/libft-api/bin/journals.rs index dbe2c40..30bc20c 100644 --- a/libft-api/bin/journals.rs +++ b/libft-api/bin/journals.rs @@ -1,74 +1,34 @@ -use std::{sync::Arc, time::Duration}; +use std::io::Write; -use libft_api::{campus_id::GYEONGSAN, prelude::*, FT_PISCINE_CURSUS_ID}; -use tokio::{sync::Semaphore, task, time::sleep}; +use futures::FutureExt; +use libft_api::{campus_id::GYEONGSAN, prelude::*}; #[tokio::main] async fn main() { - println!("id|user_id|item_type|item_id|reason|created_at|updated_at|event_at"); - let thread_num = 8; - let permit = Arc::new(Semaphore::new(thread_num)); + let client = FtClient::new(FtClientReqwestConnector::new()); - for mut page in 1..=thread_num { - let permit = Arc::clone(&permit); - task::spawn(async move { - let _permit = permit.acquire().await.unwrap(); - loop { - let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) - .await - .unwrap(); - - let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(token)); - let res = session - .campus_id_journals( - FtApiCampusIdJournalsRequest::new( - FtCampusId::new(GYEONGSAN), - "2025-1-20".to_string(), - "2025-1-23".to_string(), - ) - .with_page(page as u16) - .with_filter(vec![FtFilterOption::new( - FtFilterField::CursusId, - vec![FT_PISCINE_CURSUS_ID.to_string()], - )]), + let req: ReqFn = |session, page| { + async move { + session + .campus_id_journals( + FtApiCampusIdJournalsRequest::new( + FtCampusId(GYEONGSAN), + "2025-10-1".to_string(), + "2025-10-3".to_string(), ) - .await; + .with_page(page) + .with_per_page(100), + ) + .await + } + .boxed() + }; - match res { - Ok(res) => { - if res.journals.is_empty() { - break; - } - for ele in res.journals { - println!( - "{},{},{},{},{},{},{},{},{},{}", - ele.id, - ele.user_id, - ele.item_type, - ele.item_id, - ele.cursus_id, - ele.campus_id, - ele.reason, - ele.created_at.0, - ele.updated_at.0, - ele.event_at.0, - ); - } - page += thread_num; - } - Err(FtClientError::RateLimitError(_)) => { - eprintln!("rate limit, try again."); - sleep(Duration::new(1, 42)).await - } - Err(e) => { - eprintln!("other error: {e}"); - break; - } - } - } - }) - .await - .unwrap(); + let mut result = Vec::new(); + for i in 1..=2 { + result.push(scroller(&client, 2, i, req).await); } + + let mut file = std::fs::File::open("temp.json").unwrap(); + let _ = file.write_all(serde_json::to_string_pretty(&result).unwrap().as_bytes()); } diff --git a/libft-api/src/api/campus/campus_id_journals.rs b/libft-api/src/api/campus/campus_id_journals.rs index 0b9ec88..be776ab 100644 --- a/libft-api/src/api/campus/campus_id_journals.rs +++ b/libft-api/src/api/campus/campus_id_journals.rs @@ -14,7 +14,7 @@ pub struct FtApiCampusIdJournalsRequest { pub sort: Option>, pub range: Option>, pub filter: Option>, - pub page: Option, + pub page: Option, pub per_page: Option, } @@ -28,6 +28,27 @@ impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { + /// Get journals for a specific campus. + /// + /// This action requires the 'Advanced staff' role. + /// This resource is paginated, with a default of 30 items per page. + /// You have to provide parameters with FtApiCampusIdJournalsRequest structure + /// + /// # Parameters + /// + /// * `begin_at`: **Required** (`String`). Must be before or equal to `end_at`. The date range must be 124 days maximum. + /// * `end_at`: **Required** (`String`). Must be after or equal to `begin_at`. The date range must be 124 days maximum. + /// * `campus_id`: **Required** (`String`). The campus ID or slug. + /// * `sort`: Optional. The sort field. Sorted by `id` desc by default. + /// Must be one of: `id`, `user_id`, `item_type`, `item_id`, `cursus_id`, `campus_id`, `reason`, `created_at`, `updated_at`, `event_at`, `alumni`, `closed`. + /// * `filter`: Optional. Filtering on one or more fields. + /// Must be one of: `id`, `user_id`, `item_type`, `item_id`, `cursus_id`, `campus_id`, `reason`, `created_at`, `updated_at`, `event_at`, `alumni`, `closed`, `event`. + /// * `page[size]`: Optional (`Integer`). The number of items per page. Defaults to 30, maximum 100. + /// * `page[number]`: Optional (`Integer`). The current page number. + /// + /// # Errors + /// + /// * This function will return an error if the authenticated user does not have the [`Advanced staff`] role. pub async fn campus_id_journals( &self, req: FtApiCampusIdJournalsRequest, diff --git a/libft-api/src/api/group/groups.rs b/libft-api/src/api/group/groups.rs index 8c18e86..0d4238b 100644 --- a/libft-api/src/api/group/groups.rs +++ b/libft-api/src/api/group/groups.rs @@ -63,28 +63,28 @@ mod tests { use super::*; - #[tokio::test] - async fn post_groups() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) - .await - .unwrap(); - - let client = FtClient::new(FtClientReqwestConnector::with_connector( - reqwest::Client::new(), - )); - - let session = client.open_session(token); - - let res = session - .groups_users_post(FtApiGroupsUsersPostRequest::new(FtApiGroupsUsersPostBody { - group_id: FtGroupId::new(FT_GROUP_ID_TEST_ACCOUNT), - user_id: FtUserId::new(212_750), - })) - .await - .unwrap(); - - assert_eq!(res.group.id, FtGroupId::new(FT_GROUP_ID_TEST_ACCOUNT)); - } + // #[tokio::test] + // async fn post_groups() { + // let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + // .await + // .unwrap(); + // + // let client = FtClient::new(FtClientReqwestConnector::with_connector( + // reqwest::Client::new(), + // )); + // + // let session = client.open_session(token); + // + // let res = session + // .groups_users_post(FtApiGroupsUsersPostRequest::new(FtApiGroupsUsersPostBody { + // group_id: FtGroupId::new(FT_GROUP_ID_TEST_ACCOUNT), + // user_id: FtUserId::new(212_750), + // })) + // .await + // .unwrap(); + // + // assert_eq!(res.group.id, FtGroupId::new(FT_GROUP_ID_TEST_ACCOUNT)); + // } #[tokio::test] async fn get_groups() { diff --git a/libft-api/src/api/user/users_id_correction_points_add.rs b/libft-api/src/api/user/users_id_correction_points_add.rs index 6fc00a2..6895eeb 100644 --- a/libft-api/src/api/user/users_id_correction_points_add.rs +++ b/libft-api/src/api/user/users_id_correction_points_add.rs @@ -60,7 +60,7 @@ mod tests { #[tokio::test] async fn correction_points_add_test() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/auth.rs b/libft-api/src/auth.rs index 86df310..4ab40c0 100644 --- a/libft-api/src/auth.rs +++ b/libft-api/src/auth.rs @@ -10,16 +10,23 @@ use chrono::{DateTime, TimeZone, Utc}; use serde::{Deserialize, Serialize}; // TODO: add scope +/// Authentication information for the 42 API. pub struct AuthInfo { uid: String, secret: String, } impl AuthInfo { + /// Create a new `AuthInfo` from the given UID and secret. pub fn from_env(uid: String, secret: String) -> AuthInfo { AuthInfo { uid, secret } } + /// Build `AuthInfo` from environment variables. + /// + /// # Errors + /// + /// This function will return an error if the `FT_API_CLIENT_UID` or `FT_API_CLIENT_SECRET` environment variables are not set. pub fn build_from_env() -> Result { let uid = config_env_var("FT_API_CLIENT_UID")?; let secret = config_env_var("FT_API_CLIENT_SECRET")?; @@ -29,6 +36,7 @@ impl AuthInfo { #[inline] // TODO: replace scope to field 'scope' + /// Get the parameters for the API token request. pub fn get_params(&self) -> [(&str, &str); 4] { [ ("grant_type", "client_credentials"), @@ -40,6 +48,7 @@ impl AuthInfo { } #[derive(Debug, PartialEq, PartialOrd, Clone, Serialize, Deserialize)] +/// Represents an API token from the 42 API. pub struct FtApiToken { access_token: String, token_type: AccessTokenType, @@ -50,6 +59,7 @@ pub struct FtApiToken { } impl FtApiToken { + /// Get the token value as a string. pub fn get_token_value(&self) -> String { format!("{} {}", self.token_type, self.access_token) } @@ -68,12 +78,19 @@ impl Display for AccessTokenType { } #[derive(Debug)] +/// Represents an error that can occur when handling an API token. pub enum TokenError { + /// An I/O error occurred. IOError(io::Error), + /// An error occurred during JSON serialization or deserialization. SerdeError(SerdeError), + /// The token has expired. TokenExpired, + /// The token lifetime could not be parsed. TokenLifeTimeParsingFailed, + /// The temporary token was not found. TempTokenNotFound, + /// An error occurred while building the token. BuildError(String), } impl From for TokenError { @@ -115,6 +132,11 @@ impl FtApiToken { } } + /// Try to get a token from the cache, or build a new one if it's not available. + /// + /// # Errors + /// + /// This function will return an error if it fails to build a new token. pub async fn try_get(info: AuthInfo) -> Result { if let Ok(token) = Self::__try_get() { return Ok(token); @@ -131,6 +153,11 @@ impl FtApiToken { Ok(token) } + /// Save the token to the cache. + /// + /// # Errors + /// + /// This function will return an error if it fails to create the cache file or write to it. pub fn save(&self) -> Result<(), TokenError> { let tmpdir = std::env::temp_dir().join(".ft_api_auth_token"); let mut token = File::create_new(tmpdir)?; @@ -138,6 +165,11 @@ impl FtApiToken { Ok(()) } + /// Build a new token from the given `AuthInfo`. + /// + /// # Errors + /// + /// This function will return an error if the request to the API fails or if the response cannot be parsed. pub async fn build(info: AuthInfo) -> Result { let params = info.get_params(); diff --git a/libft-api/src/common/client.rs b/libft-api/src/common/client.rs index c6ccd4b..71ffb07 100644 --- a/libft-api/src/common/client.rs +++ b/libft-api/src/common/client.rs @@ -20,11 +20,13 @@ where pub meta: HeaderMetaData, } +/// The HTTP API client. #[derive(Clone, Debug)] pub struct FtClientHttpApi where FCHC: FtClientHttpConnector + Send, { + /// The HTTP connector. pub connector: Arc, } @@ -55,7 +57,9 @@ pub struct FtEnvelopeMessage { pub warnings: Option>, } +/// A trait for an HTTP client that can connect to the 42 API. pub trait FtClientHttpConnector { + /// Send an HTTP GET request to the given URI. fn http_get_uri<'a, RS>( &'a self, full_uri: Url, @@ -65,6 +69,7 @@ pub trait FtClientHttpConnector { where RS: for<'de> serde::de::Deserialize<'de> + Send + 'a; + /// Send an HTTP GET request to the given relative URI. fn http_get<'a, 'p, RS, PT, TS>( &'a self, method_relative_uri: &str, @@ -87,6 +92,7 @@ pub trait FtClientHttpConnector { } } + /// Send an HTTP POST request to the given URI. fn http_post_uri<'a, RQ, RS>( &'a self, full_uri: Url, @@ -97,6 +103,7 @@ pub trait FtClientHttpConnector { RQ: serde::ser::Serialize + Send + Sync, RS: for<'de> serde::de::Deserialize<'de> + Send + 'a; + /// Send an HTTP POST request to the given relative URI. fn http_post<'a, RQ, RS>( &'a self, method_relative_uri: &str, @@ -113,6 +120,7 @@ pub trait FtClientHttpConnector { } } + /// Send an HTTP PATCH request to the given URI. fn http_patch_uri<'a, RQ, RS>( &'a self, full_uri: Url, @@ -123,6 +131,7 @@ pub trait FtClientHttpConnector { RQ: serde::ser::Serialize + Send + Sync, RS: for<'de> serde::de::Deserialize<'de> + Send + 'a; + /// Send an HTTP PATCH request to the given relative URI. fn http_patch<'a, RQ, RS>( &'a self, method_relative_uri: &str, @@ -139,6 +148,7 @@ pub trait FtClientHttpConnector { } } + /// Send an HTTP DELETE request to the given URI. fn http_delete_uri<'a, RQ, RS>( &'a self, full_uri: Url, @@ -149,6 +159,7 @@ pub trait FtClientHttpConnector { RQ: serde::ser::Serialize + Send + Sync, RS: for<'de> serde::de::Deserialize<'de> + Send + 'a; + /// Send an HTTP DELETE request to the given relative URI. fn http_delete<'a, RQ, RS>( &'a self, method_relative_uri: &str, @@ -165,6 +176,7 @@ pub trait FtClientHttpConnector { } } + /// Create a new `Url` from a relative URI. fn create_method_uri_path(&self, method_relative_uri: &str) -> ClientResult { Ok(FtClientHttpApiUri::create_method_uri_path(method_relative_uri).parse()?) } @@ -174,6 +186,7 @@ impl FtClient where FCHC: FtClientHttpConnector + Send + Sync, { + /// Create a new `FtClient` with the given HTTP connector. pub fn new(http_connector: FCHC) -> Self { Self { http_api: FtClientHttpApi::new(Arc::new(http_connector)), @@ -181,6 +194,7 @@ where } } + /// Create a new `FtClient` with the given HTTP connector and rate limits. pub fn with_ratelimits(http_connector: FCHC, secondly: u64, hourly: u64) -> Self { Self { http_api: FtClientHttpApi::new(Arc::new(http_connector)), @@ -188,6 +202,7 @@ where } } + /// Open a new session for the client. pub fn open_session(&'_ self, token: FtApiToken) -> FtClientSession<'_, FCHC> { // TODO: Add tracer for LOGGING // let http_session_span = span!(Level::DEBUG, "Ft API request",); diff --git a/libft-api/src/connector.rs b/libft-api/src/connector.rs index a3cba86..ee62a16 100644 --- a/libft-api/src/connector.rs +++ b/libft-api/src/connector.rs @@ -14,6 +14,7 @@ use crate::{ HeaderMetaData, }; +/// A client for the 42 API that uses `reqwest` as the underlying HTTP client. pub struct FtClientReqwestConnector { reqwest_connector: Client, ft_api_url: String, @@ -25,18 +26,23 @@ impl Default for FtClientReqwestConnector { } } +/// A context for a single API call. #[derive(Debug, Clone)] pub struct FtClientApiCallContext<'a> { + /// The tracing span for the call. pub tracing_span: &'a Span, + /// The current page number, if the call is paginated. pub current_page: Option, } impl FtClientReqwestConnector { + /// Create a new `FtClientReqwestConnector` with a default `reqwest` client. #[must_use] pub fn new() -> Self { Self::with_connector(reqwest::Client::new()) } + /// Create a new `FtClientReqwestConnector` with the given `reqwest` client. #[must_use] pub fn with_connector(connector: Client) -> Self { Self { @@ -45,6 +51,7 @@ impl FtClientReqwestConnector { } } + /// Set the 42 API URL for the client. #[must_use] pub fn with_ft_api_url(self, ft_api_url: &str) -> Self { Self { From 26189f368cf7578d1b16f0bfc9c5f2e7ee19c262 Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Tue, 14 Oct 2025 16:41:27 +0900 Subject: [PATCH 10/18] refactor(core)!: overhaul crate structure and API for clarity This commit introduces a major refactoring of the `libft-api` crate to improve its structure, ergonomics, and maintainability. Key changes include: - Reorganized the module hierarchy into distinct `api`, `models`, `auth`, `common`, `connector`, and `info` modules. - Introduced top-level and model-specific preludes for easier imports. - Migrated all binaries from the `bin/` directory to `examples/`, converting them to library examples. - Replaced `FtApiToken::build` with `FtApiToken::try_get` for consistent error handling. - Added crate-level documentation to `lib.rs` explaining the new structure and usage. - Cleaned up dependencies and `use` statements across the codebase. BREAKING CHANGE: - The entire public API surface has been reorganized. Module paths for most types and functions have changed. Users will need to update their `use` statements. - Binaries are no longer available via `cargo install`. They must now be run as examples using `cargo run --example `. - `FtApiToken::build` has been removed in favor of `FtApiToken::try_get`. - The type of `FtProjectSessionId` has been changed from `i16` to `u16`. --- libft-api/Cargo.toml | 49 +-- libft-api/bin/campus_users.rs | 203 ------------- libft-api/bin/evaluation.rs | 287 ------------------ libft-api/bin/exam_resubscribe.rs | 47 --- libft-api/bin/final_score.rs | 245 --------------- libft-api/bin/get_user_ext.rs | 105 ------- libft-api/bin/journals.rs | 34 --- libft-api/bin/location_stat.rs | 133 -------- libft-api/bin/locations.rs | 50 --- libft-api/bin/project_stats.rs | 79 ----- libft-api/bin/teams.rs | 202 ------------ libft-api/bin/user_creation.rs | 125 -------- libft-api/bin/user_subscribe.rs | 95 ------ .../{bin/blackholed.rs => examples/scroll.rs} | 3 +- libft-api/src/api.rs | 6 + libft-api/src/api/campus/campus_id.rs | 7 +- .../src/api/campus/campus_id_journals.rs | 11 +- .../src/api/campus/campus_id_locations.rs | 8 +- libft-api/src/api/campus/campus_id_users.rs | 8 +- libft-api/src/api/campus/campus_users.rs | 8 +- .../src/api/cursus/cursus_id_projects.rs | 8 +- libft-api/src/api/exam/exams.rs | 7 +- libft-api/src/api/group/groups.rs | 7 +- libft-api/src/api/prelude.rs | 5 +- libft-api/src/api/project/project_data.rs | 12 +- libft-api/src/api/project/projects.rs | 5 +- .../src/api/project/projects_id_teams.rs | 13 +- .../project_sessions_id_scale_teams.rs | 7 +- .../project_sessions_id_teams.rs | 13 +- .../src/api/project_user/projects_users.rs | 9 +- libft-api/src/api/scale_team/scale_teams.rs | 5 +- libft-api/src/api/user/users.rs | 33 +- libft-api/src/api/user/users_id.rs | 2 +- .../users_id_correction_point_historics.rs | 5 +- .../user/users_id_correction_points_add.rs | 21 +- .../src/api/user/users_id_cursus_users.rs | 9 +- libft-api/src/api/user/users_id_locations.rs | 7 +- .../src/api/user/users_id_locations_stats.rs | 4 +- .../src/api/user/users_id_projects_users.rs | 5 +- libft-api/src/api/user/users_id_teams.rs | 5 +- libft-api/src/axum_support/mod.rs | 19 -- libft-api/src/common/client.rs | 5 +- libft-api/src/common/paginator.rs | 2 +- libft-api/src/connector.rs | 7 +- libft-api/src/info.rs | 5 +- libft-api/src/lib.rs | 56 +++- libft-api/src/models.rs | 121 ++------ libft-api/src/models/achievement.rs | 4 +- libft-api/src/models/campus.rs | 3 +- libft-api/src/models/campus_user.rs | 5 +- libft-api/src/models/common.rs | 1 - .../src/models/correction_point_history.rs | 12 +- libft-api/src/models/cursus_user.rs | 13 +- libft-api/src/models/datetime.rs | 47 +++ libft-api/src/models/exam.rs | 3 +- libft-api/src/models/feedback.rs | 4 +- libft-api/src/models/flag.rs | 4 +- libft-api/src/models/group.rs | 1 + libft-api/src/models/image.rs | 3 +- libft-api/src/models/journals.rs | 3 +- libft-api/src/models/language.rs | 3 +- libft-api/src/models/locations.rs | 3 +- libft-api/src/models/prelude.rs | 24 ++ libft-api/src/models/project.rs | 3 +- libft-api/src/models/project_data.rs | 3 +- libft-api/src/models/project_session.rs | 101 +++--- libft-api/src/models/projects_users.rs | 3 +- libft-api/src/models/scale.rs | 3 +- libft-api/src/models/scale_teams.rs | 3 +- libft-api/src/models/team.rs | 9 +- libft-api/src/models/title.rs | 2 +- libft-api/src/models/user.rs | 3 +- libft-api/src/prelude.rs | 6 + 73 files changed, 353 insertions(+), 2018 deletions(-) delete mode 100644 libft-api/bin/campus_users.rs delete mode 100644 libft-api/bin/evaluation.rs delete mode 100644 libft-api/bin/exam_resubscribe.rs delete mode 100644 libft-api/bin/final_score.rs delete mode 100644 libft-api/bin/get_user_ext.rs delete mode 100644 libft-api/bin/journals.rs delete mode 100644 libft-api/bin/location_stat.rs delete mode 100644 libft-api/bin/locations.rs delete mode 100644 libft-api/bin/project_stats.rs delete mode 100644 libft-api/bin/teams.rs delete mode 100644 libft-api/bin/user_creation.rs delete mode 100644 libft-api/bin/user_subscribe.rs rename libft-api/{bin/blackholed.rs => examples/scroll.rs} (96%) delete mode 100644 libft-api/src/axum_support/mod.rs delete mode 100644 libft-api/src/models/common.rs create mode 100644 libft-api/src/models/datetime.rs create mode 100644 libft-api/src/models/prelude.rs create mode 100644 libft-api/src/prelude.rs diff --git a/libft-api/Cargo.toml b/libft-api/Cargo.toml index c1cdf09..83bfba0 100644 --- a/libft-api/Cargo.toml +++ b/libft-api/Cargo.toml @@ -10,53 +10,8 @@ exclude = ["src/main.rs"] [lib] path="src/lib.rs" -[[bin]] -name="project_stats" -path="bin/project_stats.rs" - -[[bin]] -name="locations" -path="bin/locations.rs" - -[[bin]] -name="blackholed" -path="bin/blackholed.rs" - -[[bin]] -name="journals" -path="bin/journals.rs" - -[[bin]] -name="campus_users" -path="bin/campus_users.rs" - -[[bin]] -name="user_creation" -path="bin/user_creation.rs" - -[[bin]] -name="evaluation" -path="bin/evaluation.rs" - -[[bin]] -name="user_subscribe" -path="bin/user_subscribe.rs" - -[[bin]] -name="teams" -path="bin/teams.rs" - -[[bin]] -name="get_user_ext" -path="bin/get_user_ext.rs" - -[[bin]] -name="exam_resubscribe" -path="bin/exam_resubscribe.rs" - -# [[bin]] -# name="location_stat" -# path="bin/location_stat.rs" +[[example]] +name = "scroll" [dependencies] serde = { version = "1.0.228", features = ["derive"] } diff --git a/libft-api/bin/campus_users.rs b/libft-api/bin/campus_users.rs deleted file mode 100644 index a2f0ba4..0000000 --- a/libft-api/bin/campus_users.rs +++ /dev/null @@ -1,203 +0,0 @@ -use chrono::Utc; -use libft_api::prelude::*; -use std::{io::Write, ops::ControlFlow, sync::Arc, time::Duration}; -use tokio::{sync::Semaphore, task::JoinSet, time::sleep}; -use tracing::info; - -#[tokio::main] -async fn main() -> Result<(), Box> { - tracing_subscriber::fmt::init(); - let thread_num = 4; - let permit = Arc::new(Semaphore::new(thread_num)); - // 3기 2차? - let ids = [ - 212531, 212530, 212529, 212528, 212527, 212526, 212525, 212524, 212523, 212522, 212521, - 212520, 212519, 212518, 212517, 212516, 212515, 212514, 212513, 212512, 212511, 212510, - 212509, 212508, 212507, 212506, 212505, 212504, 212503, 212502, 212501, 212500, 212499, - 212498, 212497, 212496, 212495, 212494, 212493, 212492, 212491, 212490, 212489, 212488, - 212487, 212486, 212485, 212484, 212483, 212482, 212481, 212480, 212479, 212478, 212477, - 212476, 212475, 212474, 212473, 212472, 212471, 212470, 212469, 212468, 212467, 212466, - 212465, 212464, 212463, 212462, 212461, 212460, 212459, 212458, 212457, 212456, 212455, - 212454, 212453, 212452, 212638, 212637, 212629, 212628, 212627, 212626, 212625, 212624, - 212623, 212622, 212621, 212620, 212619, 212618, 212617, 212616, 212615, 212614, 212613, - 212612, 212611, 212610, 212609, 212608, 212607, 212606, 212605, 212604, 212603, 212602, - 212601, 212600, 212599, 212598, 212597, 212596, 212595, 212594, 212593, 212592, 212591, - 212590, 212589, 212588, 212587, 212586, 212585, 212584, 212583, 212582, 212581, 212580, - 212579, 212578, 212577, 212576, 212575, 212574, 212573, 212572, 212571, 212570, 212569, - 212568, 212567, 212566, 212565, 212564, 212563, 212562, 212561, 212560, 212559, 212558, - 212557, 212556, 212555, 212554, 212553, 212552, 212551, 212550, 212549, 212548, 212547, - 212546, 212545, 212544, 212543, 212542, 212541, 212540, 212539, 212538, 212537, 212536, - 212535, 212534, 212533, 212532, - ] - .map(FtUserId::new); - - // let mut handles = JoinSet::new(); - // - // for mut page in 1..=thread_num { - // let permit = Arc::clone(&permit); - // handles.spawn(async move { - // let _permit = permit.acquire().await.unwrap(); - // let mut result = Vec::new(); - // loop { - // if let ControlFlow::Break(()) = get_users(&mut result, thread_num, &mut page).await - // { - // break result - // .into_iter() - // .filter_map(|user| user.id) - // .collect::>(); - // } - // } - // }); - // } - // - // let mut ids = Vec::new(); - // while let Some(Ok(res)) = handles.join_next().await { - // ids.extend(res); - // } - // info!("{:#?}", ids); - - let mut handles = JoinSet::new(); - - let mut result = Vec::new(); - for id in ids { - let permit = Arc::clone(&permit); - handles.spawn(async move { - let _permit = permit.acquire().await.unwrap(); - let mut result = Vec::new(); - let mut page = 1; - loop { - if let ControlFlow::Break(()) = - get_projects_users(&mut result, &id, &mut page).await - { - break result; - } - } - }); - } - - while let Some(Ok(res)) = handles.join_next().await { - result.extend(res); - info!("{}", result.len()); - } - - let file_path = format!("./progress_{}.csv", Utc::now().format("%Y-%m-%d_%H-%M-%S")); - - let mut file = std::fs::File::create(&file_path).expect("Failed to create output file"); - - file.write_all(&serde_json::to_vec(&result).unwrap()); - - /* - for projects_user in result { - let (id, login) = { - let user = projects_user - .user - .expect("projects_users always have user."); - ( - user.id.map(|id| id.to_string()).unwrap_or("".to_string()), - user.login - .map(|id| id.to_string()) - .unwrap_or("".to_string()), - ) - }; - writeln!( - file, - "{},{},{},{:?},{},{:?},{}", - id, - login, - projects_user.project.name, - projects_user.marked_at, - projects_user.created_at.value(), - projects_user.final_mark, - Utc::now() - ) - .expect("Failed to write record"); - } - */ - - println!("Output written to: {}", file_path); - Ok(()) -} - -async fn get_projects_users( - result: &mut Vec, - id: &FtUserId, - page: &mut i32, -) -> ControlFlow<()> { - let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) - .await - .unwrap(); - let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(token)); - let res = session - .users_id_projects_users( - FtApiUsersIdProjectsUsersRequest::new(*id) - .with_per_page(100) - .with_page(*page as u16), - ) - .await; - match res { - Ok(res) => { - if res.projects_users.is_empty() { - return ControlFlow::Break(()); - } - result.extend(res.projects_users); - *page += 1; - } - Err(FtClientError::RateLimitError(_)) => sleep(Duration::new(1, 42)).await, - Err(e) => { - eprintln!("other error: {e}"); - return ControlFlow::Break(()); - } - } - ControlFlow::Continue(()) -} - -// async fn get_users( -// result: &mut Vec, -// thread_num: usize, -// page: &mut usize, -// ) -> ControlFlow<()> { -// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) -// .await -// .unwrap(); -// let client = FtClient::new(FtClientReqwestConnector::new()); -// let session = Arc::new(client.open_sssion(token)); -// let res = session -// .users( -// FtApiUsersRequest::new() -// .with_per_page(100) -// .with_page(*page as u16) -// .with_range(vec![FtRangeOption::new( -// FtRangeField::CreatedAt, -// vec!["2025-1-1".to_string(), "2025-2-1".to_string()], -// )]) -// .with_filter(vec![ -// FtFilterOption::new( -// FtFilterField::PrimaryCampusId, -// vec![GYONGSAN.to_string()], -// ), -// FtFilterOption::new(FtFilterField::Kind, vec!["student".to_string()]), -// ]), -// ) -// .await; -// -// match res { -// Ok(res) => { -// if res.users.is_empty() { -// return ControlFlow::Break(()); -// } -// res.users -// .iter() -// .for_each(|user| println!("{:?}, {:?}", user.id, user.login)); -// result.extend(res.users); -// info!("{}", result.len()); -// *page += thread_num; -// } -// Err(FtClientError::RateLimitError(_)) => sleep(Duration::new(1, 42)).await, -// Err(e) => { -// eprintln!("other error: {e}"); -// return ControlFlow::Break(()); -// } -// } -// ControlFlow::Continue(()) -// } diff --git a/libft-api/bin/evaluation.rs b/libft-api/bin/evaluation.rs deleted file mode 100644 index 972959a..0000000 --- a/libft-api/bin/evaluation.rs +++ /dev/null @@ -1,287 +0,0 @@ -use std::{collections::HashMap, io::Write, ops::ControlFlow, sync::Arc, time::Duration}; - -use chrono::Utc; -use libft_api::{campus_id::*, prelude::*, FT_PISCINE_CURSUS_ID}; -use rvstruct::ValueStruct; -use tokio::{sync::Semaphore, task::JoinSet, time::sleep}; -use tracing::info; - -#[tokio::main] -async fn main() -> Result<(), Box> { - tracing_subscriber::fmt::init(); - let thread_num = 8; - let permit = Arc::new(Semaphore::new(thread_num)); - - let ids = [ - 212531, 212530, 212529, 212528, 212527, 212526, 212525, 212524, 212523, 212522, 212521, - 212520, 212519, 212518, 212517, 212516, 212515, 212514, 212513, 212512, 212511, 212510, - 212509, 212508, 212507, 212506, 212505, 212504, 212503, 212502, 212501, 212500, 212499, - 212498, 212497, 212496, 212495, 212494, 212493, 212492, 212491, 212490, 212489, 212488, - 212487, 212486, 212485, 212484, 212483, 212482, 212481, 212480, 212479, 212478, 212477, - 212476, 212475, 212474, 212473, 212472, 212471, 212470, 212469, 212468, 212467, 212466, - 212465, 212464, 212463, 212462, 212461, 212460, 212459, 212458, 212457, 212456, 212455, - 212454, 212453, 212452, 212638, 212637, 212629, 212628, 212627, 212626, 212625, 212624, - 212623, 212622, 212621, 212620, 212619, 212618, 212617, 212616, 212615, 212614, 212613, - 212612, 212611, 212610, 212609, 212608, 212607, 212606, 212605, 212604, 212603, 212602, - 212601, 212600, 212599, 212598, 212597, 212596, 212595, 212594, 212593, 212592, 212591, - 212590, 212589, 212588, 212587, 212586, 212585, 212584, 212583, 212582, 212581, 212580, - 212579, 212578, 212577, 212576, 212575, 212574, 212573, 212572, 212571, 212570, 212569, - 212568, 212567, 212566, 212565, 212564, 212563, 212562, 212561, 212560, 212559, 212558, - 212557, 212556, 212555, 212554, 212553, 212552, 212551, 212550, 212549, 212548, 212547, - 212546, 212545, 212544, 212543, 212542, 212541, 212540, 212539, 212538, 212537, 212536, - 212535, 212534, 212533, 212532, - ] - .map(FtUserId::new); - let mut handles = JoinSet::new(); - - for id in ids { - let permit = Arc::clone(&permit); - handles.spawn(async move { - let _permit = permit.acquire().await.unwrap(); - let mut result = HashMap::new(); - let mut page = 1; - loop { - if let ControlFlow::Break(()) = - get_evaluation_historics(&mut result, &id, &mut page).await - { - break result; - } - } - }); - } - - let mut historics_of_students = Vec::new(); - while let Some(Ok(res)) = handles.join_next().await { - historics_of_students.extend(res); - info!("{}", historics_of_students.len()); - } - - let file_path = format!( - "/Users/hdoo/works/gsia/codes/libft-api/libft-api/bin/piscine/third_cohort/first_round/evaluation_historics_{}.csv", - Utc::now().format("%Y-%m-%d_%H-%M-%S") - ); - - let mut file = std::fs::File::create(&file_path).expect("Failed to create output file"); - - file.write_all( - "id, created_at, reason, scale_team_id, sum, total, updated_at, intra_id\n".as_bytes(), - )?; - - for (intra_id, historics) in historics_of_students { - for history in historics { - writeln!( - file, - "{},{},{},{},{},{},{},{}", - history.id, - history.created_at.0.to_utc(), - history.reason, - history - .scale_team_id - .map(|team| team.value().to_string()) - .unwrap_or("".to_string()), - history.sum, - history.total, - history.updated_at.0.to_utc(), - intra_id - ) - .expect("Failed to write record"); - } - } - - let mut handles = JoinSet::new(); - - let mut scale_teams = Vec::new(); - for mut page in 1..=thread_num { - let permit = Arc::clone(&permit); - handles.spawn(async move { - let _permit = permit.acquire().await.unwrap(); - let mut result = Vec::new(); - loop { - if let ControlFlow::Break(()) = - get_scale_teams(&mut result, &mut page, thread_num).await - { - break result; - } - } - }); - } - - while let Some(Ok(res)) = handles.join_next().await { - scale_teams.extend(res); - info!("{}", scale_teams.len()); - } - - let file_path = format!( - "/Users/hdoo/works/gsia/codes/libft-api/libft-api/bin/piscine/third_cohort/first_round/scale_teams_{}.csv", - Utc::now().format("%Y-%m-%d_%H-%M-%S") - ); - - let mut file = std::fs::File::create(&file_path).expect("Failed to create output file"); - - file.write_all("project_idㅣscale_team_idㅣcreated_atㅣupdated_atㅣfinal_markㅣbegin_atㅣcorrectorㅣcorrectedsㅣfilled_atㅣtruantㅣteam.userㅣcommentㅣfeedback\n".as_bytes())?; - - for scale_team in scale_teams { - let corrector = match scale_team.corrector { - FtCorrector::User(ft_user) => { - ft_user.login.map(|login| login.0).unwrap_or("".to_string()) - } - FtCorrector::String(s) => s, - }; - let correcteds = match scale_team.correcteds { - FtCorrecteds::String(s) => s, - FtCorrecteds::Vec(vec) => vec - .into_iter() - .map(|user| user.login.map(|l| l.0).unwrap_or("".to_string())) - .collect::>() - .join(","), - }; - let begin_at = match scale_team.begin_at { - Some(date) => date.0.to_utc().to_string(), - None => "".to_string(), - }; - let filled_at = match scale_team.filled_at { - Some(date) => date.0.to_utc().to_string(), - None => "".to_string(), - }; - - let truant = match scale_team.truant { - Some(user) => user - .login - .map(|l| l.0.to_string()) - .unwrap_or("".to_string()), - None => "".to_string(), - }; - let (team_uesr, project_id) = match scale_team.team { - Some(team) => { - let user = team - .users - .map(|users| { - users - .into_iter() - .map(|user| { - user.login - .map(|l| l.0.to_string()) - .unwrap_or("".to_string()) - }) - .collect::>() - .join(",") - }) - .unwrap_or("".to_string()); - let project_id = team - .project_id - .map(|project_id| project_id.to_string()) - .unwrap_or("".to_string()); - (user, project_id) - } - None => ("".to_string(), "".to_string()), - }; - let final_mark = match scale_team.final_mark { - Some(final_mark) => final_mark.value().to_string(), - None => "".to_string(), - }; - writeln!( - file, - "{}ㅣ{}ㅣ{}ㅣ{}ㅣ{}ㅣ{}ㅣ{}ㅣ{}ㅣ{}ㅣ{}ㅣ{}ㅣ{:?}ㅣ{:?}", - project_id, - scale_team.id, - scale_team.created_at.0.to_utc(), - scale_team.updated_at.0.to_utc(), - final_mark, - begin_at, - corrector, - correcteds, - filled_at, - truant, - team_uesr, - scale_team.comment, - scale_team.feedback - ) - .expect("Failed to write record"); - } - - println!("Output written to: {}", file_path); - Ok(()) -} - -async fn get_evaluation_historics( - result: &mut HashMap>, - id: &FtUserId, - page: &mut i32, -) -> ControlFlow<()> { - let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) - .await - .unwrap(); - let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(token)); - let res = session - .users_id_correction_point_historics( - FtApiUsersIdCorrectionPointHistoricsRequest::new(*id) - .with_filter(vec![FtFilterOption::new( - FtFilterField::Sum, - vec!["-1".to_owned()], - )]) - .with_per_page(100) - .with_page(*page as u16), - ) - .await; - match res { - Ok(res) => { - if res.historics.is_empty() { - return ControlFlow::Break(()); - } - result.entry(*id).or_default().extend(res.historics); - *page += 1; - } - Err(FtClientError::RateLimitError(_)) => sleep(Duration::new(1, 42)).await, - Err(e) => { - eprintln!("other error: {e}"); - return ControlFlow::Break(()); - } - } - ControlFlow::Continue(()) -} - -async fn get_scale_teams( - result: &mut Vec, - page: &mut usize, - thread_num: usize, -) -> ControlFlow<()> { - let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) - .await - .unwrap(); - let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(token)); - let res = session - .scale_teams( - FtApiScaleTeamsRequest::new() - .with_range(vec![FtRangeOption::new( - FtRangeField::CreatedAt, - vec!["2025-1-19".to_string(), "2025-3-1".to_string()], - )]) - .with_filter(vec![ - FtFilterOption::new(FtFilterField::CampusId, vec![GYEONGSAN.to_string()]), - FtFilterOption::new( - FtFilterField::CursusId, - vec![FT_PISCINE_CURSUS_ID.to_string()], - ), - ]) - .with_per_page(100) - .with_page(*page as u16), - ) - .await; - match res { - Ok(res) => { - if res.scale_teams.is_empty() { - return ControlFlow::Break(()); - } - result.extend(res.scale_teams); - *page += thread_num; - } - Err(FtClientError::RateLimitError(_)) => sleep(Duration::new(1, 42)).await, - Err(e) => { - eprintln!("other error: {e}"); - return ControlFlow::Break(()); - } - } - ControlFlow::Continue(()) -} diff --git a/libft-api/bin/exam_resubscribe.rs b/libft-api/bin/exam_resubscribe.rs deleted file mode 100644 index 767102b..0000000 --- a/libft-api/bin/exam_resubscribe.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::sync::Arc; - -use libft_api::prelude::*; -use tokio::{sync::Semaphore, task::JoinSet}; - -#[derive(Debug)] -struct ExamSet { - exam_id: FtExamId, - project_id: FtProjectId, -} - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - let permit = Arc::new(Semaphore::new(1)); - - let target_users = []; - - let mut handles = JoinSet::new(); - - for id in target_users { - let permit = Arc::clone(&permit); - handles.spawn(async move { - let _permit = permit.acquire().await.unwrap(); - - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) - .await - .unwrap(); - let client = FtClient::new(FtClientReqwestConnector::new()); - - let session = client.open_session(token); - - let exam_res = session - .exams_users_post( - FtApiExamsUsersPostRequest::new(FtApiExamsUsersPostBody { - user_id: FtUserId::new(id), - }), - FtExamId::new(22331), - ) - .await; - }); - } - - while let Some(Ok(res)) = handles.join_next().await { - println!("{:?}", res); - } -} diff --git a/libft-api/bin/final_score.rs b/libft-api/bin/final_score.rs deleted file mode 100644 index 4191c14..0000000 --- a/libft-api/bin/final_score.rs +++ /dev/null @@ -1,245 +0,0 @@ -use std::{collections::HashMap, io::Write, ops::ControlFlow, sync::Arc, time::Duration}; - -use chrono::Utc; -use libft_api::{campus_id::*, prelude::*, FT_PISCINE_CURSUS_ID}; -use rvstruct::ValueStruct; -use tokio::{sync::Semaphore, task::JoinSet, time::sleep}; -use tracing::info; - -#[tokio::main] -async fn main() -> Result<(), Box> { - tracing_subscriber::fmt::init(); - let thread_num = 8; - let permit = Arc::new(Semaphore::new(thread_num)); - - let ids = [ - 212531, 212530, 212529, 212528, 212527, 212526, 212525, 212524, 212523, 212522, 212521, - 212520, 212519, 212518, 212517, 212516, 212515, 212514, 212513, 212512, 212511, 212510, - 212509, 212508, 212507, 212506, 212505, 212504, 212503, 212502, 212501, 212500, 212499, - 212498, 212497, 212496, 212495, 212494, 212493, 212492, 212491, 212490, 212489, 212488, - 212487, 212486, 212485, 212484, 212483, 212482, 212481, 212480, 212479, 212478, 212477, - 212476, 212475, 212474, 212473, 212472, 212471, 212470, 212469, 212468, 212467, 212466, - 212465, 212464, 212463, 212462, 212461, 212460, 212459, 212458, 212457, 212456, 212455, - 212454, 212453, 212452, 212638, 212637, 212629, 212628, 212627, 212626, 212625, 212624, - 212623, 212622, 212621, 212620, 212619, 212618, 212617, 212616, 212615, 212614, 212613, - 212612, 212611, 212610, 212609, 212608, 212607, 212606, 212605, 212604, 212603, 212602, - 212601, 212600, 212599, 212598, 212597, 212596, 212595, 212594, 212593, 212592, 212591, - 212590, 212589, 212588, 212587, 212586, 212585, 212584, 212583, 212582, 212581, 212580, - 212579, 212578, 212577, 212576, 212575, 212574, 212573, 212572, 212571, 212570, 212569, - 212568, 212567, 212566, 212565, 212564, 212563, 212562, 212561, 212560, 212559, 212558, - 212557, 212556, 212555, 212554, 212553, 212552, 212551, 212550, 212549, 212548, 212547, - 212546, 212545, 212544, 212543, 212542, 212541, 212540, 212539, 212538, 212537, 212536, - 212535, 212534, 212533, 212532, - ] - .map(FtUserId::new); - - let mut teams_task = JoinSet::new(); - for id in ids { - let permit = Arc::clone(&permit); - teams_task.spawn(async move { - let _permit = permit.acquire().await.unwrap(); - let mut result = Vec::new(); - let mut page = 1; - loop { - if let ControlFlow::Break(()) = get_user_id_teams(&mut result, &mut page, id).await - { - break result; - } - } - }); - } - - let mut teams_by_id = HashMap::new(); - while let Some(Ok(res)) = teams_task.join_next().await { - for team in res { - tracing::info!("{}", team.id.0); - teams_by_id.entry(team.id).or_insert(team); - } - } - - let mut handles = JoinSet::new(); - - let mut scale_teams = Vec::new(); - for mut page in 1..=thread_num { - let permit = Arc::clone(&permit); - handles.spawn(async move { - let _permit = permit.acquire().await.unwrap(); - let mut result = Vec::new(); - loop { - if let ControlFlow::Break(()) = - get_scale_teams(&mut result, &mut page, thread_num).await - { - break result; - } - } - }); - } - - while let Some(Ok(res)) = handles.join_next().await { - scale_teams.extend(res); - } - - let file_path = format!( - "/Users/hdoo/works/gsia/codes/gs_stat_bins/data/piscine/third_cohort/first_round/final_mark{}.csv", - Utc::now().format("%Y-%m-%d_%H-%M-%S") - ); - - let mut file = std::fs::File::create(&file_path).expect("Failed to create output file"); - - file.write_all( - "project_id|evaluator|evaluated|feedback_detail_score_avg|evaluated_mark|mulinette_mark\n" - .as_bytes(), - )?; - - for scale_team in scale_teams { - let corrector = match scale_team.corrector { - FtCorrector::User(ft_user) => { - ft_user.login.map(|login| login.0).unwrap_or("".to_string()) - } - FtCorrector::String(s) => s, - }; - let correcteds = match scale_team.correcteds { - FtCorrecteds::String(s) => vec![s], - FtCorrecteds::Vec(ft_users) => ft_users - .into_iter() - .map(|user| user.login.map(|login| login.0).unwrap_or("".to_string())) - .collect::>(), - }; - - let feedback_detail_score_avg = match scale_team.feedbacks { - Some(feedbacks) if !feedbacks.is_empty() => { - let count = feedbacks.len(); - let total: i32 = feedbacks - .into_iter() - .map(|f| f.rating.map(|r| r.into_value()).unwrap_or(0)) - .sum(); - Some(total as f32 / count as f32) - } - _ => None, - }; - - let evaluated_mark = scale_team.final_mark; - - let (project_id, moulinette_mark) = match scale_team.team { - Some(team) => match teams_by_id.remove(&team.id) { - Some(target_team) => { - let moulinette_mark = match target_team.teams_uploads { - Some(teams_uploads) => teams_uploads - .into_iter() - .map(|team| team.final_mark) - .max_by(|a, b| a.cmp(b)), - None => None, - }; - let project_id = target_team.project_id; - (project_id, moulinette_mark) - } - None => (None, None), - }, - None => (None, None), - }; - - for corrected in correcteds { - writeln!( - file, - "{:?}|{:?}|{}|{:?}|{:?}|{:?}", - project_id, - corrector, - corrected, - feedback_detail_score_avg, - evaluated_mark, - moulinette_mark - ) - .expect("Failed to write record"); - } - } - - println!("Output written to: {}", file_path); - Ok(()) -} - -async fn get_user_id_teams( - result: &mut Vec, - page: &mut u16, - id: FtUserId, -) -> ControlFlow<()> { - let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) - .await - .unwrap(); - let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(token)); - let res = session - .users_id_teams( - FtApiUsersIdTeamsRequest::new(id) - .with_per_page(100) - .with_page(*page), - ) - .await; - - match res { - Ok(res) => { - if res.teams.is_empty() { - ControlFlow::Break(()) - } else { - result.extend(res.teams); - *page += 1; - - ControlFlow::Continue(()) - } - } - Err(e) => match e { - FtClientError::RateLimitError(ft_rate_limit_error) => { - sleep(Duration::new(1, 42)).await; - ControlFlow::Continue(()) - } - _ => { - tracing::error!("{:?}", e); - ControlFlow::Break(()) - } - }, - } -} - -async fn get_scale_teams( - result: &mut Vec, - page: &mut usize, - thread_num: usize, -) -> ControlFlow<()> { - let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) - .await - .unwrap(); - let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(token)); - let res = session - .scale_teams( - FtApiScaleTeamsRequest::new() - .with_range(vec![FtRangeOption::new( - FtRangeField::CreatedAt, - vec!["2025-1-19".to_string(), "2025-3-1".to_string()], - )]) - .with_filter(vec![ - FtFilterOption::new(FtFilterField::CampusId, vec![GYEONGSAN.to_string()]), - FtFilterOption::new( - FtFilterField::CursusId, - vec![FT_PISCINE_CURSUS_ID.to_string()], - ), - ]) - .with_per_page(100) - .with_page(*page as u16), - ) - .await; - match res { - Ok(res) => { - if res.scale_teams.is_empty() { - return ControlFlow::Break(()); - } - result.extend(res.scale_teams); - *page += thread_num; - } - Err(FtClientError::RateLimitError(_)) => sleep(Duration::new(1, 42)).await, - Err(e) => { - eprintln!("other error: {e}"); - return ControlFlow::Break(()); - } - } - ControlFlow::Continue(()) -} diff --git a/libft-api/bin/get_user_ext.rs b/libft-api/bin/get_user_ext.rs deleted file mode 100644 index 976548a..0000000 --- a/libft-api/bin/get_user_ext.rs +++ /dev/null @@ -1,105 +0,0 @@ -use std::{ops::ControlFlow, sync::Arc, time::Duration}; - -use libft_api::prelude::*; -use tokio::{sync::Semaphore, task::JoinSet, time::sleep}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - tracing_subscriber::fmt::init(); - let thread_num = 4; - let permit = Arc::new(Semaphore::new(thread_num)); - - let ids = [ - 172410, 197482, 190887, 172305, 172353, 197422, 197429, 190783, 197456, 190848, - 190815, - // 172394, 174189, 190846, 174084, 190820, 172357, 190800, 197497, 172418, 172352, 172349, - // 197528, 190909, 174169, 197496, 174101, 197397, 174128, 174104, 174127, 174112, 197454, - // 174184, 197455, 197495, 197484, 172327, 197507, 190797, 197498, 197444, 174097, 190898, - // 172325, 174113, 172307, 174153, 172346, 172356, 190862, 197402, 174156, 190839, 197518, - // 197483, 174185, 174152, 174145, 197459, 197504, 174131, 190847, 197523, 197521, 197511, - // 197406, 197403, 172364, 197486, 172362, 190795, 190802, 197525, 174188, 197457, 190806, - // 174089, 174135, 174129, 197400, 190817, 174081, 174147, 197489, 172308, 197463, 190913, - // 197437, 197605, 172400, 197516, 190885, 197449, 174161, 174186, 174110, 197439, 190838, - // 172329, 190870, 172370, 174085, 174111, 190849, 172416, 190876, 197606, 197519, 174138, - // 174149, 172413, 190845, 197527, 190895, 174168, 174137, 172414, 190832, 197537, 172375, - // 197441, 174151, 190808, 197472, 172390, 197520, 190843, 172348, 172392, 190896, 172389, - // 197448, 197417, 174139, 190907, 172335, 174095, 197494, 190910, 190816, 197445, 197541, - // 174130, 174150, 190823, 197467, 190821, 190784, 190926, 174142, 197421, 197420, 174093, - // 197435, 197453, 197530, 174102, 190886, 190861, 174103, 197447, 174123, 174099, 174096, - // 174178, 172350, 197543, 197474, 174117, 172402, 172324, 172367, 190790, 197490, 190803, - // 174133, 197529, 190855, 197428, 197542, 197499, 190837, 190865, 174154, 197547, 197501, - // 190812, 190818, 197418, 172310, 190836, 197540, 172342, 190869, 197407, 197533, 190911, - // 197487, 172318, 190903, 190831, 190937, 174109, 174115, 190854, 190866, 174181, 190813, - // 174091, 172361, 172344, 190785, 197505, 197532, 197531, 172309, 172323, 174157, 197514, - // 190791, 174105, 190810, 174183, 190794, 197395, 197458, 197481, 190905, 197412, 174086, - // 197548, 197536, 172351, 190829, 174165, 197503, 172385, 172404, 197526, 172365, 197399, - // 197538, 172401, 197409, 174119, 174083, 174177, 197539, 197432, 190874, 190844, 172319, - // 174141, 190786, 174087, 172378, 190883, 172396, 174160, 190884, 174092, 174132, 197442, - // 197398, 174190, 190853, 172330, 197413, 197469, 174094, 172366, 172368, 172322, 197427, - // 174120, 197408, 197425, 172360, 197434, 172399, 173488, 151095, 159380, 84509, 212592, - // 212527, 212590, 212600, 212458, 212489, 212601, 212464, 212628, 212493, 212582, 212591, - // 212469, 212456, 212608, 212615, 212498, 212625, 212562, 212512, 212612, 212468, 212571, - // 212471, 212606, 212560, 212525, 212501, 212572, 212587, 212452, 212460, 212496, 212557, - // 212476, 212529, 212534, 212586, 212543, 212602, 212567, 212524, 212477, 212481, 212561, - // 212473, 212495, 212522, 212570, 212517, 212538, 212539, 212459, 212462, 212544, 212482, - // 212558, 212559, 212457, 212472, 212548, 212553, 212609, 212583, 212535, 212518, 212467, - // 212521, 212545, 212533, 212568, 212595, 212505, 212465, 212503, 212499, 212514, 212624, - // 212466, 212454, 212549, 212540, 212487, 212555, 212497, 212556, 212623, 212494, 212530, - // 212581, 212502, 212510, 212546, 212579, - ] - .map(FtUserId::new); - - let mut users_task = JoinSet::new(); - for id in ids { - let permit = Arc::clone(&permit); - users_task.spawn(async move { - let _permit = permit.acquire().await.unwrap(); - loop { - if let ControlFlow::Break(result) = get_user_info(id).await { - break result; - } - } - }); - } - - let mut users = Vec::new(); - - while let Some(Ok(Some(user))) = users_task.join_next().await { - users.push(user); - } - - if let Err(e) = std::fs::write( - "users.json", - serde_json::to_string_pretty(&users).expect("restore from des"), - ) { - tracing::error!("{e}"); - } - - Ok(()) -} - -async fn get_user_info(id: FtUserId) -> ControlFlow> { - let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) - .await - .unwrap(); - let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(token)); - let res = session - .users_id(FtApiUsersIdRequest::new(FtUserIdentifier::UserId(id))) - .await; - - match res { - Ok(res) => ControlFlow::Break(Some(res.user)), - Err(e) => match e { - FtClientError::RateLimitError(ft_rate_limit_error) => { - tracing::error!("{ft_rate_limit_error}"); - sleep(Duration::new(1, 42)).await; - ControlFlow::Continue(()) - } - _ => { - tracing::error!("{:?}", e); - ControlFlow::Break(None) - } - }, - } -} diff --git a/libft-api/bin/journals.rs b/libft-api/bin/journals.rs deleted file mode 100644 index 30bc20c..0000000 --- a/libft-api/bin/journals.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::io::Write; - -use futures::FutureExt; -use libft_api::{campus_id::GYEONGSAN, prelude::*}; - -#[tokio::main] -async fn main() { - let client = FtClient::new(FtClientReqwestConnector::new()); - - let req: ReqFn = |session, page| { - async move { - session - .campus_id_journals( - FtApiCampusIdJournalsRequest::new( - FtCampusId(GYEONGSAN), - "2025-10-1".to_string(), - "2025-10-3".to_string(), - ) - .with_page(page) - .with_per_page(100), - ) - .await - } - .boxed() - }; - - let mut result = Vec::new(); - for i in 1..=2 { - result.push(scroller(&client, 2, i, req).await); - } - - let mut file = std::fs::File::open("temp.json").unwrap(); - let _ = file.write_all(serde_json::to_string_pretty(&result).unwrap().as_bytes()); -} diff --git a/libft-api/bin/location_stat.rs b/libft-api/bin/location_stat.rs deleted file mode 100644 index d20e92c..0000000 --- a/libft-api/bin/location_stat.rs +++ /dev/null @@ -1,133 +0,0 @@ -// use std::{ -// collections::HashMap, -// io::Write, -// ops::Deref, -// sync::Arc, -// }; -// -// use chrono::Utc; -// use libft_api::prelude::*; -// use tokio::{sync::Semaphore, task::JoinSet}; -// use tracing::{debug, info}; -// -// #[tokio::main] -// async fn main() -> Result<(), Box> { -// tracing_subscriber::fmt::init(); -// let thread_num = 7; -// let permit = Arc::new(Semaphore::new(thread_num)); -// -// // 3rd cohort piscine first round -// // let ids = [ -// // 212531, 212530, 212529, 212528, 212527, 212526, 212525, 212524, 212523, 212522, 212521, -// // 212520, 212519, 212518, 212517, 212516, 212515, 212514, 212513, 212512, 212511, 212510, -// // 212509, 212508, 212507, 212506, 212505, 212504, 212503, 212502, 212501, 212500, 212499, -// // 212498, 212497, 212496, 212495, 212494, 212493, 212492, 212491, 212490, 212489, 212488, -// // 212487, 212486, 212485, 212484, 212483, 212482, 212481, 212480, 212479, 212478, 212477, -// // 212476, 212475, 212474, 212473, 212472, 212471, 212470, 212469, 212468, 212467, 212466, -// // 212465, 212464, 212463, 212462, 212461, 212460, 212459, 212458, 212457, 212456, 212455, -// // 212454, 212453, 212452, 212638, 212637, 212629, 212628, 212627, 212626, 212625, 212624, -// // 212623, 212622, 212621, 212620, 212619, 212618, 212617, 212616, 212615, 212614, 212613, -// // 212612, 212611, 212610, 212609, 212608, 212607, 212606, 212605, 212604, 212603, 212602, -// // 212601, 212600, 212599, 212598, 212597, 212596, 212595, 212594, 212593, 212592, 212591, -// // 212590, 212589, 212588, 212587, 212586, 212585, 212584, 212583, 212582, 212581, 212580, -// // 212579, 212578, 212577, 212576, 212575, 212574, 212573, 212572, 212571, 212570, 212569, -// // 212568, 212567, 212566, 212565, 212564, 212563, 212562, 212561, 212560, 212559, 212558, -// // 212557, 212556, 212555, 212554, 212553, 212552, 212551, 212550, 212549, 212548, 212547, -// // 212546, 212545, 212544, 212543, 212542, 212541, 212540, 212539, 212538, 212537, 212536, -// // 212535, 212534, 212533, 212532, -// // ] -// // .map(FtUserId::new); -// -// let ids = [ -// 172410, 197482, 190887, 172305, 172353, 197422, 197429, 190783, 197456, 190848, 190815, -// 172394, 174189, 190846, 174084, 190820, 172357, 190800, 197497, 172418, 172352, 172349, -// 197528, 190909, 174169, 197496, 174101, 197397, 174128, 174104, 174127, 174112, 197454, -// 174184, 197455, 197495, 197484, 172327, 197507, 190797, 197498, 197444, 174097, 190898, -// 172325, 174113, 172307, 174153, 172346, 172356, 190862, 197402, 174156, 190839, 197518, -// 197483, 174185, 174152, 174145, 197459, 197504, 174131, 190847, 197523, 197521, 197511, -// 197406, 197403, 172364, 197486, 172362, 190795, 190802, 197525, 174188, 197457, 190806, -// 174089, 174135, 174129, 197400, 190817, 174081, 174147, 197489, 172308, 197463, 190913, -// 197437, 197605, 172400, 197516, 190885, 197449, 174161, 174186, 174110, 197439, 190838, -// 172329, 190870, 172370, 174085, 174111, 190849, 172416, 190876, 197606, 197519, 174138, -// 174149, 172413, 190845, 197527, 190895, 174168, 174137, 172414, 190832, 197537, 172375, -// 197441, 174151, 190808, 197472, 172390, 197520, 190843, 172348, 172392, 190896, 172389, -// 197448, 197417, 174139, 190907, 172335, 174095, 197494, 190910, 190816, 197445, 197541, -// 174130, 174150, 190823, 197467, 190821, 190784, 190926, 174142, 197421, 197420, 174093, -// 197435, 197453, 197530, 174102, 190886, 190861, 174103, 197447, 174123, 174099, 174096, -// 174178, 172350, 197543, 197474, 174117, 172402, 172324, 172367, 190790, 197490, 190803, -// 174133, 197529, 190855, 197428, 197542, 197499, 190837, 190865, 174154, 197547, 197501, -// 190812, 190818, 197418, 172310, 190836, 197540, 172342, 190869, 197407, 197533, 190911, -// 197487, 172318, 190903, 190831, 190937, 174109, 174115, 190854, 190866, 174181, 190813, -// 174091, 172361, 172344, 190785, 197505, 197532, 197531, 172309, 172323, 174157, 197514, -// 190791, 174105, 190810, 174183, 190794, 197395, 197458, 197481, 190905, 197412, 174086, -// 197548, 197536, 172351, 190829, 174165, 197503, 172385, 172404, 197526, 172365, 197399, -// 197538, 172401, 197409, 174119, 174083, 174177, 197539, 197432, 190874, 190844, 172319, -// 174141, 190786, 174087, 172378, 190883, 172396, 174160, 190884, 174092, 174132, 197442, -// 197398, 174190, 190853, 172330, 197413, 197469, 174094, 172366, 172368, 172322, 197427, -// 174120, 197408, 197425, 172360, 197434, 172399, 173488, -// ] -// .map(FtUserId::new); -// -// Ok(()) -// } -// -// async fn save_location_stat( -// ids: Arc>, -// permit: Arc, -// ) -> Result<(), Box> { -// let mut handles = JoinSet::new(); -// -// for id in ids.clone().deref().clone() { -// let permit = Arc::clone(&permit); -// handles.spawn(async move { -// let _permit = permit.acquire().await.unwrap(); -// loop { -// if let Ok(res) = get_location_stat(&id).await { -// debug!("{id}: {:?}", res.stats.len()); -// break (id, res); -// } -// } -// }); -// } -// -// let mut location_stats = HashMap::new(); -// while let Some(Ok((id, res))) = handles.join_next().await { -// location_stats.entry(id).or_insert(res); -// info!("{}", location_stats.len()); -// } -// -// let file_path = format!( -// "/Users/hdoo/works/gsia/codes/libft-api/libft-api/bin/piscine/third_cohort/first_round/location_stats_{}.csv", -// Utc::now().format("%Y-%m-%d_%H-%M-%S") -// ); -// -// let mut file = std::fs::File::create(&file_path).expect("Failed to create output file"); -// -// file.write_all("intra_id,date,time\n".as_bytes())?; -// -// for (intra_id, location_stat) in location_stats { -// for (date, time) in location_stat.stats { -// writeln!(file, "{},{},{}", intra_id, date, time).expect("Failed to write record"); -// } -// } -// -// println!("Output written to: {}", file_path); -// Ok(()) -// } -// -// async fn get_location_stat(id: &FtUserId) -> ClientResult { -// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) -// .await -// .unwrap(); -// let client = FtClient::new(FtClientReqwestConnector::new()); -// let session = Arc::new(client.open_session(token)); -// let res = session -// .users_id_locations_stats( -// FtApiUsersIdLocationsStatsRequest::new(id) -// .with_begin_at("2024-1-1".parse().unwrap()) -// .with_end_at("2025-3-1".parse().unwrap()), -// ) -// .await; -// debug!("{:?}", res); -// res -// } diff --git a/libft-api/bin/locations.rs b/libft-api/bin/locations.rs deleted file mode 100644 index 1abb5c8..0000000 --- a/libft-api/bin/locations.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::time::Duration; - -use libft_api::{campus_id::GYEONGSAN, prelude::*}; -use tokio::time::sleep; - -#[tokio::main] -async fn main() { - let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) - .await - .unwrap(); - - let client = FtClient::new(FtClientReqwestConnector::new()); - let session = client.open_session(token); - - let mut page = 1; - loop { - let res = session - .campus_id_locations( - FtApiCampusIdLocationsRequest::new(FtCampusId::new(GYEONGSAN)) - .with_page(page) - .with_per_page(100) - .with_range(vec![FtRangeOption::new( - FtRangeField::BeginAt, - vec!["2024-10-1".to_owned(), "2025-1-1".to_owned()], - )]), - ) - .await; - - println!("host,date"); - match res { - Ok(res) => { - if res.location.is_empty() { - break; - } - for ele in res.location { - println!("{},{}", ele.host, ele.begin_at.0); - } - page += 1; - } - Err(FtClientError::RateLimitError(_)) => { - eprintln!("rate limit, try again."); - sleep(Duration::new(1, 42)).await - } - Err(e) => { - eprintln!("other error: {e}"); - break; - } - } - } -} diff --git a/libft-api/bin/project_stats.rs b/libft-api/bin/project_stats.rs deleted file mode 100644 index 04cafdb..0000000 --- a/libft-api/bin/project_stats.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::time::Duration; - -use libft_api::{campus_id::GYEONGSAN, prelude::*, EXAM_RANK_03, MINISHELL, PHILOSOPHERS}; -use tokio::time::sleep; - -#[tokio::main] -async fn main() { - let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) - .await - .unwrap(); - - let client = FtClient::new(FtClientReqwestConnector::new()); - let session = client.open_session(token); - - let mut page = 1; - println!("login|project|final_mark|retriable_at|status|team_mate"); - loop { - let res = session - .projects_uesrs( - FtApiProjectsUsersRequest::new() - .with_page(page) - .with_per_page(100) - .with_filter(vec![ - FtFilterOption::new( - FtFilterField::ProjectId, - vec![ - EXAM_RANK_03.to_string(), - PHILOSOPHERS.to_string(), - MINISHELL.to_string(), - ], - ), - FtFilterOption::new(FtFilterField::Campus, vec![GYEONGSAN.to_string()]), - ]), - ) - .await; - - match res { - Ok(res) => { - if res.projects_users.is_empty() { - break; - } - for ele in res.projects_users { - let team_mate = match ele.teams { - Some(mut teams) => match teams.pop() { - Some(team) => match team.users { - Some(users) => users - .into_iter() - .map(|user| user.login) - .collect::>>(), - None => vec![None], - }, - None => vec![None], - }, - None => vec![None], - }; - - println!( - "{:?}|{}|{:?}|{:?}|{}|{:?}", - ele.user.expect("projects_users always have FtUser").login, - ele.project.name, - ele.final_mark, - ele.retriable_at, - ele.status, - team_mate - ); - } - page += 1; - } - Err(FtClientError::RateLimitError(_)) => { - eprintln!("rate limit, try again."); - sleep(Duration::new(1, 42)).await - } - Err(e) => { - eprintln!("other error: {e}"); - break; - } - } - } -} diff --git a/libft-api/bin/teams.rs b/libft-api/bin/teams.rs deleted file mode 100644 index e6f1d8b..0000000 --- a/libft-api/bin/teams.rs +++ /dev/null @@ -1,202 +0,0 @@ -use std::sync::Arc; - -use chrono::{TimeDelta, TimeZone, Utc}; -use ft_project_session_ids::c_piscine::C_PISCINE_RUSH_02; -use libft_api::{campus_id::*, prelude::*, FT_PISCINE_CURSUS_ID}; -use rvstruct::ValueStruct; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let begin_at = Utc.with_ymd_and_hms(2025, 1, 28, 7, 0, 0).unwrap(); - let body = vec![ - FtApiScaleTeamsMultipleCreateBody { - begin_at: FtDateTimeUtc::new(begin_at.clone()), - user_id: FtUserId::new(174094), - team_id: FtTeamId::new(6298862), - }, - FtApiScaleTeamsMultipleCreateBody { - begin_at: FtDateTimeUtc::new(begin_at), - user_id: FtUserId::new(172309), - team_id: FtTeamId::new(6298846), - }, - ]; - let res = post_scale_team(body).await.unwrap(); - println!("{res:?}"); - - Ok(()) -} - -async fn temp() { - tracing_subscriber::fmt::init(); - - let evaluators = [174094, 172309].map(FtUserId::new); - - let project_teams = get_project_teams( - FtProjectSessionId::new(C_PISCINE_RUSH_02), - "2025-1-20".to_string(), - "2025-2-15".to_string(), - ) - .await - .teams; - - project_teams - .iter() - .for_each(|teams| println!("{}|{:?}", teams.id, teams.users)); - - let begin_at = Utc.with_ymd_and_hms(2025, 1, 28, 5, 0, 0).unwrap(); - let mut bodys = Vec::new(); - for (i, project_team) in project_teams.iter().enumerate() { - let evaluator = evaluators.get(i % evaluators.len()).unwrap().clone(); - let iter = i / evaluators.len(); - let begin_at = begin_at - .checked_add_signed(TimeDelta::new(iter as i64 * 60 * 60 * 1, 0).unwrap()) - .map(FtDateTimeUtc::new) - .unwrap(); - bodys.push(FtApiScaleTeamsMultipleCreateBody { - begin_at, - user_id: evaluator, - team_id: project_team.id.clone(), - }); - } - - for ele in bodys.iter() { - println!("{},{},{}", ele.user_id, ele.team_id, ele.begin_at.value()); - } - - // let res = post_scale_team(bodys).await.unwrap(); - // - // let file_path = format!( - // "/Users/hdoo/works/gsia/libft-api/libft-api/bin/piscine/third_cohort/first_round/rush_teams_{}.csv", - // Utc::now().format("%Y-%m-%d_%H-%M-%S") - // ); - // - // let mut file = std::fs::File::create(&file_path).expect("Failed to create output file"); - // - // file.write_all("project_idㅣscale_team_idㅣcreated_atㅣupdated_atㅣfinal_markㅣbegin_atㅣcorrectorㅣcorrectedsㅣfilled_atㅣtruantㅣteam.userㅣcommentㅣfeedback\n".as_bytes())?; - // - // for scale_team in res.scale_teams { - // let corrector = match scale_team.corrector { - // FtCorrector::User(ft_user) => { - // ft_user.login.map(|login| login.0).unwrap_or("".to_string()) - // } - // FtCorrector::String(s) => s, - // }; - // let correcteds = match scale_team.correcteds { - // FtCorrecteds::String(s) => s, - // FtCorrecteds::Vec(vec) => vec - // .into_iter() - // .map(|user| user.login.map(|l| l.0).unwrap_or("".to_string())) - // .collect::>() - // .join(","), - // }; - // let begin_at = match scale_team.begin_at { - // Some(date) => date.0.to_utc().to_string(), - // None => "".to_string(), - // }; - // let filled_at = match scale_team.filled_at { - // Some(date) => date.0.to_utc().to_string(), - // None => "".to_string(), - // }; - // - // let truant = match scale_team.truant { - // Some(user) => user - // .login - // .map(|l| l.0.to_string()) - // .unwrap_or("".to_string()), - // None => "".to_string(), - // }; - // let (team_uesr, project_id) = match scale_team.team { - // Some(team) => { - // let user = team - // .users - // .map(|users| { - // users - // .into_iter() - // .map(|user| { - // user.login - // .map(|l| l.0.to_string()) - // .unwrap_or("".to_string()) - // }) - // .collect::>() - // .join(",") - // }) - // .unwrap_or("".to_string()); - // let project_id = team - // .project_id - // .map(|project_id| project_id.to_string()) - // .unwrap_or("".to_string()); - // (user, project_id) - // } - // None => ("".to_string(), "".to_string()), - // }; - // let final_mark = match scale_team.final_mark { - // Some(final_mark) => final_mark.value().to_string(), - // None => "".to_string(), - // }; - // writeln!( - // file, - // "{}ㅣ{}ㅣ{}ㅣ{}ㅣ{}ㅣ{}ㅣ{}ㅣ{}ㅣ{}ㅣ{}ㅣ{}ㅣ{:?}ㅣ{:?}", - // project_id, - // scale_team.id, - // scale_team.created_at.0.to_utc(), - // scale_team.updated_at.0.to_utc(), - // final_mark, - // begin_at, - // corrector, - // correcteds, - // filled_at, - // truant, - // team_uesr, - // scale_team.comment, - // scale_team.feedback - // ) - // .expect("Failed to write record"); - // } - // - // println!("Output written to: {}", file_path); -} - -async fn post_scale_team( - bodys: Vec, -) -> Result { - let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) - .await - .unwrap(); - let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(token)); - - session - .scale_teams_multiple_create_post(FtApiScaleTeamsMultipleCreateRequest::new(bodys)) - .await -} - -async fn get_project_teams( - project_session_id: FtProjectSessionId, - begin_at: String, - end_at: String, -) -> FtApiProjectSessionsTeamsResponse { - let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) - .await - .unwrap(); - let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(token)); - let res = session - .project_sessions_id_teams( - FtApiProjectSessionsTeamsRequest::new(project_session_id) - .with_per_page(100) - .with_filter(vec![ - FtFilterOption::new(FtFilterField::Campus, vec![GYEONGSAN.to_string()]), - FtFilterOption::new( - FtFilterField::Cursus, - vec![FT_PISCINE_CURSUS_ID.to_string()], - ), - ]) - .with_range(vec![FtRangeOption::new( - FtRangeField::CreatedAt, - vec![begin_at, end_at], - )]), - ) - .await; - - res.unwrap() -} diff --git a/libft-api/bin/user_creation.rs b/libft-api/bin/user_creation.rs deleted file mode 100644 index ccc5915..0000000 --- a/libft-api/bin/user_creation.rs +++ /dev/null @@ -1,125 +0,0 @@ -use chrono::{TimeDelta, Utc}; -use libft_api::{campus_id::*, prelude::*, FT_GROUP_ID_TEST_ACCOUNT, FT_PISCINE_CURSUS_ID}; -use std::sync::Arc; -use tokio::{sync::Semaphore, task::JoinSet}; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - let permit = Arc::new(Semaphore::new(7)); - let mut handles = JoinSet::new(); - let test_user_ids = 3..10; - - for id in test_user_ids { - let permit = Arc::clone(&permit); - handles.spawn(async move { - let _permit = permit.acquire().await.unwrap(); - post_users(id).await - }); - } - - let mut ids = Vec::new(); - while let Some(Ok(res)) = handles.join_next().await { - ids.extend(res); - } - - let newly_created_users = ids - .into_iter() - .filter_map(|res| res.user.id) - .collect::>(); - - let mut handles = JoinSet::new(); - - let mut result = Vec::new(); - for id in newly_created_users.clone() { - let permit = Arc::clone(&permit); - handles.spawn(async move { - let _permit = permit.acquire().await.unwrap(); - assign_group(id).await - }); - } - - while let Some(Ok(res)) = handles.join_next().await { - result.extend(res); - } - println!("success: {}", result.len()); - - let mut handles = JoinSet::new(); - - let mut result = Vec::new(); - for id in newly_created_users { - let permit = Arc::clone(&permit); - handles.spawn(async move { - let _permit = permit.acquire().await.unwrap(); - add_cursus(id, FtCursusId::new(FT_PISCINE_CURSUS_ID)).await - }); - } - - while let Some(Ok(res)) = handles.join_next().await { - result.extend(res); - } - println!("success: {}", result.len()); -} - -async fn assign_group(id: FtUserId) -> Result { - let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) - .await - .unwrap(); - let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(token)); - - session - .groups_users_post(FtApiGroupsUsersPostRequest::new(FtApiGroupsUsersPostBody { - group_id: FtGroupId::new(FT_GROUP_ID_TEST_ACCOUNT), - user_id: id, - })) - .await -} - -async fn add_cursus( - id: FtUserId, - cursus: FtCursusId, -) -> Result { - let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) - .await - .unwrap(); - let client = FtClient::new(FtClientReqwestConnector::new()); - let session = Arc::new(client.open_session(token)); - - session - .users_id_cursus_users_post(FtApiUsersIdCursusUsersPostRequest::new( - FtApiCursusUsersBody { - cursus_id: cursus, - user_id: id, - begin_at: Utc::now() - .checked_add_signed(TimeDelta::new(60, 0).unwrap()) - .unwrap() - .to_string(), - has_coalition: false, - }, - )) - .await -} - -async fn post_users(id: usize) -> Result { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) - .await - .unwrap(); - let client = FtClient::new(FtClientReqwestConnector::new()); - - let session = client.open_session(token); - - session - .users_post(FtApiUsersPostRequest::new(FtApiUserPostBody { - email: "yondoo@42gyeongsan.kr".to_string(), - campus_id: FtCampusId::new(GYEONGSAN), - first_name: "TEST".to_string(), - last_name: "ACCOUNT".to_string(), - login: format!("exam-gs{:02}", id), - password: format!("Exam-gs{:02}@4242", id), - kind: FtKind::Student, - pool_month: "january".to_string(), - pool_year: 2025, - })) - .await -} diff --git a/libft-api/bin/user_subscribe.rs b/libft-api/bin/user_subscribe.rs deleted file mode 100644 index c21d3cd..0000000 --- a/libft-api/bin/user_subscribe.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::sync::Arc; - -use libft_api::prelude::*; -use tokio::{sync::Semaphore, task::JoinSet}; - -#[derive(Debug)] -struct ExamSet { - exam_id: FtExamId, - project_id: FtProjectId, -} - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - let permit = Arc::new(Semaphore::new(1)); - - let target_users = []; - let exam_sets = Arc::new([ - ExamSet { - exam_id: FtExamId::new(22085), - project_id: FtProjectId::new(1302), - }, - ExamSet { - exam_id: FtExamId::new(22086), - project_id: FtProjectId::new(1303), - }, - ExamSet { - exam_id: FtExamId::new(22087), - project_id: FtProjectId::new(1304), - }, - ]); - - let mut handles = JoinSet::new(); - - for id in target_users { - let permit = Arc::clone(&permit); - let exam_sets = Arc::clone(&exam_sets); - handles.spawn(async move { - let _permit = permit.acquire().await.unwrap(); - - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) - .await - .unwrap(); - let client = FtClient::new(FtClientReqwestConnector::new()); - - let session = client.open_session(token); - - for exam_set in exam_sets.iter() { - let project_res = session - .projects_uesrs_post(FtApiProjectsUsersPostRequest::new( - FtApiProjectsUsersPostBody::new( - exam_set.project_id.clone(), - FtUserId::new(id), - ), - )) - .await; - - match project_res { - Ok(_) => println!( - "Successfully subscribed user {} to project {}", - id, exam_set.project_id - ), - Err(e) => println!( - "Failed to subscribe user {} to project {}: {:?}", - id, exam_set.project_id, e - ), - } - - let exam_res = session - .exams_users_post( - FtApiExamsUsersPostRequest::new(FtApiExamsUsersPostBody { - user_id: FtUserId::new(id), - }), - exam_set.exam_id.clone(), - ) - .await; - - match exam_res { - Ok(_) => println!( - "Successfully subscribed user {} to exam {}", - id, exam_set.exam_id - ), - Err(e) => println!( - "Failed to subscribe user {} to exam {}: {:?}", - id, exam_set.exam_id, e - ), - } - } - }); - } - - while let Some(Ok(res)) = handles.join_next().await { - println!("{:?}", res); - } -} diff --git a/libft-api/bin/blackholed.rs b/libft-api/examples/scroll.rs similarity index 96% rename from libft-api/bin/blackholed.rs rename to libft-api/examples/scroll.rs index 07c122a..7c2710e 100644 --- a/libft-api/bin/blackholed.rs +++ b/libft-api/examples/scroll.rs @@ -1,7 +1,7 @@ use std::{io::Write, sync::Arc}; use futures::FutureExt; -use libft_api::{campus_id::*, prelude::*}; +use libft_api::{info::campus_id::SEOUL, prelude::*}; use tokio::task::JoinSet; use tracing::info_span; @@ -41,6 +41,7 @@ async fn main() { let client = Arc::clone(&client); handles.spawn(async move { scroller(&client, thread_num, i, request_builder).await }); } + let mut all = Vec::::new(); while let Some(res) = handles.join_next().await { match res { diff --git a/libft-api/src/api.rs b/libft-api/src/api.rs index 9644f8b..29e3756 100644 --- a/libft-api/src/api.rs +++ b/libft-api/src/api.rs @@ -1,3 +1,8 @@ +//! Endpoint-specific clients for the 42 Intra API. +//! +//! Each submodule mirrors an API domain (campus, user, project, exam, and so on) and exposes +//! request/response types plus the associated `FtClientSession` helpers for issuing calls. + mod campus; mod cursus; mod exam; @@ -10,6 +15,7 @@ mod user; pub mod prelude; +/// Convenience abstraction for wrapper types that contain a `Vec` under a single field. pub trait HasVec { fn get_vec(&self) -> &Vec; fn take_vec(self) -> Vec; diff --git a/libft-api/src/api/campus/campus_id.rs b/libft-api/src/api/campus/campus_id.rs index 0bb9292..3584570 100644 --- a/libft-api/src/api/campus/campus_id.rs +++ b/libft-api/src/api/campus/campus_id.rs @@ -1,4 +1,5 @@ -use crate::{prelude::*, to_param, HasVec}; +use crate::prelude::*; +use crate::to_param; use libft_api_derive::HasVector; use rsb_derive::Builder; use serde::{Deserialize, Serialize}; @@ -63,11 +64,11 @@ where #[cfg(test)] mod tests { - use crate::prelude::*; + use super::*; #[tokio::test] async fn basic() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/api/campus/campus_id_journals.rs b/libft-api/src/api/campus/campus_id_journals.rs index be776ab..3416b15 100644 --- a/libft-api/src/api/campus/campus_id_journals.rs +++ b/libft-api/src/api/campus/campus_id_journals.rs @@ -2,7 +2,9 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; use tracing::debug; -use crate::{prelude::*, to_param, HasVec}; +use crate::prelude::*; +use crate::to_param; + use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] @@ -90,11 +92,14 @@ where #[cfg(test)] mod tests { - use crate::{campus_id::GYEONGSAN, prelude::*}; + + use crate::info::campus_id::GYEONGSAN; + + use super::*; #[tokio::test] async fn location_with_params() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/api/campus/campus_id_locations.rs b/libft-api/src/api/campus/campus_id_locations.rs index 77e9425..da994fa 100644 --- a/libft-api/src/api/campus/campus_id_locations.rs +++ b/libft-api/src/api/campus/campus_id_locations.rs @@ -1,4 +1,5 @@ -use crate::{prelude::*, to_param, HasVec}; +use crate::prelude::*; +use crate::to_param; use libft_api_derive::HasVector; use rsb_derive::Builder; use serde::{Deserialize, Serialize}; @@ -64,11 +65,12 @@ where #[cfg(test)] mod tests { - use crate::{campus_id::GYEONGSAN, prelude::*}; + use super::*; + use crate::info::campus_id::GYEONGSAN; #[tokio::test] async fn location_with_params() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/api/campus/campus_id_users.rs b/libft-api/src/api/campus/campus_id_users.rs index 070b885..bc8512d 100644 --- a/libft-api/src/api/campus/campus_id_users.rs +++ b/libft-api/src/api/campus/campus_id_users.rs @@ -1,4 +1,5 @@ -use crate::{prelude::*, to_param, HasVec}; +use crate::prelude::*; +use crate::to_param; use libft_api_derive::HasVector; use rsb_derive::Builder; use serde::{Deserialize, Serialize}; @@ -61,11 +62,12 @@ where #[cfg(test)] mod tests { - use crate::{campus_id::GYEONGSAN, prelude::*}; + use super::*; + use crate::info::campus_id::GYEONGSAN; #[tokio::test] async fn basic() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/api/campus/campus_users.rs b/libft-api/src/api/campus/campus_users.rs index bab6d3b..c9aa023 100644 --- a/libft-api/src/api/campus/campus_users.rs +++ b/libft-api/src/api/campus/campus_users.rs @@ -1,7 +1,9 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{prelude::*, to_param}; +use crate::prelude::*; +use crate::to_param; +use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiCampusUsersRequest { @@ -13,7 +15,7 @@ pub struct FtApiCampusUsersRequest { pub per_page: Option, } -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiCampusUsersResponse { pub campus_users: Vec, @@ -68,7 +70,7 @@ mod tests { #[tokio::test] async fn basic() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/api/cursus/cursus_id_projects.rs b/libft-api/src/api/cursus/cursus_id_projects.rs index a4f30b3..41ed26b 100644 --- a/libft-api/src/api/cursus/cursus_id_projects.rs +++ b/libft-api/src/api/cursus/cursus_id_projects.rs @@ -1,7 +1,8 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{prelude::*, to_param, HasVec}; +use crate::prelude::*; +use crate::to_param; use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] @@ -63,13 +64,10 @@ where #[cfg(test)] mod tests { use super::*; - use crate::{ - AuthInfo, FtApiToken, FtClient, FtClientReqwestConnector, FtCursusId, FT_CURSUS_ID, - }; #[tokio::test] async fn basic() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/api/exam/exams.rs b/libft-api/src/api/exam/exams.rs index d4cc573..2c3e970 100644 --- a/libft-api/src/api/exam/exams.rs +++ b/libft-api/src/api/exam/exams.rs @@ -1,7 +1,8 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{prelude::*, to_param, HasVec}; +use crate::prelude::*; +use crate::to_param; use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] @@ -42,7 +43,7 @@ where /// ``` /// #[tokio::test] /// async fn post_exams() { - /// let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + /// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) /// .await /// .unwrap(); /// @@ -115,7 +116,7 @@ mod tests { #[tokio::test] async fn get_exams() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/api/group/groups.rs b/libft-api/src/api/group/groups.rs index 0d4238b..ac93aac 100644 --- a/libft-api/src/api/group/groups.rs +++ b/libft-api/src/api/group/groups.rs @@ -1,7 +1,8 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{prelude::*, to_param, HasVec}; +use crate::prelude::*; +use crate::to_param; use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] @@ -65,7 +66,7 @@ mod tests { // #[tokio::test] // async fn post_groups() { - // let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + // let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) // .await // .unwrap(); // @@ -88,7 +89,7 @@ mod tests { #[tokio::test] async fn get_groups() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/api/prelude.rs b/libft-api/src/api/prelude.rs index 8adcfb3..92e9f0a 100644 --- a/libft-api/src/api/prelude.rs +++ b/libft-api/src/api/prelude.rs @@ -8,7 +8,4 @@ pub use super::project_user::*; pub use super::scale_team::*; pub use super::user::*; -pub use crate::auth::*; -pub use crate::common::*; -pub use crate::models::*; -pub use crate::FtClientReqwestConnector; +pub use super::HasVec; diff --git a/libft-api/src/api/project/project_data.rs b/libft-api/src/api/project/project_data.rs index 5faf52c..f2a215f 100644 --- a/libft-api/src/api/project/project_data.rs +++ b/libft-api/src/api/project/project_data.rs @@ -1,11 +1,9 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{ - convert_filter_option_to_tuple, convert_range_option_to_tuple, to_param, ClientResult, - FtClientHttpConnector, FtClientSession, FtCursusId, FtFilterOption, FtProjectData, - FtRangeOption, FtSortOption, -}; +use crate::prelude::*; +use crate::to_param; +use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiProjectDataRequest { @@ -18,7 +16,7 @@ pub struct FtApiProjectDataRequest { pub per_page: Option, } -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiProjectDataResponse { pub project_data: Vec, @@ -71,7 +69,7 @@ mod tests { #[tokio::test] async fn project_data() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/api/project/projects.rs b/libft-api/src/api/project/projects.rs index f83a181..deaf416 100644 --- a/libft-api/src/api/project/projects.rs +++ b/libft-api/src/api/project/projects.rs @@ -1,7 +1,8 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{prelude::*, to_param, HasVec}; +use crate::prelude::*; +use crate::to_param; use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] @@ -66,7 +67,7 @@ mod tests { #[tokio::test] async fn projects() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/api/project/projects_id_teams.rs b/libft-api/src/api/project/projects_id_teams.rs index c81fc73..7e226c0 100644 --- a/libft-api/src/api/project/projects_id_teams.rs +++ b/libft-api/src/api/project/projects_id_teams.rs @@ -1,12 +1,9 @@ +use crate::prelude::*; +use crate::to_param; +use libft_api_derive::HasVector; use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{ - convert_filter_option_to_tuple, convert_range_option_to_tuple, to_param, ClientResult, - FtClientHttpConnector, FtClientSession, FtCursusId, FtFilterOption, FtProjectId, FtRangeOption, - FtSortOption, FtTeam, -}; - #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiProjectsIdTeamsRequest { pub project_id: FtProjectId, @@ -18,7 +15,7 @@ pub struct FtApiProjectsIdTeamsRequest { pub per_page: Option, } -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiProjectsIdTeamsResponse { pub teams: Vec, @@ -72,7 +69,7 @@ mod tests { #[tokio::test] async fn projects_id_teams_basic_test() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/api/project_session/project_sessions_id_scale_teams.rs b/libft-api/src/api/project_session/project_sessions_id_scale_teams.rs index 0ebd984..7c2a326 100644 --- a/libft-api/src/api/project_session/project_sessions_id_scale_teams.rs +++ b/libft-api/src/api/project_session/project_sessions_id_scale_teams.rs @@ -1,4 +1,5 @@ -use crate::{prelude::*, HasVec}; +use crate::prelude::*; +use crate::to_param; use libft_api_derive::HasVector; use rsb_derive::Builder; use serde::{Deserialize, Serialize}; @@ -34,11 +35,11 @@ where mod tests { use ft_project_session_ids::ft_cursus::inner::LIBFT; - use crate::prelude::*; + use super::*; #[tokio::test] async fn location_deserialize() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/api/project_session/project_sessions_id_teams.rs b/libft-api/src/api/project_session/project_sessions_id_teams.rs index e588b1c..11c29ad 100644 --- a/libft-api/src/api/project_session/project_sessions_id_teams.rs +++ b/libft-api/src/api/project_session/project_sessions_id_teams.rs @@ -1,7 +1,8 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{prelude::*, to_param, HasVec}; +use crate::prelude::*; +use crate::to_param; use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder, HasVector)] @@ -61,15 +62,13 @@ where #[cfg(test)] mod tests { + use crate::prelude::ft_project_session_ids::ft_cursus::inner::LIBFT; + use super::*; - use crate::{ - ft_project_session_ids::ft_cursus::inner::LIBFT, AuthInfo, FtApiToken, FtClient, - FtClientReqwestConnector, FtFilterField, FtProjectSessionId, - }; #[tokio::test] async fn location_deserialize() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); @@ -86,7 +85,7 @@ mod tests { #[tokio::test] async fn location_deserialize_with_filter() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/api/project_user/projects_users.rs b/libft-api/src/api/project_user/projects_users.rs index 9970954..4377cfb 100644 --- a/libft-api/src/api/project_user/projects_users.rs +++ b/libft-api/src/api/project_user/projects_users.rs @@ -1,4 +1,5 @@ -use crate::{prelude::*, to_param, HasVec}; +use crate::prelude::*; +use crate::to_param; use libft_api_derive::HasVector; use rsb_derive::Builder; use serde::{Deserialize, Serialize}; @@ -87,13 +88,13 @@ where #[cfg(test)] mod tests { - use crate::*; + use crate::info::ft_cursus::COMMON_CORE_SUBJECTS; use super::*; #[tokio::test] async fn basic() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); @@ -102,7 +103,7 @@ mod tests { )); let session = client.open_session(token); - let project_ids = ALL_INNER_SUBJECTS_ID + let project_ids = COMMON_CORE_SUBJECTS .into_iter() .map(|id| id.to_string()) .collect(); diff --git a/libft-api/src/api/scale_team/scale_teams.rs b/libft-api/src/api/scale_team/scale_teams.rs index 3cce4fa..c12f2e0 100644 --- a/libft-api/src/api/scale_team/scale_teams.rs +++ b/libft-api/src/api/scale_team/scale_teams.rs @@ -1,4 +1,5 @@ -use crate::{prelude::*, to_param, HasVec}; +use crate::prelude::*; +use crate::to_param; use libft_api_derive::HasVector; use rsb_derive::Builder; @@ -95,7 +96,7 @@ mod tests { #[tokio::test] async fn basic() { tracing_subscriber::fmt::init(); - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/api/user/users.rs b/libft-api/src/api/user/users.rs index f63a657..7c57d51 100644 --- a/libft-api/src/api/user/users.rs +++ b/libft-api/src/api/user/users.rs @@ -1,7 +1,8 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{prelude::*, to_param, HasVec}; +use crate::prelude::*; +use crate::to_param; use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] @@ -91,11 +92,10 @@ where mod tests { use super::*; - use crate::*; #[tokio::test] async fn basic() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); @@ -108,31 +108,4 @@ mod tests { assert!(res.is_ok()); } - - // #[tokio::test] - // async fn user_creation() { - // let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) - // .await - // .unwrap(); - // - // let client = FtClient::new(FtClientReqwestConnector::with_connector( - // reqwest::Client::new(), - // )); - // - // let session = client.open_session(token); - // let res = session - // .users_post(FtApiUsersPostRequest::new(FtApiUserPostBody { - // email: "yondoo@42gyeongsan.kr".to_string(), - // campus_id: FtCampusId::new(GYEONGSAN), - // first_name: "TEST".to_string(), - // last_name: "ACCOUNT".to_string(), - // login: "exam-gs03".to_string(), - // password: "Exam-gs03@4242".to_string(), - // kind: FtKind::Student, - // pool_month: "january".to_string(), - // pool_year: 2025, - // })) - // .await - // .unwrap(); - // } } diff --git a/libft-api/src/api/user/users_id.rs b/libft-api/src/api/user/users_id.rs index 73b16cc..b4e8d5d 100644 --- a/libft-api/src/api/user/users_id.rs +++ b/libft-api/src/api/user/users_id.rs @@ -68,7 +68,7 @@ mod tests { #[tokio::test] async fn basic() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/api/user/users_id_correction_point_historics.rs b/libft-api/src/api/user/users_id_correction_point_historics.rs index 5687db9..a35ab60 100644 --- a/libft-api/src/api/user/users_id_correction_point_historics.rs +++ b/libft-api/src/api/user/users_id_correction_point_historics.rs @@ -1,7 +1,8 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{prelude::*, to_param, HasVec}; +use crate::prelude::*; +use crate::to_param; use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] @@ -66,7 +67,7 @@ mod tests { #[tokio::test] async fn basic() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/api/user/users_id_correction_points_add.rs b/libft-api/src/api/user/users_id_correction_points_add.rs index 6895eeb..84d1a41 100644 --- a/libft-api/src/api/user/users_id_correction_points_add.rs +++ b/libft-api/src/api/user/users_id_correction_points_add.rs @@ -1,8 +1,7 @@ use rsb_derive::Builder; -use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; -use crate::prelude::*; +use crate::{prelude::*, to_param}; #[derive(Debug, Serialize, Deserialize, Builder)] #[serde(transparent)] @@ -17,20 +16,11 @@ pub struct FtApiUsersIdCorrectionPointsAddRequest { pub amount: FtCorrectionPointsAmount, } -#[derive(Debug, Eq, Hash, PartialEq, PartialOrd, Clone, Serialize, Deserialize, ValueStruct)] -pub struct FtCorrectionPointsReason(String); - -#[derive(Debug, Eq, Hash, PartialEq, PartialOrd, Clone, Serialize, Deserialize, ValueStruct)] -pub struct FtCorrectionPointsAmount(i32); impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { - /// - /// - /// # Errors - /// - /// This function will return an error if + /// You need a roles `Advanced tutor` to use this API pub async fn users_id_correction_points_add( &self, request: FtApiUsersIdCorrectionPointsAddRequest, @@ -43,7 +33,7 @@ where #[cfg(test)] mod tests { - use crate::prelude::*; + use super::*; #[test] fn correction_points_add_request_serde() { @@ -75,8 +65,7 @@ mod tests { reason: FtCorrectionPointsReason::new("test".to_owned()), amount: FtCorrectionPointsAmount::new(42), }) - .await; - - assert!(res.is_ok()); + .await + .unwrap(); } } diff --git a/libft-api/src/api/user/users_id_cursus_users.rs b/libft-api/src/api/user/users_id_cursus_users.rs index 58e1158..01ef302 100644 --- a/libft-api/src/api/user/users_id_cursus_users.rs +++ b/libft-api/src/api/user/users_id_cursus_users.rs @@ -1,7 +1,8 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{prelude::*, to_param, HasVec}; +use crate::prelude::*; +use crate::to_param; use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] @@ -90,7 +91,7 @@ where /// /// #[tokio::main] /// async fn main() { - /// let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + /// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) /// .await /// .unwrap(); /// @@ -172,7 +173,7 @@ mod tests { // #[tokio::test] // async fn add_cursus() { - // let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + // let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) // .await // .unwrap(); // @@ -201,7 +202,7 @@ mod tests { #[tokio::test] async fn basic() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/api/user/users_id_locations.rs b/libft-api/src/api/user/users_id_locations.rs index 0eb2fb4..88e48e9 100644 --- a/libft-api/src/api/user/users_id_locations.rs +++ b/libft-api/src/api/user/users_id_locations.rs @@ -1,7 +1,8 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{prelude::*, to_param, HasVec}; +use crate::prelude::*; +use crate::to_param; use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] @@ -61,12 +62,12 @@ where #[cfg(test)] mod tests { - use crate::{prelude::*, TEST_USER_YONDOO_ID}; + use crate::prelude::*; /// Checks the filter[active] is working properly. #[tokio::test] async fn is_active() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/api/user/users_id_locations_stats.rs b/libft-api/src/api/user/users_id_locations_stats.rs index aaa9fc1..13b2987 100644 --- a/libft-api/src/api/user/users_id_locations_stats.rs +++ b/libft-api/src/api/user/users_id_locations_stats.rs @@ -60,12 +60,12 @@ where #[cfg(test)] mod tests { - use crate::{prelude::*, TEST_USER_YONDOO_ID}; + use crate::prelude::*; use chrono::{Days, Local}; #[tokio::test] async fn specific_date_range() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/api/user/users_id_projects_users.rs b/libft-api/src/api/user/users_id_projects_users.rs index 7995560..1275518 100644 --- a/libft-api/src/api/user/users_id_projects_users.rs +++ b/libft-api/src/api/user/users_id_projects_users.rs @@ -1,4 +1,5 @@ -use crate::{prelude::*, to_param, HasVec}; +use crate::prelude::*; +use crate::to_param; use libft_api_derive::HasVector; use rsb_derive::Builder; use serde::{Deserialize, Serialize}; @@ -74,7 +75,7 @@ mod tests { #[tokio::test] async fn basic() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/api/user/users_id_teams.rs b/libft-api/src/api/user/users_id_teams.rs index 4c31fdf..6439151 100644 --- a/libft-api/src/api/user/users_id_teams.rs +++ b/libft-api/src/api/user/users_id_teams.rs @@ -1,7 +1,8 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{prelude::*, to_param, HasVec}; +use crate::prelude::*; +use crate::to_param; use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] @@ -73,7 +74,7 @@ mod tests { #[tokio::test] async fn basic() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); diff --git a/libft-api/src/axum_support/mod.rs b/libft-api/src/axum_support/mod.rs deleted file mode 100644 index cae28a8..0000000 --- a/libft-api/src/axum_support/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -use std::sync::Arc; - -use crate::{FtClient, FtClientHttpConnector}; - -pub struct FtEventsAxumListener -where - SCHC: FtClientHttpConnector + Send + Sync, -{ - pub client: Arc>, -} - -impl FtEventsAxumListener -where - SCHC: FtClientHttpConnector + Send + Sync, -{ - pub fn new(client: Arc>) -> Self { - Self { client } - } -} diff --git a/libft-api/src/common/client.rs b/libft-api/src/common/client.rs index 71ffb07..5a2fa44 100644 --- a/libft-api/src/common/client.rs +++ b/libft-api/src/common/client.rs @@ -4,8 +4,9 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; use url::Url; -use crate::common::ratelimiter::HeaderMetaData; -use crate::{FtApiToken, FtClientError, FtClientReqwestConnector, RateLimiter}; +use crate::auth::FtApiToken; +use crate::common::*; +use crate::connector::*; pub type ClientResult = std::result::Result; diff --git a/libft-api/src/common/paginator.rs b/libft-api/src/common/paginator.rs index 263f6d2..0607d7e 100644 --- a/libft-api/src/common/paginator.rs +++ b/libft-api/src/common/paginator.rs @@ -1,7 +1,7 @@ use std::{ops::ControlFlow, sync::Arc, time::Duration}; use crate::prelude::*; -use crate::HasVec; + use futures::future::BoxFuture; use tokio::time::sleep; diff --git a/libft-api/src/connector.rs b/libft-api/src/connector.rs index ee62a16..79cf592 100644 --- a/libft-api/src/connector.rs +++ b/libft-api/src/connector.rs @@ -8,11 +8,8 @@ use reqwest::{ use tracing::{debug, info, Span}; use url::Url; -use crate::{ - map_serde_error, ClientResult, FtApiToken, FtClientError, FtClientHttpApiUri, - FtClientHttpConnector, FtEnvelopeMessage, FtHttpError, FtRateLimitError, FtReqwestError, - HeaderMetaData, -}; +use crate::auth::FtApiToken; +use crate::common::*; /// A client for the 42 API that uses `reqwest` as the underlying HTTP client. pub struct FtClientReqwestConnector { diff --git a/libft-api/src/info.rs b/libft-api/src/info.rs index 0460b48..454cfb2 100644 --- a/libft-api/src/info.rs +++ b/libft-api/src/info.rs @@ -60,10 +60,9 @@ pub mod campus_id { pub const LYON: i32 = 9; pub const PARIS: i32 = 1; } -pub use ft_cursus::*; -mod ft_cursus { +pub mod ft_cursus { pub use inner::*; - pub const ALL_INNER_SUBJECTS_ID: [u16; 33] = [ + pub const COMMON_CORE_SUBJECTS: [u16; 33] = [ LIBFT, FT_PRINTF, GET_NEXT_LINE, diff --git a/libft-api/src/lib.rs b/libft-api/src/lib.rs index f7d1c31..184c968 100644 --- a/libft-api/src/lib.rs +++ b/libft-api/src/lib.rs @@ -1,17 +1,51 @@ // #![warn(clippy::pedantic)] +//! # libft-api +//! +//! `libft-api` provides typed, asynchronous access to the [42 Intra API](https://api.intra.42.fr/). +//! It wraps common endpoints with strongly typed requests, automatic rate limiting, and reusable +//! session management. +//! +//! ## Quick start +//! ```rust,no_run +//! use libft_api::{prelude::*}; +//! +//! # async fn run() -> libft_api::ClientResult<()> { +//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let client = FtClient::new(FtClientReqwestConnector::new()); +//! let session = client.open_session(token); +//! let response = session +//! .campus_id_locations( +//! FtApiCampusIdLocationsRequest::new(FtCampusId::new(GYEONGSAN)).with_per_page(5), +//! ) +//! .await?; +//! for location in response.location { +//! println!("{} @ {}", location.user.login, location.host); +//! } +//! # Ok(()) +//! # } +//! # tokio::runtime::Runtime::new().unwrap().block_on(run()).unwrap(); +//! ``` +//! +//! Set the `FT_API_CLIENT_UID` and `FT_API_CLIENT_SECRET` environment variables before building a +//! token. The default `FtClientReqwestConnector` reuses a shared `reqwest` client and applies the +//! crate's rate limiter, so you stay within the platform quotas. +//! +//! ## Modules +//! * `api` — high-level endpoint clients grouped by 42 domain (campus, user, projects, exams). +//! * `models` — serde-powered representations of request and response payloads. +//! * `auth` — helpers for building OAuth tokens and refreshing sessions. +//! * `axum_support` — Axum extractors and middleware for wiring the client into services. +//! +//! Explore the `bin/` directory for runnable examples of each workflow, and enable tracing with +//! `RUST_LOG=info` to inspect HTTP activity during development. #![feature(macro_metavar_expr_concat)] -pub use api::*; -mod api; -pub use auth::*; +pub mod api; +pub mod models; + mod auth; -pub use common::*; mod common; -pub use models::*; -mod models; -pub use connector::*; +pub mod info; +pub mod prelude; + mod connector; -pub use axum_support::*; -mod axum_support; -pub use info::*; -mod info; diff --git a/libft-api/src/models.rs b/libft-api/src/models.rs index fadd034..39f74b0 100644 --- a/libft-api/src/models.rs +++ b/libft-api/src/models.rs @@ -1,95 +1,26 @@ -use chrono::{DateTime, FixedOffset, Utc}; -use rvstruct::ValueStruct; -use serde::{Deserialize, Serialize}; - -pub use locations::*; -mod locations; -pub use scale_teams::*; -mod scale_teams; -pub use flag::*; -mod flag; -pub use project_session::*; -mod project_session; -pub use project::*; -mod project; -pub use project_data::*; -mod project_data; -pub use projects_users::*; -mod projects_users; -pub use scale::*; -mod scale; -pub use feedback::*; -mod feedback; -pub use team::*; -mod team; -pub use language::*; -mod language; -pub use image::*; -mod image; -pub use user::*; -mod user; -pub use campus::*; -mod campus; -pub use correction_point_history::*; -mod correction_point_history; -mod cursus_user; -pub use cursus_user::*; -mod campus_user; -pub use campus_user::*; -mod journals; -pub use journals::*; -mod group; -pub use group::*; -mod exam; -pub use exam::*; -mod achievement; -pub use achievement::*; -mod title; -pub use title::*; -mod role; -pub use role::*; - -mod common; - -#[derive(Serialize, PartialEq, PartialOrd, Deserialize, Debug, ValueStruct)] -pub struct FtDateTimeUtc(pub DateTime); - -#[derive(Serialize, PartialEq, PartialOrd, Deserialize, Debug, ValueStruct)] -pub struct FtDateTimeFixedOffset(DateTime); - -pub type Seresult = Result; - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::from_str; - - #[derive(Debug, PartialEq, PartialOrd, Clone, Serialize, Deserialize)] - struct FtTestUser { - user: FtLoginId, - } - - #[test] - fn test_loginid() { - let json_user = r#"{ "user": "hdoo"}"#; - let expected_user = FtTestUser { - user: FtLoginId("hdoo".to_string()), - }; - let deserialize_login: FtTestUser = from_str(json_user).unwrap(); - assert_eq!(deserialize_login, expected_user); - } - - #[test] - fn partial_user() { - let raw_partial_user = r#" - { - "id": 183812, - "login": "nkanaan", - "url": "https://api.intra.42.fr/v2/users/nkanaan" - } - "#; - - let res: Result = serde_json::from_str(raw_partial_user); - assert!(res.is_ok(), "{:?}", res); - } -} +pub mod achievement; +pub mod campus; +pub mod campus_user; +pub mod correction_point_history; +pub mod cursus_user; +pub mod datetime; +pub mod exam; +pub mod feedback; +pub mod flag; +pub mod group; +pub mod image; +pub mod journals; +pub mod language; +pub mod locations; +pub mod project; +pub mod project_data; +pub mod project_session; +pub mod projects_users; +pub mod role; +pub mod scale; +pub mod scale_teams; +pub mod team; +pub mod title; +pub mod user; + +pub mod prelude; diff --git a/libft-api/src/models/achievement.rs b/libft-api/src/models/achievement.rs index b13ce51..1a8197d 100644 --- a/libft-api/src/models/achievement.rs +++ b/libft-api/src/models/achievement.rs @@ -1,9 +1,9 @@ +use crate::models::prelude::*; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; -use super::FtUrl; +// use crate::models::prelude::*; -// // FtAchievement and its field structs // diff --git a/libft-api/src/models/campus.rs b/libft-api/src/models/campus.rs index e79836d..81c3634 100644 --- a/libft-api/src/models/campus.rs +++ b/libft-api/src/models/campus.rs @@ -1,8 +1,7 @@ +use crate::models::prelude::*; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; -use crate::{FtDateTimeUtc, FtLanguage, FtUrl}; - #[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct FtCampus { pub id: FtCampusId, diff --git a/libft-api/src/models/campus_user.rs b/libft-api/src/models/campus_user.rs index 175efa5..5f77f66 100644 --- a/libft-api/src/models/campus_user.rs +++ b/libft-api/src/models/campus_user.rs @@ -1,9 +1,8 @@ +use crate::models::prelude::*; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; -use crate::FtDateTimeUtc; - -use super::{FtCampusId, FtUserId}; +// use crate::models::prelude::*; #[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct FtCampusUser { diff --git a/libft-api/src/models/common.rs b/libft-api/src/models/common.rs deleted file mode 100644 index 8b13789..0000000 --- a/libft-api/src/models/common.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/libft-api/src/models/correction_point_history.rs b/libft-api/src/models/correction_point_history.rs index c593c3f..d6aa2f9 100644 --- a/libft-api/src/models/correction_point_history.rs +++ b/libft-api/src/models/correction_point_history.rs @@ -1,10 +1,8 @@ +use crate::models::prelude::*; +use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; use std::option::Option; -use crate::api::prelude::*; - -use super::{FtDateTimeUtc, FtScaleTeamId}; - #[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct FtCorrectionPointHistory { pub id: FtCorrectionPointHistoryId, @@ -16,6 +14,12 @@ pub struct FtCorrectionPointHistory { pub updated_at: FtDateTimeUtc, } +#[derive(Debug, Eq, Hash, PartialEq, PartialOrd, Clone, Serialize, Deserialize, ValueStruct)] +pub struct FtCorrectionPointsAmount(i32); + +#[derive(Debug, Eq, Hash, PartialEq, PartialOrd, Clone, Serialize, Deserialize, ValueStruct)] +pub struct FtCorrectionPointsReason(String); + #[derive(Debug, Eq, Hash, PartialEq, PartialOrd, Clone, Serialize, Deserialize)] pub struct FtCorrectionPointHistoryId(u64); diff --git a/libft-api/src/models/cursus_user.rs b/libft-api/src/models/cursus_user.rs index a280f23..586c0b8 100644 --- a/libft-api/src/models/cursus_user.rs +++ b/libft-api/src/models/cursus_user.rs @@ -44,9 +44,13 @@ pub struct FtSkillName(String); #[derive(Debug, PartialEq, PartialOrd, Clone, Serialize, Deserialize, ValueStruct)] pub struct FtSkillLevel(f64); -#[test] -fn parse_to_struct() { - let raw = r#"[ +#[cfg(test)] +mod tests { + use crate::api::prelude::*; + + #[test] + fn parse_to_struct() { + let raw = r#"[ { "grade": null, "level": 0.0, @@ -277,5 +281,6 @@ fn parse_to_struct() { ] "#; - serde_json::from_str::(raw).unwrap(); + serde_json::from_str::(raw).unwrap(); + } } diff --git a/libft-api/src/models/datetime.rs b/libft-api/src/models/datetime.rs new file mode 100644 index 0000000..1b92027 --- /dev/null +++ b/libft-api/src/models/datetime.rs @@ -0,0 +1,47 @@ +use chrono::{DateTime, FixedOffset, Utc}; +use rvstruct::ValueStruct; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, PartialEq, PartialOrd, Deserialize, Debug, ValueStruct)] +pub struct FtDateTimeUtc(pub DateTime); + +#[derive(Serialize, PartialEq, PartialOrd, Deserialize, Debug, ValueStruct)] +pub struct FtDateTimeFixedOffset(DateTime); + +pub type Seresult = Result; + +#[cfg(test)] +mod tests { + use serde::{Deserialize, Serialize}; + use serde_json::from_str; + + use crate::models::prelude::*; + #[derive(Debug, PartialEq, PartialOrd, Clone, Serialize, Deserialize)] + struct FtTestUser { + user: FtLoginId, + } + + #[test] + fn test_loginid() { + let json_user = r#"{ "user": "hdoo"}"#; + let expected_user = FtTestUser { + user: FtLoginId("hdoo".to_string()), + }; + let deserialize_login: FtTestUser = from_str(json_user).unwrap(); + assert_eq!(deserialize_login, expected_user); + } + + #[test] + fn partial_user() { + let raw_partial_user = r#" + { + "id": 183812, + "login": "nkanaan", + "url": "https://api.intra.42.fr/v2/users/nkanaan" + } + "#; + + let res: Result = serde_json::from_str(raw_partial_user); + assert!(res.is_ok(), "{:?}", res); + } +} diff --git a/libft-api/src/models/exam.rs b/libft-api/src/models/exam.rs index 6dd1809..e3fee95 100644 --- a/libft-api/src/models/exam.rs +++ b/libft-api/src/models/exam.rs @@ -1,8 +1,7 @@ +use crate::models::prelude::*; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; -use super::{FtDateTimeUtc, FtUser, FtUserId}; - #[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct FtExamUser { pub id: FtExamUserId, diff --git a/libft-api/src/models/feedback.rs b/libft-api/src/models/feedback.rs index 94461d9..f066912 100644 --- a/libft-api/src/models/feedback.rs +++ b/libft-api/src/models/feedback.rs @@ -1,8 +1,7 @@ +use crate::models::prelude::*; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; -use crate::{FtComment, FtDateTimeUtc, FtUser}; - #[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct FtFeedback { pub comment: FtComment, @@ -36,7 +35,6 @@ pub struct FtRating(i32); #[test] fn deser_feedbacks() { - use crate::Seresult; let raw_feedbacks = r#" [ { diff --git a/libft-api/src/models/flag.rs b/libft-api/src/models/flag.rs index ce71180..cd4824a 100644 --- a/libft-api/src/models/flag.rs +++ b/libft-api/src/models/flag.rs @@ -1,7 +1,6 @@ +use crate::models::prelude::*; use serde::{Deserialize, Serialize}; -use crate::FtDateTimeUtc; - #[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct FtFlag { pub id: i8, @@ -80,7 +79,6 @@ pub struct FtFlagName(String); // // #[cfg(test)] // mod tests { -// use super::*; // #[test] // fn deserialize_flag() { // let raw_string = r#"[ diff --git a/libft-api/src/models/group.rs b/libft-api/src/models/group.rs index c7f7678..dc92581 100644 --- a/libft-api/src/models/group.rs +++ b/libft-api/src/models/group.rs @@ -1,3 +1,4 @@ +use crate::models::prelude::*; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; diff --git a/libft-api/src/models/image.rs b/libft-api/src/models/image.rs index cb95752..5ed0dae 100644 --- a/libft-api/src/models/image.rs +++ b/libft-api/src/models/image.rs @@ -1,7 +1,6 @@ +use crate::models::prelude::*; use serde::{Deserialize, Serialize}; -use crate::FtUrl; - #[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct FtImage { pub link: Option, diff --git a/libft-api/src/models/journals.rs b/libft-api/src/models/journals.rs index fc3f396..c0f41b5 100644 --- a/libft-api/src/models/journals.rs +++ b/libft-api/src/models/journals.rs @@ -1,8 +1,7 @@ +use crate::models::prelude::*; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; -use super::{FtCampusId, FtCursusId, FtDateTimeUtc, FtUserId}; - #[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct FtJournal { pub id: FtJournalId, diff --git a/libft-api/src/models/language.rs b/libft-api/src/models/language.rs index e3eb91e..562dc36 100644 --- a/libft-api/src/models/language.rs +++ b/libft-api/src/models/language.rs @@ -1,8 +1,7 @@ +use crate::models::prelude::*; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; -use crate::FtDateTimeUtc; - #[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct FtLanguage { pub id: FtLanguageId, diff --git a/libft-api/src/models/locations.rs b/libft-api/src/models/locations.rs index 8c85f52..a0331b4 100644 --- a/libft-api/src/models/locations.rs +++ b/libft-api/src/models/locations.rs @@ -1,8 +1,7 @@ +use crate::models::prelude::*; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; -use crate::models::{FtCampusId, FtDateTimeUtc, FtUser}; - #[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct FtLocation { pub id: FtLocationId, diff --git a/libft-api/src/models/prelude.rs b/libft-api/src/models/prelude.rs new file mode 100644 index 0000000..f6cd3e4 --- /dev/null +++ b/libft-api/src/models/prelude.rs @@ -0,0 +1,24 @@ +pub use super::achievement::*; +pub use super::campus::*; +pub use super::campus_user::*; +pub use super::correction_point_history::*; +pub use super::cursus_user::*; +pub use super::datetime::*; +pub use super::exam::*; +pub use super::feedback::*; +pub use super::flag::*; +pub use super::group::*; +pub use super::image::*; +pub use super::journals::*; +pub use super::language::*; +pub use super::locations::*; +pub use super::project::*; +pub use super::project_data::*; +pub use super::project_session::*; +pub use super::projects_users::*; +pub use super::role::*; +pub use super::scale::*; +pub use super::scale_teams::*; +pub use super::team::*; +pub use super::title::*; +pub use super::user::*; diff --git a/libft-api/src/models/project.rs b/libft-api/src/models/project.rs index 00701be..88f5b0f 100644 --- a/libft-api/src/models/project.rs +++ b/libft-api/src/models/project.rs @@ -1,8 +1,7 @@ +use crate::models::prelude::*; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; -use crate::{FtCampus, FtDateTimeUtc, FtProjectSession, FtUrl}; - #[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct FtProject { pub campus: Option>, diff --git a/libft-api/src/models/project_data.rs b/libft-api/src/models/project_data.rs index d0b0fef..ff09ad2 100644 --- a/libft-api/src/models/project_data.rs +++ b/libft-api/src/models/project_data.rs @@ -1,8 +1,7 @@ +use crate::models::prelude::*; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; -use crate::FtProjectSessionId; - #[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct FtProjectData { pub by: Vec>, diff --git a/libft-api/src/models/project_session.rs b/libft-api/src/models/project_session.rs index 3ce1212..d28ea08 100644 --- a/libft-api/src/models/project_session.rs +++ b/libft-api/src/models/project_session.rs @@ -1,8 +1,7 @@ +use crate::models::prelude::*; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; -use crate::{FtDateTimeUtc, FtProjectId, FtScale}; - #[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct FtProjectSession { pub id: FtProjectSessionId, @@ -61,61 +60,61 @@ pub struct FtFileSize(u64); pub struct FtMimeType(String); #[derive(Debug, PartialEq, PartialOrd, Clone, Serialize, Deserialize, ValueStruct)] -pub struct FtProjectSessionId(pub i16); +pub struct FtProjectSessionId(pub u16); pub mod ft_project_session_ids { pub mod ft_cursus { pub mod inner { - pub const FT_TRANSCENDENCE: i16 = 11835; - pub const WEBSERV: i16 = 11837; - pub const INCEPTION: i16 = 11848; - pub const CPP_MODULE_05: i16 = 11843; - pub const CPP_MODULE_06: i16 = 11844; - pub const CPP_MODULE_07: i16 = 11845; - pub const CPP_MODULE_08: i16 = 11846; - pub const CPP_MODULE_09: i16 = 11847; - pub const NETPRACTICE: i16 = 11851; - pub const CUB3D: i16 = 11850; - pub const MINIRT: i16 = 11849; - pub const CPP_MODULE_00: i16 = 11838; - pub const CPP_MODULE_01: i16 = 11839; - pub const CPP_MODULE_02: i16 = 11840; - pub const CPP_MODULE_03: i16 = 11841; - pub const CPP_MODULE_04: i16 = 11842; - pub const MINISHELL: i16 = 11852; - pub const PHILOSOPHER: i16 = 11853; - pub const PUSH_SWAP: i16 = 11854; - pub const PIPEX: i16 = 11833; - pub const MINITALK: i16 = 11834; - pub const FDF: i16 = 11856; - pub const FRACT_OL: i16 = 11855; - pub const SO_LONG: i16 = 11857; - pub const BORN2BEROOT: i16 = 11831; - pub const FT_PRINTF: i16 = 11832; - pub const GET_NEXT_LINE: i16 = 11830; - pub const LIBFT: i16 = 11805; + pub const FT_TRANSCENDENCE: u16 = 11835; + pub const WEBSERV: u16 = 11837; + pub const INCEPTION: u16 = 11848; + pub const CPP_MODULE_05: u16 = 11843; + pub const CPP_MODULE_06: u16 = 11844; + pub const CPP_MODULE_07: u16 = 11845; + pub const CPP_MODULE_08: u16 = 11846; + pub const CPP_MODULE_09: u16 = 11847; + pub const NETPRACTICE: u16 = 11851; + pub const CUB3D: u16 = 11850; + pub const MINIRT: u16 = 11849; + pub const CPP_MODULE_00: u16 = 11838; + pub const CPP_MODULE_01: u16 = 11839; + pub const CPP_MODULE_02: u16 = 11840; + pub const CPP_MODULE_03: u16 = 11841; + pub const CPP_MODULE_04: u16 = 11842; + pub const MINISHELL: u16 = 11852; + pub const PHILOSOPHER: u16 = 11853; + pub const PUSH_SWAP: u16 = 11854; + pub const PIPEX: u16 = 11833; + pub const MINITALK: u16 = 11834; + pub const FDF: u16 = 11856; + pub const FRACT_OL: u16 = 11855; + pub const SO_LONG: u16 = 11857; + pub const BORN2BEROOT: u16 = 11831; + pub const FT_PRINTF: u16 = 11832; + pub const GET_NEXT_LINE: u16 = 11830; + pub const LIBFT: u16 = 11805; } } pub mod c_piscine { - pub const C_PISCINE_C_13: i16 = 11290; - pub const C_PISCINE_C_12: i16 = 11289; - pub const C_PISCINE_C_11: i16 = 11288; - pub const C_PISCINE_C_10: i16 = 11287; - pub const C_PISCINE_C_09: i16 = 11286; - pub const C_PISCINE_C_08: i16 = 11285; - pub const C_PISCINE_C_07: i16 = 11284; - pub const C_PISCINE_C_06: i16 = 11283; - pub const C_PISCINE_C_05: i16 = 11282; - pub const C_PISCINE_C_04: i16 = 11281; - pub const C_PISCINE_C_03: i16 = 11280; - pub const C_PISCINE_C_02: i16 = 11279; - pub const C_PISCINE_C_01: i16 = 11278; - pub const C_PISCINE_C_00: i16 = 11277; - pub const C_PISCINE_SHELL_01: i16 = 11291; - pub const C_PISCINE_SHELL_00: i16 = 11193; - pub const C_PISCINE_RUSH_02: i16 = 11306; - pub const C_PISCINE_RUSH_01: i16 = 11305; - pub const C_PISCINE_RUSH_00: i16 = 11304; - pub const C_PISCINE_BSQ: i16 = 11353; + pub const C_PISCINE_C_13: u16 = 11290; + pub const C_PISCINE_C_12: u16 = 11289; + pub const C_PISCINE_C_11: u16 = 11288; + pub const C_PISCINE_C_10: u16 = 11287; + pub const C_PISCINE_C_09: u16 = 11286; + pub const C_PISCINE_C_08: u16 = 11285; + pub const C_PISCINE_C_07: u16 = 11284; + pub const C_PISCINE_C_06: u16 = 11283; + pub const C_PISCINE_C_05: u16 = 11282; + pub const C_PISCINE_C_04: u16 = 11281; + pub const C_PISCINE_C_03: u16 = 11280; + pub const C_PISCINE_C_02: u16 = 11279; + pub const C_PISCINE_C_01: u16 = 11278; + pub const C_PISCINE_C_00: u16 = 11277; + pub const C_PISCINE_SHELL_01: u16 = 11291; + pub const C_PISCINE_SHELL_00: u16 = 11193; + pub const C_PISCINE_RUSH_02: u16 = 11306; + pub const C_PISCINE_RUSH_01: u16 = 11305; + pub const C_PISCINE_RUSH_00: u16 = 11304; + pub const C_PISCINE_BSQ: u16 = 11353; } } diff --git a/libft-api/src/models/projects_users.rs b/libft-api/src/models/projects_users.rs index cce7fbb..1d3af79 100644 --- a/libft-api/src/models/projects_users.rs +++ b/libft-api/src/models/projects_users.rs @@ -1,8 +1,7 @@ +use crate::models::prelude::*; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; -use crate::prelude::*; - #[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct FtProjectsUser { pub created_at: FtDateTimeUtc, diff --git a/libft-api/src/models/scale.rs b/libft-api/src/models/scale.rs index c76fda7..87a765a 100644 --- a/libft-api/src/models/scale.rs +++ b/libft-api/src/models/scale.rs @@ -1,8 +1,7 @@ +use crate::models::prelude::*; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; -use crate::*; - #[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct FtScale { pub id: FtScaleId, diff --git a/libft-api/src/models/scale_teams.rs b/libft-api/src/models/scale_teams.rs index 991bb50..80cdc50 100644 --- a/libft-api/src/models/scale_teams.rs +++ b/libft-api/src/models/scale_teams.rs @@ -1,8 +1,7 @@ +use crate::models::prelude::*; use rvstruct::ValueStruct; use serde::{Deserialize, Deserializer, Serialize}; -use crate::*; - #[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct FtScaleTeam { pub id: FtScaleTeamId, diff --git a/libft-api/src/models/team.rs b/libft-api/src/models/team.rs index af1d64a..c515043 100644 --- a/libft-api/src/models/team.rs +++ b/libft-api/src/models/team.rs @@ -1,10 +1,7 @@ +use crate::models::prelude::*; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; -use crate::{FtFinalMark, FtProjectId, FtProjectSessionId, FtScaleTeam, FtUrl, FtUser}; - -use super::FtDateTimeUtc; - #[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct FtTeam { pub id: FtTeamId, @@ -2225,8 +2222,6 @@ fn test_ft_team_deserialization() { ], "corrector": { "id": 51965, - "login": "supervisor", - "url": "https://api.intra.42.fr/v2/users/supervisor" }, "truant": {}, "filled_at": null, @@ -2292,8 +2287,6 @@ fn test_ft_team_deserialization() { ], "corrector": { "id": 51965, - "login": "supervisor", - "url": "https://api.intra.42.fr/v2/users/supervisor" }, "truant": {}, "filled_at": null, diff --git a/libft-api/src/models/title.rs b/libft-api/src/models/title.rs index 1586c8d..e90b8d9 100644 --- a/libft-api/src/models/title.rs +++ b/libft-api/src/models/title.rs @@ -1,7 +1,7 @@ +use crate::models::prelude::*; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; -use super::FtDateTimeUtc; // // FtTitle and its field structs // diff --git a/libft-api/src/models/user.rs b/libft-api/src/models/user.rs index 9bb80a3..804bfaa 100644 --- a/libft-api/src/models/user.rs +++ b/libft-api/src/models/user.rs @@ -1,9 +1,8 @@ +use crate::models::prelude::*; use rsb_derive::Builder; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; -use crate::prelude::*; - #[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize, Builder)] pub struct FtUser { pub achievements: Option>, diff --git a/libft-api/src/prelude.rs b/libft-api/src/prelude.rs new file mode 100644 index 0000000..23a9bd2 --- /dev/null +++ b/libft-api/src/prelude.rs @@ -0,0 +1,6 @@ +pub use crate::api::prelude::*; +pub use crate::auth::*; +pub use crate::common::*; +pub use crate::connector::FtClientReqwestConnector; +pub use crate::info::*; +pub use crate::models::prelude::*; From 91a1ad07d9116f21e184abeb4905035da02d9a41 Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Tue, 14 Oct 2025 17:06:04 +0900 Subject: [PATCH 11/18] refactor(api): simplify connector and clean up modules This commit introduces several refactoring changes to the `libft-api` crate to simplify its internal structure and improve code quality. The `FtClientApiCallContext` struct has been removed from the `connector` module. This simplifies the API call process by removing the need to pass around context, contributing to a "borrow-less" design. Additionally, wildcard imports (`use crate::*;`) have been removed from test modules in favor of more specific imports. Unused imports have also been cleaned up across various files. The API submodules are now explicitly made public for better library structure. --- libft-api/src/api.rs | 18 +++++++++--------- libft-api/src/api/exam/exams.rs | 2 +- libft-api/src/api/group/groups.rs | 2 +- libft-api/src/api/project/project_data.rs | 2 +- libft-api/src/api/project/projects.rs | 2 +- libft-api/src/api/project/projects_id_teams.rs | 2 +- .../project_sessions_id_scale_teams.rs | 1 - libft-api/src/api/scale_team/scale_teams.rs | 2 +- libft-api/src/api/user/users_id.rs | 2 +- .../users_id_correction_point_historics.rs | 2 +- .../api/user/users_id_correction_points_add.rs | 2 +- .../src/api/user/users_id_projects_users.rs | 2 +- libft-api/src/api/user/users_id_teams.rs | 2 +- libft-api/src/connector.rs | 11 +---------- libft-api/src/models/group.rs | 1 - 15 files changed, 21 insertions(+), 32 deletions(-) diff --git a/libft-api/src/api.rs b/libft-api/src/api.rs index 29e3756..658a9a1 100644 --- a/libft-api/src/api.rs +++ b/libft-api/src/api.rs @@ -3,15 +3,15 @@ //! Each submodule mirrors an API domain (campus, user, project, exam, and so on) and exposes //! request/response types plus the associated `FtClientSession` helpers for issuing calls. -mod campus; -mod cursus; -mod exam; -mod group; -mod project; -mod project_session; -mod project_user; -mod scale_team; -mod user; +pub mod campus; +pub mod cursus; +pub mod exam; +pub mod group; +pub mod project; +pub mod project_session; +pub mod project_user; +pub mod scale_team; +pub mod user; pub mod prelude; diff --git a/libft-api/src/api/exam/exams.rs b/libft-api/src/api/exam/exams.rs index 2c3e970..85c31b4 100644 --- a/libft-api/src/api/exam/exams.rs +++ b/libft-api/src/api/exam/exams.rs @@ -110,7 +110,7 @@ where #[cfg(test)] mod tests { - use crate::*; + use super::*; diff --git a/libft-api/src/api/group/groups.rs b/libft-api/src/api/group/groups.rs index ac93aac..ccae62f 100644 --- a/libft-api/src/api/group/groups.rs +++ b/libft-api/src/api/group/groups.rs @@ -60,7 +60,7 @@ where #[cfg(test)] mod tests { - use crate::*; + use super::*; diff --git a/libft-api/src/api/project/project_data.rs b/libft-api/src/api/project/project_data.rs index f2a215f..7ec976e 100644 --- a/libft-api/src/api/project/project_data.rs +++ b/libft-api/src/api/project/project_data.rs @@ -63,7 +63,7 @@ where #[cfg(test)] mod tests { - use crate::*; + use super::*; diff --git a/libft-api/src/api/project/projects.rs b/libft-api/src/api/project/projects.rs index deaf416..eaf0e39 100644 --- a/libft-api/src/api/project/projects.rs +++ b/libft-api/src/api/project/projects.rs @@ -61,7 +61,7 @@ where #[cfg(test)] mod tests { - use crate::*; + use super::*; diff --git a/libft-api/src/api/project/projects_id_teams.rs b/libft-api/src/api/project/projects_id_teams.rs index 7e226c0..aa24ec4 100644 --- a/libft-api/src/api/project/projects_id_teams.rs +++ b/libft-api/src/api/project/projects_id_teams.rs @@ -63,7 +63,7 @@ where #[cfg(test)] mod tests { - use crate::*; + use super::*; diff --git a/libft-api/src/api/project_session/project_sessions_id_scale_teams.rs b/libft-api/src/api/project_session/project_sessions_id_scale_teams.rs index 7c2a326..c615d7d 100644 --- a/libft-api/src/api/project_session/project_sessions_id_scale_teams.rs +++ b/libft-api/src/api/project_session/project_sessions_id_scale_teams.rs @@ -1,5 +1,4 @@ use crate::prelude::*; -use crate::to_param; use libft_api_derive::HasVector; use rsb_derive::Builder; use serde::{Deserialize, Serialize}; diff --git a/libft-api/src/api/scale_team/scale_teams.rs b/libft-api/src/api/scale_team/scale_teams.rs index c12f2e0..ac63793 100644 --- a/libft-api/src/api/scale_team/scale_teams.rs +++ b/libft-api/src/api/scale_team/scale_teams.rs @@ -91,7 +91,7 @@ mod tests { use campus_id::GYEONGSAN; use super::*; - use crate::*; + #[tokio::test] async fn basic() { diff --git a/libft-api/src/api/user/users_id.rs b/libft-api/src/api/user/users_id.rs index b4e8d5d..cdb4e16 100644 --- a/libft-api/src/api/user/users_id.rs +++ b/libft-api/src/api/user/users_id.rs @@ -64,7 +64,7 @@ where mod tests { use super::*; - use crate::*; + #[tokio::test] async fn basic() { diff --git a/libft-api/src/api/user/users_id_correction_point_historics.rs b/libft-api/src/api/user/users_id_correction_point_historics.rs index a35ab60..4d8601d 100644 --- a/libft-api/src/api/user/users_id_correction_point_historics.rs +++ b/libft-api/src/api/user/users_id_correction_point_historics.rs @@ -63,7 +63,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::*; + #[tokio::test] async fn basic() { diff --git a/libft-api/src/api/user/users_id_correction_points_add.rs b/libft-api/src/api/user/users_id_correction_points_add.rs index 84d1a41..0a0446c 100644 --- a/libft-api/src/api/user/users_id_correction_points_add.rs +++ b/libft-api/src/api/user/users_id_correction_points_add.rs @@ -1,7 +1,7 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{prelude::*, to_param}; +use crate::prelude::*; #[derive(Debug, Serialize, Deserialize, Builder)] #[serde(transparent)] diff --git a/libft-api/src/api/user/users_id_projects_users.rs b/libft-api/src/api/user/users_id_projects_users.rs index 1275518..91e7714 100644 --- a/libft-api/src/api/user/users_id_projects_users.rs +++ b/libft-api/src/api/user/users_id_projects_users.rs @@ -71,7 +71,7 @@ where mod tests { use super::*; - use crate::*; + #[tokio::test] async fn basic() { diff --git a/libft-api/src/api/user/users_id_teams.rs b/libft-api/src/api/user/users_id_teams.rs index 6439151..7763be0 100644 --- a/libft-api/src/api/user/users_id_teams.rs +++ b/libft-api/src/api/user/users_id_teams.rs @@ -70,7 +70,7 @@ where mod tests { use super::*; - use crate::*; + #[tokio::test] async fn basic() { diff --git a/libft-api/src/connector.rs b/libft-api/src/connector.rs index 79cf592..c4d8aee 100644 --- a/libft-api/src/connector.rs +++ b/libft-api/src/connector.rs @@ -5,7 +5,7 @@ use reqwest::{ header::{self, AUTHORIZATION}, Client, RequestBuilder, StatusCode, }; -use tracing::{debug, info, Span}; +use tracing::{debug, info}; use url::Url; use crate::auth::FtApiToken; @@ -23,15 +23,6 @@ impl Default for FtClientReqwestConnector { } } -/// A context for a single API call. -#[derive(Debug, Clone)] -pub struct FtClientApiCallContext<'a> { - /// The tracing span for the call. - pub tracing_span: &'a Span, - /// The current page number, if the call is paginated. - pub current_page: Option, -} - impl FtClientReqwestConnector { /// Create a new `FtClientReqwestConnector` with a default `reqwest` client. #[must_use] diff --git a/libft-api/src/models/group.rs b/libft-api/src/models/group.rs index dc92581..c7f7678 100644 --- a/libft-api/src/models/group.rs +++ b/libft-api/src/models/group.rs @@ -1,4 +1,3 @@ -use crate::models::prelude::*; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; From c1ea2a3055f0a5df8eb93ad71909b4414407e3c3 Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Tue, 14 Oct 2025 17:08:55 +0900 Subject: [PATCH 12/18] fix(api): silence compiler warnings The result of the API call in the `users_id_correction_points_add` test is now ignored to fix an unused variable warning. Additionally, the `unexpected_cfgs` lint is allowed at the crate level to address warnings related to conditional compilation configurations. --- libft-api/src/api/user/users_id_correction_points_add.rs | 2 +- libft-api/src/lib.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/libft-api/src/api/user/users_id_correction_points_add.rs b/libft-api/src/api/user/users_id_correction_points_add.rs index 0a0446c..05cda90 100644 --- a/libft-api/src/api/user/users_id_correction_points_add.rs +++ b/libft-api/src/api/user/users_id_correction_points_add.rs @@ -59,7 +59,7 @@ mod tests { )); let session = client.open_session(token); - let res = session + let _ = session .users_id_correction_points_add(FtApiUsersIdCorrectionPointsAddRequest { id: FtUserId::new(crate::info::TEST_USER_YONDOO_ID), reason: FtCorrectionPointsReason::new("test".to_owned()), diff --git a/libft-api/src/lib.rs b/libft-api/src/lib.rs index 184c968..4bcf13c 100644 --- a/libft-api/src/lib.rs +++ b/libft-api/src/lib.rs @@ -39,6 +39,7 @@ //! Explore the `bin/` directory for runnable examples of each workflow, and enable tracing with //! `RUST_LOG=info` to inspect HTTP activity during development. #![feature(macro_metavar_expr_concat)] +#![allow(unexpected_cfgs)] pub mod api; pub mod models; From 5eb22741995bdf68ae847493eb54fbdbca98495c Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Tue, 14 Oct 2025 17:31:29 +0900 Subject: [PATCH 13/18] feat(api): overhaul documentation and expose auth module This commit introduces a major overhaul of the project's documentation and refines the public API. The README.md has been completely rewritten to provide a comprehensive guide for new users, including sections on features, installation, usage examples, API status, project structure, and contribution guidelines. Extensive rustdoc comments have been added to the `auth.rs` and `lib.rs` modules to improve inline documentation and auto-generated docs. The `auth` module is now public, allowing users to directly interact with authentication types like `AuthInfo` for more flexible credential management. The main library documentation has been updated to reflect the current module structure. --- libft-api/README.md | 192 ++++++++++++++++++++++++++++++++++-------- libft-api/src/auth.rs | 82 +++++++++++++++++- libft-api/src/lib.rs | 15 +++- 3 files changed, 250 insertions(+), 39 deletions(-) diff --git a/libft-api/README.md b/libft-api/README.md index c0107e7..be7faf3 100644 --- a/libft-api/README.md +++ b/libft-api/README.md @@ -1,6 +1,19 @@ -# ft-api +# libft-api - 42 Intra API Rust Library -I've made the 42 API usable with Rust. +A Rust library that provides typed, asynchronous access to the [42 Intra API](https://api.intra.42.fr/). It wraps common endpoints with strongly typed requests, automatic rate limiting, and reusable session management. + +## Features + +- **Strong Typing**: All API requests and responses are strongly typed using Rust structs +- **Rate Limiting**: Automatic handling of API rate limits +- **Session Management**: Reusable sessions for making multiple API calls +- **Async Support**: Fully asynchronous API calls using async/await +- **Caching**: Automatic token caching and refresh +- **Error Handling**: Comprehensive error types for different failure scenarios + +## Getting Started + +### Prerequisites You need the following two environment variables: @@ -9,54 +22,161 @@ FT_API_CLIENT_UID FT_API_CLIENT_SECRET ``` -## Example +These can be obtained by creating an application in your [42 profile settings](https://profile.intra.42.fr/oauth/applications). + +### Installation + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +libft-api = { git = "https://github.com/hdoo42/libft-api" } +tokio = { version = "1.0", features = ["full"] } +``` + +### Usage -Create a token -> Create a client -> Create a session (simple wrapper) -> Send API requests! +Create a token -> Create a client -> Create a session -> Send API requests! ```rust - //build a token - let res = FtApiToken::build_from_env().await; - - if let Ok(token) = res { - println!("token ok"); - let client = FtClient::new(FtClientReqwestConnector::with_connector( - reqwest::Client::new(), - )); - - let session = client.open_session(&token); - let res = session.campus_gs_locations().await?; - // res will contain all the locations for campus gs(Gyeongsan) +use libft_api::prelude::*; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Build a token from environment variables + let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + + // Create a client + let client = FtClient::new(FtClientReqwestConnector::new()); + + // Create a session + let session = client.open_session(token); + + // Send an API request + let response = session + .campus_id_locations( + FtApiCampusIdLocationsRequest::new(FtCampusId::new(GYEONGSAN)) + .with_per_page(100) + ) + .await?; + + for location in response.location { + println!("{} @ {}", location.user.login, location.host); } + + Ok(()) +} ``` -## Plans +## API Implementation Status + +### Available Endpoints + +The library currently supports the following API endpoints: + +#### Campus API +- `GET /campus/:campus_id/journals` +- `GET /campus/:campus_id/locations` +- `GET /campus/:campus_id/users` +- `GET /campus/:campus_id` +- `GET /campus/users` + +#### User API +- `GET /users` +- `GET /users/:user_id` +- `GET /users/:user_id/correction_point_historics` +- `POST /users/:user_id/correction_points_add` +- `GET /users/:user_id/locations` +- `GET /users/:user_id/locations_stats` +- `GET /users/:user_id/teams` +- `GET /users/:user_id/cursus_users` +- `GET /users/:user_id/projects_users` + +#### Project API +- `GET /projects` +- `GET /projects/:project_id/teams` +- `GET /project_data` + +#### Cursus API +- `GET /cursus/:cursus_id/projects` + +#### Exam API +- `GET /exams` + +#### Group API +- `GET /groups` + +#### Project Session API +- `GET /project_sessions/:project_session_id/scale_teams` +- `GET /project_sessions/:project_session_id/teams` -There are two major components that need to be implemented: +#### Project User API +- `GET /projects_users` -1. API Request Implementation - This involves setting up the functions and methods necessary to send requests to the 42 API. +#### Scale Team API +- `GET /scale_teams` + +### In Progress + +- Additional v3 API coverage +- More endpoint implementations + +## Project Structure + +- `src/api/` - Endpoint-specific clients for different API domains (campus, user, projects, etc.) +- `src/models/` - Serde-powered representations of API request and response data structures +- `src/auth.rs` - OAuth2 token management and authentication helpers +- `src/common.rs` - Shared utilities, error types, parameters, rate limiters, and pagination +- `src/connector.rs` - HTTP connector implementation using reqwest +- `src/info.rs` - Constants and information about 42 campuses and cursus +- `examples/` - Example implementations demonstrating library usage + +## Examples + +Check the `examples/` directory for more detailed usage examples: + +```bash +cargo run --example scroll +``` + +This example demonstrates how to use the library to get all users from the Seoul campus and save them to a JSON file. + +## Development + +### Running Tests + +```bash +cargo test +``` + +Note: Authentication tests require valid API credentials to be set in your environment. + +### Building Documentation + +```bash +cargo doc --open +``` -2. Data Structures for API Responses - This entails defining Rust structs that will map to the JSON data returned by the 42 API. These structures will be used to deserialize the API responses into Rust object. +## Contributing -## What is done? +Contributions are very welcome! Here are some ways you can contribute: -- oauth +- Report bugs and request features +- Submit pull requests for new API endpoints +- Improve documentation +- Add more comprehensive tests -### v2 +To contribute a new API endpoint, you'll typically need to: -- campus/:campus_id:/locations -- locations -- campus/:campus_id:/locations -- campus/:campus_id:/locations -- campus/:campus_id:/locations -- campus/:campus_id:/locations -- campus/:campus_id:/locations +1. Define the request and response data structures in `src/models/` +2. Create the request/response types in the appropriate module in `src/api/` +3. Implement the API call in the `FtClientSession` extension trait +4. Add tests and examples as appropriate -### v3 +## License -## Contribute? +This project is licensed under the MIT License - see the LICENSE file for details. -Contributions are very welcome. +## Support -Let me know if you need any more help! +If you need any more help, feel free to open an issue in the repository. diff --git a/libft-api/src/auth.rs b/libft-api/src/auth.rs index 4ab40c0..f313cb7 100644 --- a/libft-api/src/auth.rs +++ b/libft-api/src/auth.rs @@ -1,3 +1,11 @@ +//! Authentication module for the 42 Intra API. +//! +//! This module provides functionality for: +//! * Managing OAuth2 client credentials +//! * Building API tokens from environment variables +//! * Caching tokens to disk +//! * Handling token expiration and renewal + use serde_json::Error as SerdeError; use std::{ fmt::Display, @@ -9,8 +17,25 @@ use std::{ use chrono::{DateTime, TimeZone, Utc}; use serde::{Deserialize, Serialize}; -// TODO: add scope +// TODO: add scope /// Authentication information for the 42 API. +/// +/// Contains the client credentials (UID and secret) required to obtain an API token. +/// +/// # Example +/// +/// ```rust +/// use libft_api::prelude::*; +/// +/// // Create from environment variables +/// let auth_info = AuthInfo::build_from_env().unwrap(); +/// +/// // Or create directly with credentials +/// let auth_info = AuthInfo::from_env( +/// "your_client_id".to_string(), +/// "your_client_secret".to_string() +/// ); +/// ``` pub struct AuthInfo { uid: String, secret: String, @@ -18,15 +43,48 @@ pub struct AuthInfo { impl AuthInfo { /// Create a new `AuthInfo` from the given UID and secret. + /// + /// # Arguments + /// + /// * `uid` - The client ID for the 42 API application + /// * `secret` - The client secret for the 42 API application + /// + /// # Example + /// + /// ```rust + /// use libft_api::auth::AuthInfo; + /// + /// let auth_info = AuthInfo::from_env( + /// "your_client_id".to_string(), + /// "your_client_secret".to_string() + /// ); + /// ``` pub fn from_env(uid: String, secret: String) -> AuthInfo { AuthInfo { uid, secret } } /// Build `AuthInfo` from environment variables. /// + /// This function reads the `FT_API_CLIENT_UID` and `FT_API_CLIENT_SECRET` environment variables + /// to create an `AuthInfo` instance. + /// + /// # Environment Variables + /// + /// * `FT_API_CLIENT_UID` - The client ID for the 42 API application + /// * `FT_API_CLIENT_SECRET` - The client secret for the 42 API application + /// /// # Errors /// /// This function will return an error if the `FT_API_CLIENT_UID` or `FT_API_CLIENT_SECRET` environment variables are not set. + /// + /// # Example + /// + /// ```rust + /// use libft_api::auth::AuthInfo; + /// + /// // Requires FT_API_CLIENT_UID and FT_API_CLIENT_SECRET to be set in the environment + /// let auth_info = AuthInfo::build_from_env().unwrap(); + /// ``` pub fn build_from_env() -> Result { let uid = config_env_var("FT_API_CLIENT_UID")?; let secret = config_env_var("FT_API_CLIENT_SECRET")?; @@ -37,6 +95,12 @@ impl AuthInfo { #[inline] // TODO: replace scope to field 'scope' /// Get the parameters for the API token request. + /// + /// Returns the form parameters required to request an OAuth2 token from the 42 API. + /// + /// # Returns + /// + /// An array of key-value pairs representing the form parameters for the token request. pub fn get_params(&self) -> [(&str, &str); 4] { [ ("grant_type", "client_credentials"), @@ -49,6 +113,11 @@ impl AuthInfo { #[derive(Debug, PartialEq, PartialOrd, Clone, Serialize, Deserialize)] /// Represents an API token from the 42 API. +/// +/// This struct holds the OAuth2 access token and related metadata required to make authenticated +/// requests to the 42 Intra API. It includes expiration information and token type. +/// +/// The token is automatically cached to disk and reused until expiration. pub struct FtApiToken { access_token: String, token_type: AccessTokenType, @@ -60,6 +129,13 @@ pub struct FtApiToken { impl FtApiToken { /// Get the token value as a string. + /// + /// Returns the token in the format "TokenType AccessToken", which is the format required + /// for the Authorization header in API requests. + /// + /// # Returns + /// + /// A formatted string containing the token type and access token. pub fn get_token_value(&self) -> String { format!("{} {}", self.token_type, self.access_token) } @@ -79,6 +155,9 @@ impl Display for AccessTokenType { #[derive(Debug)] /// Represents an error that can occur when handling an API token. +/// +/// This enum covers various error conditions that can occur during token management, +/// including I/O errors, serialization errors, token expiration, and build failures. pub enum TokenError { /// An I/O error occurred. IOError(io::Error), @@ -93,6 +172,7 @@ pub enum TokenError { /// An error occurred while building the token. BuildError(String), } + impl From for TokenError { fn from(err: io::Error) -> Self { TokenError::IOError(err) diff --git a/libft-api/src/lib.rs b/libft-api/src/lib.rs index 4bcf13c..8edda2f 100644 --- a/libft-api/src/lib.rs +++ b/libft-api/src/lib.rs @@ -30,11 +30,22 @@ //! token. The default `FtClientReqwestConnector` reuses a shared `reqwest` client and applies the //! crate's rate limiter, so you stay within the platform quotas. //! +//! ## Features +//! * **Strong Typing**: All API requests and responses are strongly typed using Rust structs +//! * **Rate Limiting**: Automatic handling of API rate limits +//! * **Session Management**: Reusable sessions for making multiple API calls +//! * **Async Support**: Fully asynchronous API calls using async/await +//! * **Caching**: Automatic token caching and refresh +//! * **Error Handling**: Comprehensive error types for different failure scenarios +//! //! ## Modules //! * `api` — high-level endpoint clients grouped by 42 domain (campus, user, projects, exams). //! * `models` — serde-powered representations of request and response payloads. //! * `auth` — helpers for building OAuth tokens and refreshing sessions. -//! * `axum_support` — Axum extractors and middleware for wiring the client into services. +//! * `common` — shared utilities, error types, parameters, rate limiters, and pagination. +//! * `connector` — HTTP connector implementations (currently reqwest-based). +//! * `info` — constants and information about 42 campuses and cursus. +//! * `prelude` — convenient glob imports for common functionality. //! //! Explore the `bin/` directory for runnable examples of each workflow, and enable tracing with //! `RUST_LOG=info` to inspect HTTP activity during development. @@ -44,7 +55,7 @@ pub mod api; pub mod models; -mod auth; +pub mod auth; mod common; pub mod info; pub mod prelude; From 45a366ad9366bba34baab2d88d1bba5ce67f73a7 Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Tue, 14 Oct 2025 18:06:39 +0900 Subject: [PATCH 14/18] docs(api): add module-level and type documentation This commit introduces comprehensive documentation across several key modules of the `libft-api` crate. Module-level explanations, examples, and type-level doc comments have been added to improve usability and developer experience. The following areas have been updated: - `api`: Added module overview and `HasVec` trait documentation. - `common`: Added module overview with an example. - `connector`: Added module overview explaining its role. - `models::locations`: Documented location-related data structures. - `models::user`: Documented user-related data structures. --- libft-api/src/api.rs | 53 +++++++++++++++++++++++++++++++ libft-api/src/common.rs | 24 ++++++++++++++ libft-api/src/connector.rs | 32 +++++++++++++++++++ libft-api/src/models/locations.rs | 13 ++++++++ libft-api/src/models/user.rs | 9 ++++++ 5 files changed, 131 insertions(+) diff --git a/libft-api/src/api.rs b/libft-api/src/api.rs index 658a9a1..a6765ed 100644 --- a/libft-api/src/api.rs +++ b/libft-api/src/api.rs @@ -2,6 +2,34 @@ //! //! Each submodule mirrors an API domain (campus, user, project, exam, and so on) and exposes //! request/response types plus the associated `FtClientSession` helpers for issuing calls. +//! +//! This module provides structured access to various 42 Intra API endpoints organized by domain: +//! * **Campus**: Information about 42 campuses and their locations +//! * **Cursus**: Curriculum-related information and user cursus associations +//! * **User**: User profiles and related data +//! * **Project**: Project information and user project associations +//! * **Exam**: Exam session information +//! * **Group**: Group-related functionality +//! * **Scale Team**: Evaluation team functionality +//! * **Project Session**: Project session data +//! +//! # Example +//! +//! ```rust +//! use libft_api::prelude::*; +//! +//! async fn example() -> ClientResult<()> { +//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let client = FtClient::new(FtClientReqwestConnector::new()); +//! let session = client.open_session(token); +//! +//! // Access user endpoint through the session +//! let user_response = session.users_id(FtUsersIdRequest::new(12345)).await?; +//! println!("User login: {}", user_response.login); +//! +//! Ok(()) +//! } +//! ``` pub mod campus; pub mod cursus; @@ -16,7 +44,32 @@ pub mod user; pub mod prelude; /// Convenience abstraction for wrapper types that contain a `Vec` under a single field. +/// +/// This trait simplifies access to vector fields in API response types. +/// +/// # Example +/// +/// ```rust +/// use libft_api::api::HasVec; +/// +/// struct FtApiUsersResponse { +/// users: Vec, +/// } +/// +/// impl HasVec for FtApiUsersResponse { +/// fn get_vec(&self) -> &Vec { +/// &self.users +/// } +/// +/// fn take_vec(self) -> Vec { +/// self.users +/// } +/// } +/// ``` pub trait HasVec { + /// Get a reference to the contained vector. fn get_vec(&self) -> &Vec; + + /// Take ownership of the contained vector. fn take_vec(self) -> Vec; } diff --git a/libft-api/src/common.rs b/libft-api/src/common.rs index f07d143..bdb0fbc 100644 --- a/libft-api/src/common.rs +++ b/libft-api/src/common.rs @@ -1,3 +1,27 @@ +//! Common functionality used across the 42 Intra API client. +//! +//! This module provides shared utilities that are used throughout the libft-api crate: +//! * **Client**: Core HTTP client and session management functionality +//! * **Error**: Comprehensive error types for various failure scenarios +//! * **Parameter**: Types and utilities for building API query parameters +//! * **Rate Limiter**: Automatic rate limiting to stay within API quotas +//! * **Paginator**: Utilities for handling paginated API responses +//! +//! # Example +//! +//! ```rust +//! use libft_api::prelude::*; +//! +//! async fn example() -> ClientResult<()> { +//! // Create a client with custom rate limits +//! let client = FtClient::with_ratelimits(FtClientReqwestConnector::new(), 5, 1000); +//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let session = client.open_session(token); +//! +//! Ok(()) +//! } +//! ``` + pub use client::*; mod client; diff --git a/libft-api/src/connector.rs b/libft-api/src/connector.rs index c4d8aee..b40dc43 100644 --- a/libft-api/src/connector.rs +++ b/libft-api/src/connector.rs @@ -1,3 +1,35 @@ +//! HTTP connector implementation for the 42 Intra API client. +//! +//! This module provides the HTTP connector implementation that handles actual network communication +//! with the 42 Intra API using the `reqwest` HTTP client. It is responsible for: +//! * Making HTTP requests to the API endpoints +//! * Handling authentication via API tokens +//! * Managing rate limits and retry logic +//! * Parsing API responses and handling errors +//! * Updating rate limit metadata from response headers +//! +//! The connector automatically handles: +//! * Token-based authentication using Bearer tokens +//! * Rate limiting based on response headers +//! * JSON response deserialization +//! * HTTP status code handling +//! * Logging of API requests and responses +//! +//! # Example +//! +//! ```rust +//! use libft_api::prelude::*; +//! use reqwest::Client; +//! +//! // Create a custom connector with specific configuration +//! let http_client = Client::builder() +//! .timeout(std::time::Duration::from_secs(30)) +//! .build() +//! .unwrap(); +//! let connector = FtClientReqwestConnector::with_connector(http_client); +//! let client = FtClient::new(connector); +//! ``` + use std::time::Duration; use futures::FutureExt; diff --git a/libft-api/src/models/locations.rs b/libft-api/src/models/locations.rs index a0331b4..81365dd 100644 --- a/libft-api/src/models/locations.rs +++ b/libft-api/src/models/locations.rs @@ -1,7 +1,15 @@ +//! Data structures for 42 API location-related entities. +//! +//! This module contains data structures that represent location information +//! from the 42 Intra API, including user locations and related identifiers. + use crate::models::prelude::*; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; +/// Represents a location record from the 42 Intra API. +/// +/// A location represents where a user is currently logged in or was last active. #[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct FtLocation { pub id: FtLocationId, @@ -13,8 +21,13 @@ pub struct FtLocation { pub user: FtUser, } +/// A unique identifier for a location record. #[derive(Debug, Eq, Hash, PartialEq, PartialOrd, Clone, Serialize, Deserialize, ValueStruct)] pub struct FtLocationId(i64); +/// Represents a host or computer where a user is located. +/// +/// # Example +/// c1r1s1 (cluster 1, row 1, seat 1) #[derive(Debug, Eq, Hash, PartialEq, PartialOrd, Clone, Serialize, Deserialize, ValueStruct)] pub struct FtHost(pub String); diff --git a/libft-api/src/models/user.rs b/libft-api/src/models/user.rs index 804bfaa..aa19642 100644 --- a/libft-api/src/models/user.rs +++ b/libft-api/src/models/user.rs @@ -1,8 +1,17 @@ +//! Data structures for 42 API user-related entities. +//! +//! This module contains data structures that represent user information +//! from the 42 Intra API, including user profiles and related identifiers. + use crate::models::prelude::*; use rsb_derive::Builder; use rvstruct::ValueStruct; use serde::{Deserialize, Serialize}; +/// Represents a user from the 42 Intra API. +/// +/// Contains comprehensive information about a 42 school user including personal details, +/// academic information, achievements, and more. #[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize, Builder)] pub struct FtUser { pub achievements: Option>, From 348697fa2bbb02dfaf4ea07ad627c4911bed1a7e Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Tue, 14 Oct 2025 18:29:48 +0900 Subject: [PATCH 15/18] docs(api): add comprehensive documentation to public API This commit introduces extensive documentation across the public API of the `libft-api` and `libft-api-derive` crates. Key changes include: - Added module-level documentation explaining the purpose of each API endpoint group (e.g., campus, user, project). - Added detailed doc comments for each public API client method, including descriptions of parameters, return values, and usage examples. - Documented the `HasVector` procedural macro in `libft-api-derive`. - Added crate-level and prelude documentation to improve discoverability and usability. This effort aims to make the library easier to understand and use for developers by providing clear, in-code explanations and examples for all major functionalities. --- libft-api-derive/src/lib.rs | 56 ++++++++++++ libft-api/src/api/campus.rs | 48 +++++++++- libft-api/src/api/campus/campus_id.rs | 38 ++++++++ .../src/api/campus/campus_id_locations.rs | 49 ++++++++++ libft-api/src/api/campus/campus_id_users.rs | 48 ++++++++++ libft-api/src/api/campus/campus_users.rs | 46 ++++++++++ libft-api/src/api/cursus.rs | 29 ++++++ libft-api/src/api/exam.rs | 38 ++++++++ libft-api/src/api/exam/exams.rs | 83 +++++++++++++---- libft-api/src/api/group.rs | 43 +++++++++ libft-api/src/api/prelude.rs | 33 +++++++ libft-api/src/api/project.rs | 34 +++++++ libft-api/src/api/project_session.rs | 33 +++++++ libft-api/src/api/project_user.rs | 45 +++++++++ libft-api/src/api/scale_team.rs | 46 ++++++++++ libft-api/src/api/user.rs | 47 ++++++++++ libft-api/src/api/user/users.rs | 91 +++++++++++++++++++ libft-api/src/api/user/users_id.rs | 49 ++++++++++ libft-api/src/models.rs | 29 ++++++ libft-api/src/prelude.rs | 34 +++++++ 20 files changed, 896 insertions(+), 23 deletions(-) diff --git a/libft-api-derive/src/lib.rs b/libft-api-derive/src/lib.rs index ad5a25d..b056d81 100644 --- a/libft-api-derive/src/lib.rs +++ b/libft-api-derive/src/lib.rs @@ -1,3 +1,31 @@ +//! Procedural macros for the `libft-api` crate. +//! +//! This crate provides procedural macros that are used to reduce boilerplate code +//! in the main `libft-api` crate. The macros are implemented as derive macros +//! that automatically generate trait implementations for data structures. +//! +//! # Available Macros +//! +//! * `HasVector` - Derives the `HasVec` trait for structs that contain exactly one `Vec` field +//! +//! # Example +//! +//! ```rust +//! use libft_api_derive::HasVector; +//! use libft_api::api::HasVec; +//! +//! #[derive(HasVector)] +//! struct FtApiUsersResponse { +//! users: Vec, +//! } +//! +//! // This generates an implementation of the HasVec trait automatically: +//! // impl HasVec for FtApiUsersResponse { +//! // fn get_vec(&self) -> &Vec { &self.users } +//! // fn take_vec(self) -> Vec { self.users } +//! // } +//! ``` + extern crate proc_macro; use proc_macro::TokenStream; @@ -7,6 +35,34 @@ use syn::{ PathArguments, Type, }; +/// Derives the `HasVec` trait for structs that contain exactly one `Vec` field. +/// +/// This macro automatically implements the `HasVec` trait for structs that have +/// exactly one named field of type `Vec`. The generated implementation provides +/// methods to access and take ownership of the vector field. +/// +/// # Requirements +/// * The struct must have exactly one field of type `Vec` +/// * The struct must have named fields (not tuple or unit structs) +/// * The field type must be exactly `Vec`, not an alias or reference +/// +/// # Example +/// +/// ```rust +/// use libft_api_derive::HasVector; +/// use libft_api::api::HasVec; +/// +/// #[derive(HasVector)] +/// struct FtApiUsersResponse { +/// users: Vec, +/// } +/// +/// // This generates: +/// // impl HasVec for FtApiUsersResponse { +/// // fn get_vec(&self) -> &Vec { &self.users } +/// // fn take_vec(self) -> Vec { self.users } +/// // } +/// ``` #[proc_macro_derive(HasVector)] pub fn has_vec_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); diff --git a/libft-api/src/api/campus.rs b/libft-api/src/api/campus.rs index e1c0923..f0dabd4 100644 --- a/libft-api/src/api/campus.rs +++ b/libft-api/src/api/campus.rs @@ -1,10 +1,48 @@ -mod campus_id_journals; +//! API endpoints related to campus information. +//! +//! This module provides access to the 42 Intra API endpoints that deal with campus data. +//! It includes functionality for retrieving information about specific campuses, campus locations, +//! users associated with campuses, and campus journals. +//! +//! # Endpoints +//! +//! * **campus_id**: Retrieve information about a specific campus by its ID +//! * **campus_id_locations**: Get location information for a specific campus +//! * **campus_id_users**: Get users associated with a specific campus +//! * **campus_id_journals**: Retrieve journal information for a specific campus +//! * **campus_users**: Get campus user associations +//! +//! # Example +//! +//! ```rust +//! use libft_api::prelude::*; +//! +//! async fn example() -> ClientResult<()> { +//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let client = FtClient::new(FtClientReqwestConnector::new()); +//! let session = client.open_session(token); +//! +//! // Get all campuses +//! let response = session.campus_id(FtApiCampusIdRequest::new()).await?; +//! println!("Retrieved {} campuses", response.campus.len()); +//! +//! // Get specific campus (e.g., Paris campus) +//! let paris_response = session +//! .campus_id(FtApiCampusIdRequest::new().with_campus_id(FtCampusId::new(1))) +//! .await?; +//! println!("Paris campus: {:?}", paris_response.campus.first()); +//! +//! Ok(()) +//! } +//! ``` + +pub mod campus_id_journals; pub use campus_id_journals::*; -mod campus_id_locations; +pub mod campus_id_locations; pub use campus_id_locations::*; -mod campus_id_users; +pub mod campus_id_users; pub use campus_id_users::*; -mod campus_id; +pub mod campus_id; pub use campus_id::*; -mod campus_users; +pub mod campus_users; pub use campus_users::*; diff --git a/libft-api/src/api/campus/campus_id.rs b/libft-api/src/api/campus/campus_id.rs index 3584570..5e644e2 100644 --- a/libft-api/src/api/campus/campus_id.rs +++ b/libft-api/src/api/campus/campus_id.rs @@ -24,6 +24,44 @@ impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { + /// Retrieves information about campuses from the 42 Intra API. + /// + /// # Parameters + /// - `req`: A `FtApiCampusIdRequest` struct containing the query parameters. + /// + /// # Query Parameters + /// - `campus_id`: Optional campus ID to retrieve information about a specific campus + /// - `sort`: Optional vector of sort options to order the results + /// - `range`: Optional vector of range options to filter results by date ranges + /// - `filter`: Optional vector of filter options to filter the results + /// - `page`: Optional page number for pagination + /// - `per_page`: Optional number of items per page for pagination + /// + /// # Returns + /// - `ClientResult`: Contains a vector of `FtCampus` objects + /// + /// # Example + /// ```rust + /// use libft_api::prelude::*; + /// + /// async fn example() -> ClientResult<()> { + /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let client = FtClient::new(FtClientReqwestConnector::new()); + /// let session = client.open_session(token); + /// + /// // Get all campuses + /// let response = session.campus_id(FtApiCampusIdRequest::new()).await?; + /// println!("Total campuses: {}", response.campus.len()); + /// + /// // Get a specific campus (e.g., Paris campus with ID 1) + /// let paris_response = session + /// .campus_id(FtApiCampusIdRequest::new().with_campus_id(FtCampusId::new(1))) + /// .await?; + /// println!("Paris campus name: {:?}", paris_response.campus.first().unwrap().name); + /// + /// Ok(()) + /// } + /// ``` pub async fn campus_id( &self, req: FtApiCampusIdRequest, diff --git a/libft-api/src/api/campus/campus_id_locations.rs b/libft-api/src/api/campus/campus_id_locations.rs index da994fa..02776e4 100644 --- a/libft-api/src/api/campus/campus_id_locations.rs +++ b/libft-api/src/api/campus/campus_id_locations.rs @@ -26,6 +26,55 @@ impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { + /// Retrieves location information for a specific campus from the 42 Intra API. + /// + /// This method fetches location data for a specific campus, including information about + /// where users are currently located on that campus. + /// + /// # Parameters + /// - `req`: A `FtApiCampusIdLocationsRequest` struct containing the query parameters. + /// + /// # Query Parameters + /// - `campus_id`: The ID of the campus to retrieve location information for (required) + /// - `user_id`: Optional user ID to filter locations for a specific user + /// - `sort`: Optional vector of sort options to order the results + /// - `range`: Optional vector of range options to filter results by date ranges + /// - `filter`: Optional vector of filter options to filter the results + /// - `page`: Optional page number for pagination + /// - `per_page`: Optional number of items per page for pagination + /// + /// # Returns + /// - `ClientResult`: Contains a vector of `FtLocation` objects + /// + /// # Example + /// ```rust + /// use libft_api::prelude::*; + /// + /// async fn example() -> ClientResult<()> { + /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let client = FtClient::new(FtClientReqwestConnector::new()); + /// let session = client.open_session(token); + /// + /// // Get all locations for a specific campus (e.g., GyeongSan campus with ID 69) + /// let locations_response = session + /// .campus_id_locations( + /// FtApiCampusIdLocationsRequest::new(FtCampusId::new(69)) + /// .with_per_page(50) + /// ) + /// .await?; + /// println!("Found {} locations", locations_response.location.len()); + /// + /// // Get locations for a specific user in a specific campus + /// let user_locations = session + /// .campus_id_locations( + /// FtApiCampusIdLocationsRequest::new(FtCampusId::new(69)) + /// .with_user_id(FtUserId::new(12345)) + /// ) + /// .await?; + /// + /// Ok(()) + /// } + /// ``` pub async fn campus_id_locations( &self, req: FtApiCampusIdLocationsRequest, diff --git a/libft-api/src/api/campus/campus_id_users.rs b/libft-api/src/api/campus/campus_id_users.rs index bc8512d..1f2308e 100644 --- a/libft-api/src/api/campus/campus_id_users.rs +++ b/libft-api/src/api/campus/campus_id_users.rs @@ -25,6 +25,54 @@ impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { + /// Retrieves user information for a specific campus from the 42 Intra API. + /// + /// This method fetches information about users associated with a specific campus. + /// + /// # Parameters + /// - `req`: A `FtApiCampusIdUsersRequest` struct containing the query parameters. + /// + /// # Query Parameters + /// - `campus_id`: The ID of the campus to retrieve users for (required) + /// - `user_id`: Optional user ID to filter results for a specific user + /// - `sort`: Optional vector of sort options to order the results + /// - `range`: Optional vector of range options to filter results by date ranges + /// - `filter`: Optional vector of filter options to filter the results + /// - `page`: Optional page number for pagination + /// - `per_page`: Optional number of items per page for pagination + /// + /// # Returns + /// - `ClientResult`: Contains a vector of `FtUser` objects + /// + /// # Example + /// ```rust + /// use libft_api::prelude::*; + /// + /// async fn example() -> ClientResult<()> { + /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let client = FtClient::new(FtClientReqwestConnector::new()); + /// let session = client.open_session(token); + /// + /// // Get all users for a specific campus (e.g., GyeongSan campus with ID 69) + /// let users_response = session + /// .campus_id_users( + /// FtApiCampusIdUsersRequest::new(FtCampusId::new(69)) + /// .with_per_page(100) + /// ) + /// .await?; + /// println!("Found {} users in the campus", users_response.users.len()); + /// + /// // Get a specific user in a specific campus + /// let specific_user = session + /// .campus_id_users( + /// FtApiCampusIdUsersRequest::new(FtCampusId::new(69)) + /// .with_user_id(FtUserId::new(12345)) + /// ) + /// .await?; + /// + /// Ok(()) + /// } + /// ``` pub async fn campus_id_users( &self, req: FtApiCampusIdUsersRequest, diff --git a/libft-api/src/api/campus/campus_users.rs b/libft-api/src/api/campus/campus_users.rs index c9aa023..4a05a21 100644 --- a/libft-api/src/api/campus/campus_users.rs +++ b/libft-api/src/api/campus/campus_users.rs @@ -25,6 +25,52 @@ impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { + /// Retrieves campus user associations from the 42 Intra API. + /// + /// This method fetches information about campus user associations, which link users to campuses. + /// If a user_id is provided, it retrieves campus associations for that specific user. + /// If no user_id is provided, it retrieves all campus user associations. + /// + /// # Parameters + /// - `req`: A `FtApiCampusUsersRequest` struct containing the query parameters. + /// + /// # Query Parameters + /// - `user_id`: Optional user ID to retrieve campus associations for a specific user + /// - `sort`: Optional vector of sort options to order the results + /// - `range`: Optional vector of range options to filter results by date ranges + /// - `filter`: Optional vector of filter options to filter the results + /// - `page`: Optional page number for pagination + /// - `per_page`: Optional number of items per page for pagination + /// + /// # Returns + /// - `ClientResult`: Contains a vector of `FtCampusUser` objects + /// + /// # Example + /// ```rust + /// use libft_api::prelude::*; + /// + /// async fn example() -> ClientResult<()> { + /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let client = FtClient::new(FtClientReqwestConnector::new()); + /// let session = client.open_session(token); + /// + /// // Get all campus-user associations + /// let campus_users_response = session + /// .campus_users(FtApiCampusUsersRequest::new()) + /// .await?; + /// println!("Found {} campus-user associations", campus_users_response.campus_users.len()); + /// + /// // Get campus associations for a specific user + /// let user_campus_assoc = session + /// .campus_users( + /// FtApiCampusUsersRequest::new() + /// .with_user_id(FtUserId::new(12345)) + /// ) + /// .await?; + /// + /// Ok(()) + /// } + /// ``` pub async fn campus_users( &self, req: FtApiCampusUsersRequest, diff --git a/libft-api/src/api/cursus.rs b/libft-api/src/api/cursus.rs index 4bd79e9..5c8e1e3 100644 --- a/libft-api/src/api/cursus.rs +++ b/libft-api/src/api/cursus.rs @@ -1,2 +1,31 @@ +//! API endpoints related to cursus information. +//! +//! This module provides access to the 42 Intra API endpoints that deal with curriculum data. +//! It includes functionality for retrieving information about projects associated with specific cursus. +//! +//! # Endpoints +//! +//! * **cursus_id_projects**: Retrieve projects associated with a specific cursus by its ID +//! +//! # Example +//! +//! ```rust +//! use libft_api::prelude::*; +//! +//! async fn example() -> ClientResult<()> { +//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let client = FtClient::new(FtClientReqwestConnector::new()); +//! let session = client.open_session(token); +//! +//! // Get projects for the common core cursus (ID 21) +//! let response = session +//! .cursus_id_projects(FtApiCursusIdProjectsRequest::new(FtCursusId::new(21))) +//! .await?; +//! println!("Found {} projects in the cursus", response.projects.len()); +//! +//! Ok(()) +//! } +//! ``` + mod cursus_id_projects; pub use cursus_id_projects::*; diff --git a/libft-api/src/api/exam.rs b/libft-api/src/api/exam.rs index e9ebd6a..9b9e922 100644 --- a/libft-api/src/api/exam.rs +++ b/libft-api/src/api/exam.rs @@ -1,2 +1,40 @@ +//! API endpoints related to exam information. +//! +//! This module provides access to the 42 Intra API endpoints that deal with exam data. +//! It includes functionality for retrieving exam information and managing exam-user associations. +//! +//! # Endpoints +//! +//! * **exams**: Retrieve a list of exams with filtering, pagination, and sorting options +//! * **exams_users_post**: Create an association between a user and an exam +//! +//! # Example +//! +//! ```rust +//! use libft_api::prelude::*; +//! +//! async fn example() -> ClientResult<()> { +//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let client = FtClient::new(FtClientReqwestConnector::new()); +//! let session = client.open_session(token); +//! +//! // Get all exams +//! let response = session.exams(FtApiExamsRequest::new()).await?; +//! println!("Found {} exams", response.exams.len()); +//! +//! // Create an exam-user association (if you have the appropriate permissions) +//! // let exam_user_response = session +//! // .exams_users_post( +//! // FtApiExamsUsersPostRequest::new(FtApiExamsUsersPostBody { +//! // user_id: FtUserId::new(12345), +//! // }), +//! // FtExamId::new(22085), +//! // ) +//! // .await?; +//! +//! Ok(()) +//! } +//! ``` + mod exams; pub use exams::*; diff --git a/libft-api/src/api/exam/exams.rs b/libft-api/src/api/exam/exams.rs index 85c31b4..e72ad0c 100644 --- a/libft-api/src/api/exam/exams.rs +++ b/libft-api/src/api/exam/exams.rs @@ -40,30 +40,42 @@ impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { - /// ``` - /// #[tokio::test] - /// async fn post_exams() { - /// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) - /// .await - /// .unwrap(); + /// Retrieves a list of exams from the 42 Intra API. + /// + /// This method fetches information about exams with various filtering and pagination options. + /// + /// # Parameters + /// - `req`: A `FtApiExamsRequest` struct containing the query parameters. + /// + /// # Query Parameters + /// - `sort`: Optional vector of sort options to order the results + /// - `range`: Optional vector of range options to filter results by date ranges + /// - `filter`: Optional vector of filter options to filter the results + /// - `page`: Optional page number for pagination + /// - `per_page`: Optional number of items per page for pagination /// - /// let client = FtClient::new(FtClientReqwestConnector::with_connector( - /// reqwest::Client::new(), - /// )); + /// # Returns + /// - `ClientResult`: Contains a vector of `FtExam` objects /// + /// # Example + /// ```rust + /// use libft_api::prelude::*; + /// + /// async fn example() -> ClientResult<()> { + /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let client = FtClient::new(FtClientReqwestConnector::new()); /// let session = client.open_session(token); /// - /// let res = session - /// .exams_users_post( - /// FtApiExamsUsersPostRequest::new(FtApiExamsUsersPostBody { - /// user_id: FtUserId::new(212_750), - /// }), - /// FtExamId::new(22085), + /// // Get all exams with pagination + /// let exams_response = session + /// .exams( + /// FtApiExamsRequest::new() + /// .with_per_page(20) /// ) - /// .await - /// .unwrap(); + /// .await?; + /// println!("Found {} exams", exams_response.exams.len()); /// - /// assert_eq!(res.group.id, FtGroupId::new(FT_GROUP_ID_TEST_ACCOUNT)); + /// Ok(()) /// } /// ``` pub async fn exams(&self, req: FtApiExamsRequest) -> ClientResult { @@ -97,6 +109,41 @@ where .await } + /// Creates an association between a user and an exam from the 42 Intra API. + /// + /// This method creates an exam-user association, typically used to register a user for an exam. + /// + /// # Parameters + /// - `req`: A `FtApiExamsUsersPostRequest` struct containing the exam-user association data. + /// - `exam_id`: The ID of the exam to create the association for (required) + /// + /// # Returns + /// - `ClientResult`: Contains the created `FtExamUser` object + /// + /// # Example + /// ```rust + /// use libft_api::prelude::*; + /// + /// async fn example() -> ClientResult<()> { + /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let client = FtClient::new(FtClientReqwestConnector::new()); + /// let session = client.open_session(token); + /// + /// // Create an exam-user association (requires appropriate permissions) + /// // let exam_user_request = FtApiExamsUsersPostRequest::new( + /// // FtApiExamsUsersPostBody { + /// // user_id: FtUserId::new(12345), + /// // } + /// // ); + /// // let exam_user_response = session + /// // .exams_users_post(exam_user_request, FtExamId::new(12345)) + /// // .await?; + /// // + /// // println!("Created exam-user association with ID: {:?}", exam_user_response.exam.id); + /// + /// Ok(()) + /// } + /// ``` pub async fn exams_users_post( &self, req: FtApiExamsUsersPostRequest, diff --git a/libft-api/src/api/group.rs b/libft-api/src/api/group.rs index 6cb192a..0d34760 100644 --- a/libft-api/src/api/group.rs +++ b/libft-api/src/api/group.rs @@ -1,2 +1,45 @@ +//! API endpoints related to group information. +//! +//! This module provides access to the 42 Intra API endpoints that deal with group data. +//! It includes functionality for retrieving group information and managing group-user associations. +//! +//! # Endpoints +//! +//! * **groups**: Retrieve a list of groups with optional filtering by user ID and pagination options +//! * **groups_users_post**: Create an association between a user and a group +//! +//! # Example +//! +//! ```rust +//! use libft_api::prelude::*; +//! +//! async fn example() -> ClientResult<()> { +//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let client = FtClient::new(FtClientReqwestConnector::new()); +//! let session = client.open_session(token); +//! +//! // Get all groups +//! let response = session.groups(FtApiGroupsRequest::new()).await?; +//! println!("Found {} groups", response.groups.len()); +//! +//! // Get groups for a specific user +//! let user_groups = session +//! .groups(FtApiGroupsRequest::new().with_user_id(FtUserId::new(12345))) +//! .await?; +//! +//! // Create a group-user association (if you have the appropriate permissions) +//! // let group_user_response = session +//! // .groups_users_post(FtApiGroupsUsersPostRequest::new( +//! // FtApiGroupsUsersPostBody { +//! // group_id: FtGroupId::new(123), +//! // user_id: FtUserId::new(12345), +//! // }, +//! // )) +//! // .await?; +//! +//! Ok(()) +//! } +//! ``` + mod groups; pub use groups::*; diff --git a/libft-api/src/api/prelude.rs b/libft-api/src/api/prelude.rs index 92e9f0a..cc5eeac 100644 --- a/libft-api/src/api/prelude.rs +++ b/libft-api/src/api/prelude.rs @@ -1,3 +1,36 @@ +//! The prelude module for API endpoints in the `libft-api` crate. +//! +//! This module provides convenient glob imports for all API endpoint types, requests, and responses +//! from the various API domain modules (campus, cursus, exam, group, project, project_session, +//! project_user, scale_team, and user). By importing everything in this module, users can access +//! all API-related functionality without needing to import individual modules. +//! +//! The prelude includes: +//! * All request and response types for API endpoints +//! * All session methods for making API calls +//! * The `HasVec` trait for working with vector-based responses +//! +//! # Example +//! +//! ```rust +//! use libft_api::prelude::*; +//! use libft_api::api::prelude::*; // API-specific prelude +//! +//! async fn example() -> ClientResult<()> { +//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let client = FtClient::new(FtClientReqwestConnector::new()); +//! let session = client.open_session(token); +//! +//! // All API functionality is available through the session +//! let users = session.users(FtApiUsersRequest::new()).await?; +//! let projects = session.projects(FtApiProjectRequest::new()).await?; +//! +//! println!("Retrieved {} users and {} projects", users.users.len(), projects.projects.len()); +//! +//! Ok(()) +//! } +//! ``` + pub use super::campus::*; pub use super::cursus::*; pub use super::exam::*; diff --git a/libft-api/src/api/project.rs b/libft-api/src/api/project.rs index 71d8a09..87a514b 100644 --- a/libft-api/src/api/project.rs +++ b/libft-api/src/api/project.rs @@ -1,3 +1,37 @@ +//! API endpoints related to project information. +//! +//! This module provides access to the 42 Intra API endpoints that deal with project data. +//! It includes functionality for retrieving project information, project data, and project-team associations. +//! +//! # Endpoints +//! +//! * **projects**: Retrieve a list of projects with filtering, pagination, and sorting options +//! * **projects_id_teams**: Get teams associated with a specific project +//! * **project_data**: Additional project-related data access +//! +//! # Example +//! +//! ```rust +//! use libft_api::prelude::*; +//! +//! async fn example() -> ClientResult<()> { +//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let client = FtClient::new(FtClientReqwestConnector::new()); +//! let session = client.open_session(token); +//! +//! // Get all projects +//! let response = session.projects(FtApiProjectRequest::new()).await?; +//! println!("Found {} projects", response.projects.len()); +//! +//! // Get projects for a specific cursus +//! let cursus_projects = session +//! .projects(FtApiProjectRequest::new().with_cursus_id(FtCursusId::new(21))) +//! .await?; +//! +//! Ok(()) +//! } +//! ``` + pub use project_data::*; mod project_data; pub use projects::*; diff --git a/libft-api/src/api/project_session.rs b/libft-api/src/api/project_session.rs index c0b6d75..6f5e198 100644 --- a/libft-api/src/api/project_session.rs +++ b/libft-api/src/api/project_session.rs @@ -1,3 +1,36 @@ +//! API endpoints related to project session information. +//! +//! This module provides access to the 42 Intra API endpoints that deal with project session data. +//! It includes functionality for retrieving teams and scale teams associated with specific project sessions. +//! +//! # Endpoints +//! +//! * **project_sessions_id_teams**: Retrieve teams associated with a specific project session +//! * **project_sessions_id_scale_teams**: Retrieve scale teams (evaluation teams) associated with a specific project session +//! +//! # Example +//! +//! ```rust +//! use libft_api::prelude::*; +//! +//! async fn example() -> ClientResult<()> { +//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let client = FtClient::new(FtClientReqwestConnector::new()); +//! let session = client.open_session(token); +//! +//! // Get teams for a specific project session +//! let project_session_id = FtProjectSessionId::new(12345); // Replace with actual project session ID +//! let response = session +//! .project_sessions_id_teams( +//! FtApiProjectSessionsTeamsRequest::new(project_session_id) +//! ) +//! .await?; +//! println!("Found {} teams for the project session", response.teams.len()); +//! +//! Ok(()) +//! } +//! ``` + mod project_sessions_id_scale_teams; pub use project_sessions_id_scale_teams::*; mod project_sessions_id_teams; diff --git a/libft-api/src/api/project_user.rs b/libft-api/src/api/project_user.rs index cc372e7..b6f4938 100644 --- a/libft-api/src/api/project_user.rs +++ b/libft-api/src/api/project_user.rs @@ -1,2 +1,47 @@ +//! API endpoints related to project-user associations. +//! +//! This module provides access to the 42 Intra API endpoints that deal with project-user relationships. +//! It includes functionality for retrieving and creating associations between users and projects. +//! +//! # Endpoints +//! +//! * **projects_users**: Retrieve project-user associations with filtering, pagination, and sorting options +//! * **projects_users_post**: Create a new association between a user and a project +//! +//! # Example +//! +//! ```rust +//! use libft_api::prelude::*; +//! +//! async fn example() -> ClientResult<()> { +//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let client = FtClient::new(FtClientReqwestConnector::new()); +//! let session = client.open_session(token); +//! +//! // Get project-user associations for a specific user +//! let response = session +//! .projects_uesrs( +//! FtApiProjectsUsersRequest::new() +//! .with_filter(vec![ +//! FtFilterOption::new(FtFilterField::UserId, vec!["12345".to_owned()]) +//! ]) +//! ) +//! .await?; +//! println!("Found {} project-user associations", response.projects_users.len()); +//! +//! // Create a new project-user association (if you have the appropriate permissions) +//! // let new_assoc = session +//! // .projects_uesrs_post(FtApiProjectsUsersPostRequest::new( +//! // FtApiProjectsUsersPostBody { +//! // project_id: FtProjectId::new(123), +//! // user_id: FtUserId::new(12345), +//! // }, +//! // )) +//! // .await?; +//! +//! Ok(()) +//! } +//! ``` + mod projects_users; pub use projects_users::*; diff --git a/libft-api/src/api/scale_team.rs b/libft-api/src/api/scale_team.rs index 37cadfb..ed1eb10 100644 --- a/libft-api/src/api/scale_team.rs +++ b/libft-api/src/api/scale_team.rs @@ -1,2 +1,48 @@ +//! API endpoints related to scale team information. +//! +//! This module provides access to the 42 Intra API endpoints that deal with scale team data. +//! Scale teams are evaluation teams used for peer reviews and project assessments. +//! It includes functionality for retrieving scale teams and creating multiple scale teams at once. +//! +//! # Endpoints +//! +//! * **scale_teams**: Retrieve a list of scale teams with filtering, pagination, and sorting options +//! * **scale_teams_multiple_create_post**: Create multiple scale teams at once +//! +//! # Example +//! +//! ```rust +//! use libft_api::prelude::*; +//! +//! async fn example() -> ClientResult<()> { +//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let client = FtClient::new(FtClientReqwestConnector::new()); +//! let session = client.open_session(token); +//! +//! // Get scale teams with filtering +//! let response = session +//! .scale_teams( +//! FtApiScaleTeamsRequest::new() +//! .with_filter(vec![ +//! FtFilterOption::new(FtFilterField::CampusId, vec!["1".to_owned()]) // Paris campus +//! ]) +//! ) +//! .await?; +//! println!("Found {} scale teams", response.scale_teams.len()); +//! +//! // Create multiple scale teams at once (if you have the appropriate permissions) +//! // let create_request = FtApiScaleTeamsMultipleCreateRequest::new(vec![ +//! // FtApiScaleTeamsMultipleCreateBody { +//! // begin_at: FtDateTimeUtc::now(), +//! // user_id: FtUserId::new(12345), +//! // team_id: FtTeamId::new(67890), +//! // } +//! // ]); +//! // let created_teams = session.scale_teams_multiple_create_post(create_request).await?; +//! +//! Ok(()) +//! } +//! ``` + mod scale_teams; pub use scale_teams::*; diff --git a/libft-api/src/api/user.rs b/libft-api/src/api/user.rs index f0f962f..8aede3a 100644 --- a/libft-api/src/api/user.rs +++ b/libft-api/src/api/user.rs @@ -1,3 +1,50 @@ +//! API endpoints related to user information. +//! +//! This module provides access to the 42 Intra API endpoints that deal with user data. +//! It includes functionality for retrieving user profiles, user locations, projects, cursus information, +//! correction points, and more. +//! +//! # Endpoints +//! +//! * **users**: Retrieve a list of users with filtering, pagination, and sorting options +//! * **users_post**: Create a new user (if you have the appropriate permissions) +//! * **users_id**: Get information about a specific user by their ID or login +//! * **users_id_locations**: Get location information for a specific user +//! * **users_id_locations_stats**: Get location statistics for a specific user +//! * **users_id_teams**: Get teams associated with a specific user +//! * **users_id_cursus_users**: Get cursus information for a specific user +//! * **users_id_projects_users**: Get project associations for a specific user +//! * **users_id_correction_point_historics**: Get correction point history for a specific user +//! * **users_id_correction_points_add**: Add correction points to a specific user +//! +//! # Example +//! +//! ```rust +//! use libft_api::prelude::*; +//! +//! async fn example() -> ClientResult<()> { +//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let client = FtClient::new(FtClientReqwestConnector::new()); +//! let session = client.open_session(token); +//! +//! // Get all users (with appropriate permissions) +//! let users_response = session.users(FtApiUsersRequest::new()).await?; +//! println!("Found {} users", users_response.users.len()); +//! +//! // Get a specific user by ID +//! let user_response = session.users_id(FtUsersIdRequest::new(FtUserIdentifier::UserId(FtUserId::new(12345)))).await?; +//! if let Some(login) = &user_response.login { +//! println!("User login: {}", login.value()); +//! } +//! +//! // Get a user's location data +//! let location_response = session.users_id_locations(FtUsersIdLocationsRequest::new(FtUserIdentifier::UserId(FtUserId::new(12345)))).await?; +//! println!("Found {} location records", location_response.get_vec().len()); +//! +//! Ok(()) +//! } +//! ``` + mod users; pub use users::*; mod users_id; diff --git a/libft-api/src/api/user/users.rs b/libft-api/src/api/user/users.rs index 7c57d51..d6d23c7 100644 --- a/libft-api/src/api/user/users.rs +++ b/libft-api/src/api/user/users.rs @@ -48,6 +48,46 @@ impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { + /// Creates a new user in the 42 Intra API. + /// + /// This method creates a new user account with the provided details. + /// + /// # Parameters + /// - `req`: A `FtApiUsersPostRequest` struct containing the user creation data. + /// + /// # Returns + /// - `ClientResult`: Contains the created `FtUser` object + /// + /// # Example + /// ```rust + /// use libft_api::prelude::*; + /// use crate::models::user::FtKind; + /// + /// async fn example() -> ClientResult<()> { + /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let client = FtClient::new(FtClientReqwestConnector::new()); + /// let session = client.open_session(token); + /// + /// // Create a new user (requires appropriate permissions) + /// // let new_user_request = FtApiUsersPostRequest::new( + /// // FtApiUserPostBody { + /// // email: "newuser@example.com".to_string(), + /// // campus_id: FtCampusId::new(1), + /// // first_name: "First".to_string(), + /// // last_name: "Last".to_string(), + /// // login: "newuser".to_string(), + /// // password: "securepassword".to_string(), + /// // pool_month: "february".to_string(), + /// // pool_year: 2024, + /// // kind: FtKind::Student, + /// // } + /// // ); + /// // let new_user_response = session.users_post(new_user_request).await?; + /// // println!("Created user with ID: {:?}", new_user_response.user.id); + /// + /// Ok(()) + /// } + /// ``` pub async fn users_post( &self, req: FtApiUsersPostRequest, @@ -57,6 +97,57 @@ where self.http_session_api.http_post(url, &req).await } + /// Retrieves a list of users from the 42 Intra API. + /// + /// This method fetches user information with various filtering and pagination options. + /// + /// # Parameters + /// - `req`: A `FtApiUsersRequest` struct containing the query parameters. + /// + /// # Query Parameters + /// - `sort`: Optional vector of sort options to order the results + /// - `range`: Optional vector of range options to filter results by date ranges + /// - `filter`: Optional vector of filter options to filter the results + /// - `page`: Optional page number for pagination + /// - `per_page`: Optional number of items per page for pagination + /// + /// # Returns + /// - `ClientResult`: Contains a vector of `FtUser` objects + /// + /// # Example + /// ```rust + /// use libft_api::prelude::*; + /// + /// async fn example() -> ClientResult<()> { + /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let client = FtClient::new(FtClientReqwestConnector::new()); + /// let session = client.open_session(token); + /// + /// // Get all users with pagination + /// let users_response = session + /// .users( + /// FtApiUsersRequest::new() + /// .with_per_page(50) + /// ) + /// .await?; + /// println!("Found {} users", users_response.users.len()); + /// + /// // Get users filtered by specific criteria + /// let filtered_users = session + /// .users( + /// FtApiUsersRequest::new() + /// .with_filter(vec![ + /// FtFilterOption::new( + /// FtFilterField::CampusId, + /// vec!["1".to_string()] // Paris campus + /// ) + /// ]) + /// ) + /// .await?; + /// + /// Ok(()) + /// } + /// ``` pub async fn users(&self, req: FtApiUsersRequest) -> ClientResult { let url = "users"; let filters = convert_filter_option_to_tuple(req.filter.unwrap_or_default()).unwrap(); diff --git a/libft-api/src/api/user/users_id.rs b/libft-api/src/api/user/users_id.rs index cdb4e16..e6dec9b 100644 --- a/libft-api/src/api/user/users_id.rs +++ b/libft-api/src/api/user/users_id.rs @@ -23,6 +23,55 @@ impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { + /// Retrieves information about a specific user from the 42 Intra API. + /// + /// This method fetches detailed information about a user identified by either their user ID + /// or login name. The method supports various query parameters for filtering, sorting, and pagination. + /// + /// # Parameters + /// - `req`: A `FtApiUsersIdRequest` struct containing the query parameters, including the user identifier. + /// + /// # Query Parameters + /// - `id`: The identifier for the user (either user ID or login name) + /// - `sort`: Optional vector of sort options to order the results + /// - `range`: Optional vector of range options to filter results by date ranges + /// - `filter`: Optional vector of filter options to filter the results + /// - `page`: Optional page number for pagination + /// - `per_page`: Optional number of items per page for pagination + /// + /// # Returns + /// - `ClientResult`: Contains a `FtUser` object with detailed user information + /// + /// # Example + /// ```rust + /// use libft_api::prelude::*; + /// + /// async fn example() -> ClientResult<()> { + /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let client = FtClient::new(FtClientReqwestConnector::new()); + /// let session = client.open_session(token); + /// + /// // Get user by ID + /// let user_by_id = session + /// .users_id( + /// FtApiUsersIdRequest::new(FtUserIdentifier::UserId(FtUserId::new(12345))) + /// ) + /// .await?; + /// println!("User name: {:?} {:?}", user_by_id.user.first_name, user_by_id.user.last_name); + /// + /// // Get user by login + /// let user_by_login = session + /// .users_id( + /// FtApiUsersIdRequest::new(FtUserIdentifier::Login( + /// FtLoginId::new("user_login".to_string()) + /// )) + /// ) + /// .await?; + /// println!("User login: {:?}", user_by_login.user.login); + /// + /// Ok(()) + /// } + /// ``` pub async fn users_id(&self, req: FtApiUsersIdRequest) -> ClientResult { let url = &format!( "users/{}", diff --git a/libft-api/src/models.rs b/libft-api/src/models.rs index 39f74b0..f320550 100644 --- a/libft-api/src/models.rs +++ b/libft-api/src/models.rs @@ -1,3 +1,32 @@ +//! Data structures for 42 Intra API entities. +//! +//! This module contains all the data structures that represent entities from the 42 Intra API. +//! Each submodule corresponds to a specific type of entity, such as users, projects, campuses, +//! cursus, and more. These structures are used for serialization and deserialization of API +//! requests and responses. +//! +//! The models follow a consistent naming convention where each entity has: +//! * A main struct (e.g., `FtUser`, `FtProject`) that represents the entity +//! * Value structs (e.g., `FtUserId`, `FtLoginId`) for strongly-typed identifiers +//! * Enum types (e.g., `FtKind`, `FtPoolMonth`) for categorical values +//! +//! Most models implement serialization traits to support JSON conversion for API interactions. +//! The data structures are designed to closely match the structure of the 42 Intra API responses +//! while providing type safety and ergonomic access to the data. +//! +//! # Example +//! +//! ```rust +//! use libft_api::models::user::{FtUser, FtUserId}; +//! +//! // Example of how a user model might be used +//! fn process_user(user: FtUser) { +//! if let Some(user_id) = user.id { +//! println!("Processing user with ID: {}", user_id.value()); +//! } +//! } +//! ``` + pub mod achievement; pub mod campus; pub mod campus_user; diff --git a/libft-api/src/prelude.rs b/libft-api/src/prelude.rs index 23a9bd2..4a8ca48 100644 --- a/libft-api/src/prelude.rs +++ b/libft-api/src/prelude.rs @@ -1,3 +1,37 @@ +//! The prelude module for the `libft-api` crate. +//! +//! This module provides convenient glob imports for the most commonly used items in the `libft-api` crate. +//! By importing everything in this module, users can access all the essential functionality without +//! needing to import individual modules. +//! +//! The prelude includes: +//! * API endpoint clients and requests from the `api` module +//! * Authentication types and functions from the `auth` module +//! * Common types like error types, client, parameters, rate limiter, and paginator from the `common` module +//! * The HTTP connector implementation from the `connector` module +//! * Constants and information about 42 campuses and cursus from the `info` module +//! * All model types from the `models` module +//! +//! # Example +//! +//! ```rust +//! use libft_api::prelude::*; +//! +//! async fn example() -> ClientResult<()> { +//! // All necessary types are available through the prelude +//! let auth_info = AuthInfo::build_from_env()?; +//! let token = FtApiToken::try_get(auth_info).await?; +//! let client = FtClient::new(FtClientReqwestConnector::new()); +//! let session = client.open_session(token); +//! +//! // Now you can make API calls using the session +//! let user = session.users_id(FtUsersIdRequest::new(12345)).await?; +//! println!("User login: {}", user.login.unwrap_or_default()); +//! +//! Ok(()) +//! } +//! ``` + pub use crate::api::prelude::*; pub use crate::auth::*; pub use crate::common::*; From cb4a56859473dbc86c68138784fb471264ba798e Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Tue, 14 Oct 2025 18:33:18 +0900 Subject: [PATCH 16/18] refactor(api)!: rename campus_id module and add docs This commit introduces two main changes: 1. The `info::campus_id` module is renamed to `info::ft_campus_id` to improve naming consistency across the API. 2. Comprehensive documentation and usage examples have been added to several API endpoints, including `cursus_id_projects`, `groups`, and `projects`, to enhance developer experience. BREAKING CHANGE: The `info::campus_id` module has been renamed to `info::ft_campus_id`. Usages of campus ID constants must be updated to the new path. --- libft-api/examples/scroll.rs | 2 +- .../src/api/campus/campus_id_journals.rs | 2 +- .../src/api/campus/campus_id_locations.rs | 2 +- libft-api/src/api/campus/campus_id_users.rs | 2 +- .../src/api/cursus/cursus_id_projects.rs | 37 +++++++++++ libft-api/src/api/group/groups.rs | 63 +++++++++++++++++++ libft-api/src/api/project/projects.rs | 38 +++++++++++ libft-api/src/api/scale_team/scale_teams.rs | 3 +- libft-api/src/info.rs | 2 +- 9 files changed, 144 insertions(+), 7 deletions(-) diff --git a/libft-api/examples/scroll.rs b/libft-api/examples/scroll.rs index 7c2710e..125a8c8 100644 --- a/libft-api/examples/scroll.rs +++ b/libft-api/examples/scroll.rs @@ -1,7 +1,7 @@ use std::{io::Write, sync::Arc}; use futures::FutureExt; -use libft_api::{info::campus_id::SEOUL, prelude::*}; +use libft_api::{info::ft_campus_id::SEOUL, prelude::*}; use tokio::task::JoinSet; use tracing::info_span; diff --git a/libft-api/src/api/campus/campus_id_journals.rs b/libft-api/src/api/campus/campus_id_journals.rs index 3416b15..f0561a9 100644 --- a/libft-api/src/api/campus/campus_id_journals.rs +++ b/libft-api/src/api/campus/campus_id_journals.rs @@ -93,7 +93,7 @@ where #[cfg(test)] mod tests { - use crate::info::campus_id::GYEONGSAN; + use crate::info::ft_campus_id::GYEONGSAN; use super::*; diff --git a/libft-api/src/api/campus/campus_id_locations.rs b/libft-api/src/api/campus/campus_id_locations.rs index 02776e4..2aebc18 100644 --- a/libft-api/src/api/campus/campus_id_locations.rs +++ b/libft-api/src/api/campus/campus_id_locations.rs @@ -115,7 +115,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::info::campus_id::GYEONGSAN; + use crate::info::ft_campus_id::GYEONGSAN; #[tokio::test] async fn location_with_params() { diff --git a/libft-api/src/api/campus/campus_id_users.rs b/libft-api/src/api/campus/campus_id_users.rs index 1f2308e..3fe5092 100644 --- a/libft-api/src/api/campus/campus_id_users.rs +++ b/libft-api/src/api/campus/campus_id_users.rs @@ -111,7 +111,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::info::campus_id::GYEONGSAN; + use crate::info::ft_campus_id::GYEONGSAN; #[tokio::test] async fn basic() { diff --git a/libft-api/src/api/cursus/cursus_id_projects.rs b/libft-api/src/api/cursus/cursus_id_projects.rs index 41ed26b..0f78de0 100644 --- a/libft-api/src/api/cursus/cursus_id_projects.rs +++ b/libft-api/src/api/cursus/cursus_id_projects.rs @@ -26,6 +26,43 @@ impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { + /// Retrieves projects associated with a specific cursus from the 42 Intra API. + /// + /// # Parameters + /// - `req`: A `FtApiCursusIdProjectsRequest` struct containing the query parameters. + /// + /// # Query Parameters + /// - `cursus_id`: The ID of the cursus to retrieve projects for (required) + /// - `project_id`: Optional project ID to filter results + /// - `sort`: Optional vector of sort options + /// - `range`: Optional vector of range options + /// - `filter`: Optional vector of filter options + /// - `page`: Optional page number for pagination + /// - `per_page`: Optional number of items per page for pagination + /// + /// # Returns + /// - `ClientResult`: Contains a vector of `FtProject` objects + /// + /// # Example + /// ```rust + /// use libft_api::prelude::*; + /// + /// async fn example() -> ClientResult<()> { + /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let client = FtClient::new(FtClientReqwestConnector::new()); + /// let session = client.open_session(token); + /// + /// // Get projects for the common core cursus + /// let projects = session + /// .cursus_id_projects( + /// FtApiCursusIdProjectsRequest::new(FtCursusId::new(FT_CURSUS_ID)) + /// ) + /// .await?; + /// println!("Found {} projects", projects.projects.len()); + /// + /// Ok(()) + /// } + /// ``` pub async fn cursus_id_projects( &self, req: FtApiCursusIdProjectsRequest, diff --git a/libft-api/src/api/group/groups.rs b/libft-api/src/api/group/groups.rs index ccae62f..7f0d1d4 100644 --- a/libft-api/src/api/group/groups.rs +++ b/libft-api/src/api/group/groups.rs @@ -40,6 +40,40 @@ impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { + /// Retrieves a list of groups from the 42 Intra API. + /// + /// # Parameters + /// - `req`: A `FtApiGroupsRequest` struct containing the query parameters. + /// + /// # Query Parameters + /// - `user_id`: Optional user ID to filter groups associated with a specific user + /// - `page`: Optional page number for pagination + /// - `per_page`: Optional number of items per page for pagination + /// + /// # Returns + /// - `ClientResult`: Contains a vector of `FtGroup` objects + /// + /// # Example + /// ```rust + /// use libft_api::prelude::*; + /// + /// async fn example() -> ClientResult<()> { + /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let client = FtClient::new(FtClientReqwestConnector::new()); + /// let session = client.open_session(token); + /// + /// // Get all groups with pagination + /// let groups = session + /// .groups( + /// FtApiGroupsRequest::new() + /// .with_per_page(50) + /// ) + /// .await?; + /// println!("Found {} groups", groups.groups.len()); + /// + /// Ok(()) + /// } + /// ``` pub async fn groups(&self, req: FtApiGroupsRequest) -> ClientResult { let url = "groups"; @@ -48,6 +82,35 @@ where self.http_session_api.http_get(url, ¶ms).await } + /// Creates a group-user association in the 42 Intra API. + /// + /// # Parameters + /// - `req`: A `FtApiGroupsUsersPostRequest` struct containing the group-user association data. + /// + /// # Returns + /// - `ClientResult`: Contains the created association details + /// + /// # Example + /// ```rust + /// use libft_api::prelude::*; + /// + /// async fn example() -> ClientResult<()> { + /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let client = FtClient::new(FtClientReqwestConnector::new()); + /// let session = client.open_session(token); + /// + /// // Create a group-user association (requires appropriate permissions) + /// // let association_request = FtApiGroupsUsersPostRequest::new( + /// // FtApiGroupsUsersPostBody { + /// // group_id: FtGroupId::new(123), + /// // user_id: FtUserId::new(456), + /// // } + /// // ); + /// // let result = session.groups_users_post(association_request).await?; + /// + /// Ok(()) + /// } + /// ``` pub async fn groups_users_post( &self, req: FtApiGroupsUsersPostRequest, diff --git a/libft-api/src/api/project/projects.rs b/libft-api/src/api/project/projects.rs index eaf0e39..f130ef9 100644 --- a/libft-api/src/api/project/projects.rs +++ b/libft-api/src/api/project/projects.rs @@ -26,6 +26,44 @@ impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { + /// Retrieves a list of projects from the 42 Intra API. + /// + /// # Parameters + /// - `req`: A `FtApiProjectRequest` struct containing the query parameters. + /// + /// # Query Parameters + /// - `cursus_id`: Optional cursus ID to filter projects by cursus + /// - `project_id`: Optional project ID to filter results + /// - `sort`: Optional vector of sort options + /// - `range`: Optional vector of range options + /// - `filter`: Optional vector of filter options + /// - `page`: Optional page number for pagination + /// - `per_page`: Optional number of items per page for pagination + /// + /// # Returns + /// - `ClientResult`: Contains a vector of `FtProject` objects + /// + /// # Example + /// ```rust + /// use libft_api::prelude::*; + /// + /// async fn example() -> ClientResult<()> { + /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let client = FtClient::new(FtClientReqwestConnector::new()); + /// let session = client.open_session(token); + /// + /// // Get all projects with pagination + /// let projects = session + /// .projects( + /// FtApiProjectRequest::new() + /// .with_per_page(50) + /// ) + /// .await?; + /// println!("Found {} projects", projects.projects.len()); + /// + /// Ok(()) + /// } + /// ``` pub async fn projects(&self, req: FtApiProjectRequest) -> ClientResult { let url = "projects"; diff --git a/libft-api/src/api/scale_team/scale_teams.rs b/libft-api/src/api/scale_team/scale_teams.rs index ac63793..3ac6b1a 100644 --- a/libft-api/src/api/scale_team/scale_teams.rs +++ b/libft-api/src/api/scale_team/scale_teams.rs @@ -88,10 +88,9 @@ where #[cfg(test)] mod tests { - use campus_id::GYEONGSAN; + use crate::info::ft_campus_id::GYEONGSAN; use super::*; - #[tokio::test] async fn basic() { diff --git a/libft-api/src/info.rs b/libft-api/src/info.rs index 454cfb2..4de176c 100644 --- a/libft-api/src/info.rs +++ b/libft-api/src/info.rs @@ -5,7 +5,7 @@ pub const TEST_USER_YONDOO_ID: i32 = 180_844; pub const FT_GROUP_ID_TEST_ACCOUNT: i32 = 119; pub const FT_GROUP_ID_STAFF: i32 = 1; -pub mod campus_id { +pub mod ft_campus_id { pub const RABAT: i32 = 75; pub const ISKANDARPUTERI: i32 = 73; pub const MILANO: i32 = 72; From 59f61b56f007852cc965f74951496739d3a49791 Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Tue, 14 Oct 2025 18:42:16 +0900 Subject: [PATCH 17/18] feat(api): make connector public and add client docs This commit enhances the public API of the `libft-api` crate. The `connector` module is now public, enabling users to build their own HTTP client connectors or extend the existing functionality. Additionally, comprehensive documentation has been added to the core client components, including `FtClient`, `FtClientSession`, and related type aliases. These doc comments include usage examples to improve the developer experience. --- libft-api/src/common/client.rs | 70 ++++++++++++++++++++++++++++++++++ libft-api/src/lib.rs | 3 +- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/libft-api/src/common/client.rs b/libft-api/src/common/client.rs index 5a2fa44..12800a5 100644 --- a/libft-api/src/common/client.rs +++ b/libft-api/src/common/client.rs @@ -8,10 +8,40 @@ use crate::auth::FtApiToken; use crate::common::*; use crate::connector::*; +/// Type alias for client operation results. +/// +/// This is a convenience type alias that represents the result of API operations, +/// returning either a success value of type T or an error of type FtClientError. pub type ClientResult = std::result::Result; +/// Type alias for the default reqwest-based client implementation. +/// +/// This is a convenience type alias that represents an FtClient configured with the +/// FtClientReqwestConnector, which uses the reqwest HTTP client library. pub type FtReqwestClient = FtClient; +/// The main client for interacting with the 42 Intra API. +/// +/// The FtClient is the primary entry point for making API requests to the 42 Intra API. +/// It manages the HTTP connector, rate limiting, and provides methods to open sessions +/// for making authenticated API calls. +/// +/// # Example +/// ```rust +/// use libft_api::prelude::*; +/// +/// async fn example() -> ClientResult<()> { +/// let client = FtClient::new(FtClientReqwestConnector::new()); +/// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +/// let session = client.open_session(token); +/// +/// // Use the session to make API calls +/// let users = session.users(FtApiUsersRequest::new()).await?; +/// println!("Found {} users", users.users.len()); +/// +/// Ok(()) +/// } +/// ``` #[derive(Clone, Debug)] pub struct FtClient where @@ -22,6 +52,10 @@ where } /// The HTTP API client. +/// +/// This structure wraps the HTTP connector and provides the core functionality +/// for making HTTP requests to the 42 Intra API. It is contained within the FtClient +/// and is responsible for managing the underlying HTTP connection. #[derive(Clone, Debug)] pub struct FtClientHttpApi where @@ -31,8 +65,39 @@ where pub connector: Arc, } +/// URI utilities for the 42 API. +/// +/// This structure provides static methods for constructing URLs and handling +/// API endpoints, ensuring consistent URL formatting for all API requests. pub struct FtClientHttpApiUri; +/// A session for making authenticated API requests. +/// +/// An FtClientSession represents an authenticated session with a valid API token. +/// It provides methods for making API calls that require authentication. +/// +/// The session is created by calling `FtClient::open_session` and holds a reference +/// to the parent client and the authentication token. +/// +/// # Example +/// ```rust +/// use libft_api::prelude::*; +/// +/// async fn example() -> ClientResult<()> { +/// let client = FtClient::new(FtClientReqwestConnector::new()); +/// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +/// let session = client.open_session(token); +/// +/// // Use the session to make authenticated API calls +/// let user = session.users_id(FtUsersIdRequest::new(FtUserIdentifier::Login( +/// FtLoginId::new("user_login".to_string()) +/// ))).await?; +/// +/// println!("User login: {:?}", user.login); +/// +/// Ok(()) +/// } +/// ``` #[derive(Debug)] pub struct FtClientSession<'a, FCHC> where @@ -41,6 +106,11 @@ where pub http_session_api: FtClientHttpSessionApi<'a, FCHC>, } +/// The HTTP session API for authenticated requests. +/// +/// This structure provides the underlying HTTP functionality for authenticated +/// API requests. It holds the authentication token and a reference to the parent +/// client, allowing for authenticated API calls. #[derive(Debug)] pub struct FtClientHttpSessionApi<'a, FCHC> where diff --git a/libft-api/src/lib.rs b/libft-api/src/lib.rs index 8edda2f..6e0c48f 100644 --- a/libft-api/src/lib.rs +++ b/libft-api/src/lib.rs @@ -57,7 +57,8 @@ pub mod models; pub mod auth; mod common; + pub mod info; pub mod prelude; -mod connector; +pub mod connector; From 7cde6bd94af79607e4ca88433dcefad82d9dcf23 Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Tue, 14 Oct 2025 18:56:51 +0900 Subject: [PATCH 18/18] test --- API_Structure_Relationships.md | 139 +++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 API_Structure_Relationships.md diff --git a/API_Structure_Relationships.md b/API_Structure_Relationships.md new file mode 100644 index 0000000..22755d6 --- /dev/null +++ b/API_Structure_Relationships.md @@ -0,0 +1,139 @@ +# API Structure Relationships + +## Mermaid Diagram + +```mermaid +classDiagram + class FtClientHttpConnector { + <> + +http_get_uri(full_uri, token, ratelimiter)$ + +http_post_uri(full_uri, token, request_body)$ + +http_patch_uri(full_uri, token, request_body)$ + +http_delete_uri(full_uri, token, request_body)$ + +create_method_uri_path(method_relative_uri)$ + } + + class FtClientReqwestConnector { + -reqwest_connector: Client + -ft_api_url: String + +new()$ + +with_connector(connector)$ + +with_ft_api_url(ft_api_url)$ + -send_http_request(reqwest, url, meta)$ + } + + class FtClientHttpApiUri { + +FT_API_URI_STR: String + +create_method_uri_path(method_relative_uri)$ + +create_url_with_params(base_url, params)$ + } + + class FtClient~FCHC~ { + +http_api: FtClientHttpApi~FCHC~ + +meta: HeaderMetaData + +new(http_connector)$ + +with_ratelimits(http_connector, secondly, hourly)$ + +open_session(token)$ + } + + class FtClientHttpApi~FCHC~ { + +connector: Arc~FCHC~ + +new(http_connector)$ + } + + class FtClientSession~'a, FCHC~ { + +http_session_api: FtClientHttpSessionApi~'a, FCHC~ + } + + class FtClientHttpSessionApi~'a, FCHC~ { + -token: FtApiToken + +client: &FtClient~FCHC~ + +http_get_uri(full_uri)$ + +http_get(method_relative_uri, params)$ + +http_post(method_relative_uri, request)$ + +http_post_uri(full_uri, request)$ + +http_delete(method_relative_uri, request)$ + +http_delete_uri(full_uri, request)$ + } + + class FtApiToken { + -access_token: String + -token_type: AccessTokenType + -expires_in: i64 + -scope: String + -created_at: i64 + -secret_valid_until: i64 + +get_token_value()$ + } + + class HeaderMetaData { + -ratelimiter: RateLimiter + +new(ratelimiter)$ + } + + class ClientResult~T~ { + <> + } + + %% Relationships + FtClientHttpConnector <|-- FtClientReqwestConnector : implements + FtClientReqwestConnector --> FtClientHttpApiUri : uses + FtClientReqwestConnector --> FtApiToken : uses + FtClientReqwestConnector --> HeaderMetaData : uses + + FtClientHttpApi <--> FtClientHttpConnector : generic bound + FtClientHttpApi --> FtClientReqwestConnector : stores in Arc + + FtClient --> FtClientHttpApi : contains + FtClient --> HeaderMetaData : contains + FtClient --> FtClientReqwestConnector : generic bound + + FtClientSession --> FtClientHttpSessionApi : contains + FtClientSession --> FtClient : contains reference to + FtClientSession --> FtApiToken : through http_session_api + + FtClientHttpSessionApi --> FtClient : contains reference + FtClientHttpSessionApi --> FtApiToken : contains + FtClientHttpSessionApi --> FtClientHttpApi : uses connector + FtClientHttpSessionApi --> FtClientHttpConnector : through connector + + FtClientReqwestConnector ..> ClientResult : returns + FtClientSession ..> ClientResult : returns + FtClientHttpSessionApi ..> ClientResult : returns + + %% Inheritance relationship + FtClientReqwestConnector --|> FtClientHttpConnector + FtClientHttpApi --|> FtClientHttpConnector + FtClientHttpSessionApi --|> FtClientHttpConnector +``` + +## Explanation of Relationships + +### Core Components + +1. **FtClientHttpConnector (Trait)**: Defines the interface for HTTP connectors that can communicate with the 42 API. It has methods for GET, POST, PATCH, DELETE requests and URI creation. + +2. **FtClientReqwestConnector (Implementation)**: A concrete implementation of FtClientHttpConnector using the reqwest HTTP client library. It handles the actual network communication with the 42 Intra API. + +3. **FtClient**: The main client structure that serves as the entry point for API interactions. It contains: + - `http_api`: An instance of FtClientHttpApi that manages the connector + - `meta`: HeaderMetaData containing rate limiting information + +4. **FtClientSession<'a, FCHC>**: An authenticated session that holds a valid token and allows making authenticated API calls. + +### Data Flow + +1. User creates an `FtClient` with a connector (typically `FtClientReqwestConnector`) +2. User opens a session with a valid `FtApiToken` using `client.open_session(token)` +3. User makes API calls through the session (e.g., `session.users()`) +4. The session uses its internal `FtClientHttpSessionApi` to make requests +5. The session API calls the underlying connector to perform HTTP operations +6. The connector handles authentication, rate limiting, and communication with the API + +### Thread Safety and Concurrency + +- `FtClientHttpApi` stores the connector in an `Arc` to allow sharing across threads +- All connector implementations must implement `Send` and `Sync` for thread safety +- The generic `FCHC` parameter allows for different connector implementations while maintaining type safety + +This architecture allows for flexible connector implementations while providing a consistent API interface for users of the library.