From 66320811696058c5409803f3f67486ead618f683 Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Wed, 15 Oct 2025 14:10:53 +0900 Subject: [PATCH 1/5] Fix doctest --- libft-api/src/api/campus.rs | 2 +- libft-api/src/api/campus/campus_id.rs | 2 +- libft-api/src/api/campus/campus_id_locations.rs | 2 +- libft-api/src/api/campus/campus_id_users.rs | 2 +- libft-api/src/api/campus/campus_users.rs | 2 +- libft-api/src/api/cursus.rs | 2 +- libft-api/src/api/cursus/cursus_id_projects.rs | 2 +- libft-api/src/api/exam.rs | 2 +- libft-api/src/api/exam/exams.rs | 4 ++-- libft-api/src/api/group.rs | 2 +- libft-api/src/api/group/groups.rs | 4 ++-- libft-api/src/api/prelude.rs | 2 +- libft-api/src/api/project.rs | 2 +- libft-api/src/api/project/projects.rs | 2 +- libft-api/src/api/project_session.rs | 2 +- libft-api/src/api/project_user.rs | 2 +- libft-api/src/api/scale_team.rs | 2 +- libft-api/src/api/user.rs | 2 +- libft-api/src/api/user/users.rs | 4 ++-- libft-api/src/api/user/users_id.rs | 2 +- 20 files changed, 23 insertions(+), 23 deletions(-) diff --git a/libft-api/src/api/campus.rs b/libft-api/src/api/campus.rs index f0dabd4..e63f76f 100644 --- a/libft-api/src/api/campus.rs +++ b/libft-api/src/api/campus.rs @@ -18,7 +18,7 @@ //! use libft_api::prelude::*; //! //! async fn example() -> ClientResult<()> { -//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); //! let client = FtClient::new(FtClientReqwestConnector::new()); //! let session = client.open_session(token); //! diff --git a/libft-api/src/api/campus/campus_id.rs b/libft-api/src/api/campus/campus_id.rs index d08b2e0..a7112ea 100644 --- a/libft-api/src/api/campus/campus_id.rs +++ b/libft-api/src/api/campus/campus_id.rs @@ -45,7 +45,7 @@ where /// use libft_api::prelude::*; /// /// async fn example() -> ClientResult<()> { - /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); /// let client = FtClient::new(FtClientReqwestConnector::new()); /// let session = client.open_session(token); /// diff --git a/libft-api/src/api/campus/campus_id_locations.rs b/libft-api/src/api/campus/campus_id_locations.rs index c62a893..49b9fac 100644 --- a/libft-api/src/api/campus/campus_id_locations.rs +++ b/libft-api/src/api/campus/campus_id_locations.rs @@ -51,7 +51,7 @@ where /// use libft_api::prelude::*; /// /// async fn example() -> ClientResult<()> { - /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); /// let client = FtClient::new(FtClientReqwestConnector::new()); /// let session = client.open_session(token); /// diff --git a/libft-api/src/api/campus/campus_id_users.rs b/libft-api/src/api/campus/campus_id_users.rs index ee56ebc..5e09e77 100644 --- a/libft-api/src/api/campus/campus_id_users.rs +++ b/libft-api/src/api/campus/campus_id_users.rs @@ -49,7 +49,7 @@ where /// use libft_api::prelude::*; /// /// async fn example() -> ClientResult<()> { - /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); /// let client = FtClient::new(FtClientReqwestConnector::new()); /// let session = client.open_session(token); /// diff --git a/libft-api/src/api/campus/campus_users.rs b/libft-api/src/api/campus/campus_users.rs index 4a05a21..48d12ec 100644 --- a/libft-api/src/api/campus/campus_users.rs +++ b/libft-api/src/api/campus/campus_users.rs @@ -50,7 +50,7 @@ where /// use libft_api::prelude::*; /// /// async fn example() -> ClientResult<()> { - /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); /// let client = FtClient::new(FtClientReqwestConnector::new()); /// let session = client.open_session(token); /// diff --git a/libft-api/src/api/cursus.rs b/libft-api/src/api/cursus.rs index 5c8e1e3..977ff49 100644 --- a/libft-api/src/api/cursus.rs +++ b/libft-api/src/api/cursus.rs @@ -13,7 +13,7 @@ //! use libft_api::prelude::*; //! //! async fn example() -> ClientResult<()> { -//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); //! let client = FtClient::new(FtClientReqwestConnector::new()); //! let session = client.open_session(token); //! diff --git a/libft-api/src/api/cursus/cursus_id_projects.rs b/libft-api/src/api/cursus/cursus_id_projects.rs index 1d071c0..4c8dd5e 100644 --- a/libft-api/src/api/cursus/cursus_id_projects.rs +++ b/libft-api/src/api/cursus/cursus_id_projects.rs @@ -48,7 +48,7 @@ where /// use libft_api::prelude::*; /// /// async fn example() -> ClientResult<()> { - /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); /// let client = FtClient::new(FtClientReqwestConnector::new()); /// let session = client.open_session(token); /// diff --git a/libft-api/src/api/exam.rs b/libft-api/src/api/exam.rs index 9b9e922..588cc84 100644 --- a/libft-api/src/api/exam.rs +++ b/libft-api/src/api/exam.rs @@ -14,7 +14,7 @@ //! use libft_api::prelude::*; //! //! async fn example() -> ClientResult<()> { -//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); //! let client = FtClient::new(FtClientReqwestConnector::new()); //! let session = client.open_session(token); //! diff --git a/libft-api/src/api/exam/exams.rs b/libft-api/src/api/exam/exams.rs index 2d57fce..ef16178 100644 --- a/libft-api/src/api/exam/exams.rs +++ b/libft-api/src/api/exam/exams.rs @@ -62,7 +62,7 @@ where /// use libft_api::prelude::*; /// /// async fn example() -> ClientResult<()> { - /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); /// let client = FtClient::new(FtClientReqwestConnector::new()); /// let session = client.open_session(token); /// @@ -125,7 +125,7 @@ where /// use libft_api::prelude::*; /// /// async fn example() -> ClientResult<()> { - /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); /// let client = FtClient::new(FtClientReqwestConnector::new()); /// let session = client.open_session(token); /// diff --git a/libft-api/src/api/group.rs b/libft-api/src/api/group.rs index 0d34760..9848016 100644 --- a/libft-api/src/api/group.rs +++ b/libft-api/src/api/group.rs @@ -14,7 +14,7 @@ //! use libft_api::prelude::*; //! //! async fn example() -> ClientResult<()> { -//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); //! let client = FtClient::new(FtClientReqwestConnector::new()); //! let session = client.open_session(token); //! diff --git a/libft-api/src/api/group/groups.rs b/libft-api/src/api/group/groups.rs index b30454b..d1ba324 100644 --- a/libft-api/src/api/group/groups.rs +++ b/libft-api/src/api/group/groups.rs @@ -58,7 +58,7 @@ where /// use libft_api::prelude::*; /// /// async fn example() -> ClientResult<()> { - /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); /// let client = FtClient::new(FtClientReqwestConnector::new()); /// let session = client.open_session(token); /// @@ -95,7 +95,7 @@ where /// use libft_api::prelude::*; /// /// async fn example() -> ClientResult<()> { - /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); /// let client = FtClient::new(FtClientReqwestConnector::new()); /// let session = client.open_session(token); /// diff --git a/libft-api/src/api/prelude.rs b/libft-api/src/api/prelude.rs index cc5eeac..d799b92 100644 --- a/libft-api/src/api/prelude.rs +++ b/libft-api/src/api/prelude.rs @@ -17,7 +17,7 @@ //! use libft_api::api::prelude::*; // API-specific prelude //! //! async fn example() -> ClientResult<()> { -//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); //! let client = FtClient::new(FtClientReqwestConnector::new()); //! let session = client.open_session(token); //! diff --git a/libft-api/src/api/project.rs b/libft-api/src/api/project.rs index 87a514b..c30158f 100644 --- a/libft-api/src/api/project.rs +++ b/libft-api/src/api/project.rs @@ -15,7 +15,7 @@ //! use libft_api::prelude::*; //! //! async fn example() -> ClientResult<()> { -//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); //! let client = FtClient::new(FtClientReqwestConnector::new()); //! let session = client.open_session(token); //! diff --git a/libft-api/src/api/project/projects.rs b/libft-api/src/api/project/projects.rs index 31af96c..cf4bc3b 100644 --- a/libft-api/src/api/project/projects.rs +++ b/libft-api/src/api/project/projects.rs @@ -48,7 +48,7 @@ where /// use libft_api::prelude::*; /// /// async fn example() -> ClientResult<()> { - /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); /// let client = FtClient::new(FtClientReqwestConnector::new()); /// let session = client.open_session(token); /// diff --git a/libft-api/src/api/project_session.rs b/libft-api/src/api/project_session.rs index 6f5e198..4711bae 100644 --- a/libft-api/src/api/project_session.rs +++ b/libft-api/src/api/project_session.rs @@ -14,7 +14,7 @@ //! use libft_api::prelude::*; //! //! async fn example() -> ClientResult<()> { -//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); //! let client = FtClient::new(FtClientReqwestConnector::new()); //! let session = client.open_session(token); //! diff --git a/libft-api/src/api/project_user.rs b/libft-api/src/api/project_user.rs index b6f4938..ee23ced 100644 --- a/libft-api/src/api/project_user.rs +++ b/libft-api/src/api/project_user.rs @@ -14,7 +14,7 @@ //! use libft_api::prelude::*; //! //! async fn example() -> ClientResult<()> { -//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); //! let client = FtClient::new(FtClientReqwestConnector::new()); //! let session = client.open_session(token); //! diff --git a/libft-api/src/api/scale_team.rs b/libft-api/src/api/scale_team.rs index ed1eb10..b3fbe94 100644 --- a/libft-api/src/api/scale_team.rs +++ b/libft-api/src/api/scale_team.rs @@ -15,7 +15,7 @@ //! use libft_api::prelude::*; //! //! async fn example() -> ClientResult<()> { -//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); //! let client = FtClient::new(FtClientReqwestConnector::new()); //! let session = client.open_session(token); //! diff --git a/libft-api/src/api/user.rs b/libft-api/src/api/user.rs index 8aede3a..0ebbd5a 100644 --- a/libft-api/src/api/user.rs +++ b/libft-api/src/api/user.rs @@ -23,7 +23,7 @@ //! use libft_api::prelude::*; //! //! async fn example() -> ClientResult<()> { -//! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; +//! let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); //! let client = FtClient::new(FtClientReqwestConnector::new()); //! let session = client.open_session(token); //! diff --git a/libft-api/src/api/user/users.rs b/libft-api/src/api/user/users.rs index 99bf83b..5e96e44 100644 --- a/libft-api/src/api/user/users.rs +++ b/libft-api/src/api/user/users.rs @@ -64,7 +64,7 @@ where /// use crate::models::user::FtKind; /// /// async fn example() -> ClientResult<()> { - /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); /// let client = FtClient::new(FtClientReqwestConnector::new()); /// let session = client.open_session(token); /// @@ -119,7 +119,7 @@ where /// use libft_api::prelude::*; /// /// async fn example() -> ClientResult<()> { - /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); /// let client = FtClient::new(FtClientReqwestConnector::new()); /// let session = client.open_session(token); /// diff --git a/libft-api/src/api/user/users_id.rs b/libft-api/src/api/user/users_id.rs index ab62816..8d52a4d 100644 --- a/libft-api/src/api/user/users_id.rs +++ b/libft-api/src/api/user/users_id.rs @@ -47,7 +47,7 @@ where /// use libft_api::prelude::*; /// /// async fn example() -> ClientResult<()> { - /// let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; + /// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); /// let client = FtClient::new(FtClientReqwestConnector::new()); /// let session = client.open_session(token); /// From 8e6dc2baba39dc723b20412bf938bb7c5ef298c1 Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Mon, 20 Oct 2025 12:56:19 +0900 Subject: [PATCH 2/5] fix(doctest): correct and enable documentation tests This commit addresses numerous issues with the documentation tests (doctests) across the `libft-api` crate, ensuring they are correct, runnable, and pass CI. Key changes include: - Wrapping example code in `async fn` and using a `tokio` runtime to allow `await` syntax in doctests. - Updating API calls in examples to reflect recent changes in request/response structures and function signatures. - Replacing placeholder examples with runnable code snippets. - Improving error handling in tests and examples by using `ClientResult` and the `?` operator instead of `.unwrap()`. - Renaming `TokenError::TempTokenNotFound` to `NoTempToken` for clarity. - Changing the error type of `AuthInfo::build_from_env` to `std::env::VarError`. BREAKING CHANGE: The `AuthInfo::build_from_env` function now returns `Result` instead of `Result`. The `TokenError::TempTokenNotFound` enum variant has been renamed to `TokenError::NoTempToken`. --- libft-api/src/api.rs | 54 +++++++------------ libft-api/src/api/campus.rs | 24 --------- libft-api/src/api/campus/campus_id.rs | 36 +++---------- .../src/api/cursus/cursus_id_projects.rs | 43 ++++++++------- libft-api/src/api/group.rs | 54 +++++++++---------- libft-api/src/api/group/groups.rs | 20 ++++--- libft-api/src/api/user.rs | 37 ++++++------- libft-api/src/api/user/users.rs | 49 +++++++++-------- .../src/api/user/users_id_cursus_users.rs | 29 +++++----- libft-api/src/auth.rs | 10 ++-- libft-api/src/common/error.rs | 27 ++++++++++ 11 files changed, 180 insertions(+), 203 deletions(-) diff --git a/libft-api/src/api.rs b/libft-api/src/api.rs index 657b172..8415baf 100644 --- a/libft-api/src/api.rs +++ b/libft-api/src/api.rs @@ -15,21 +15,26 @@ //! //! # Example //! -//! ```rust -//! use libft_api::prelude::*; -//! -//! async fn example() -> ClientResult<()> { -//! let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).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(()) -//! } -//! ``` +//! # Example +//! ```rust +//! use libft_api::{prelude::*, info::ft_campus_id::GYEONGSAN}; +//! +//! # async fn run() -> 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(1), +//! ) +//! .await?; +//! for location in response.location { +//! println!("{:?} @ {:?}", location.user.login, location.host); +//! } +//! # Ok(()) +//! # } +//! # tokio::runtime::Runtime::new().unwrap().block_on(run()).unwrap(); +//! ``` pub mod campus; pub mod cursus; @@ -47,25 +52,6 @@ pub mod prelude; /// /// 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; diff --git a/libft-api/src/api/campus.rs b/libft-api/src/api/campus.rs index e63f76f..4bacf15 100644 --- a/libft-api/src/api/campus.rs +++ b/libft-api/src/api/campus.rs @@ -11,30 +11,6 @@ //! * **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().unwrap()).await.unwrap(); -//! 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::*; diff --git a/libft-api/src/api/campus/campus_id.rs b/libft-api/src/api/campus/campus_id.rs index a7112ea..4cfebd8 100644 --- a/libft-api/src/api/campus/campus_id.rs +++ b/libft-api/src/api/campus/campus_id.rs @@ -41,27 +41,8 @@ where /// - `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().unwrap()).await.unwrap(); - /// 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(()) - /// } - /// ``` + /// See Test code pub async fn campus_id( &self, req: FtApiCampusIdRequest, @@ -105,20 +86,17 @@ mod tests { use super::*; #[tokio::test] - async fn basic() { - let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) - .await - .unwrap(); - + async fn basic() -> ClientResult<()> { + let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; let client = FtClient::new(FtClientReqwestConnector::with_connector( reqwest::Client::new(), )); - let session = client.open_session(token); - let res = session + + let _ = session .campus_id(FtApiCampusIdRequest::new().with_per_page(1)) - .await; + .await?; - assert!(res.is_ok()); + Ok(()) } } diff --git a/libft-api/src/api/cursus/cursus_id_projects.rs b/libft-api/src/api/cursus/cursus_id_projects.rs index 4c8dd5e..66ef61c 100644 --- a/libft-api/src/api/cursus/cursus_id_projects.rs +++ b/libft-api/src/api/cursus/cursus_id_projects.rs @@ -44,25 +44,30 @@ where /// - `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().unwrap()).await.unwrap(); - /// 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(()) - /// } - /// ``` + /// ```rust + /// use libft_api::{prelude::*, info::ft_campus_id::GYEONGSAN}; + /// + /// # async fn run() -> ClientResult<()> { + /// let token = FtApiToken::try_get(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 + /// .cursus_id_projects( + /// FtApiCursusIdProjectsRequest::new(FtCursusId::new(FT_CURSUS_ID)).with_per_page(1), + /// ) + /// .await; + /// + /// # assert!(res.is_ok()); + /// # Ok(()) + /// # } + /// # tokio::runtime::Runtime::new().unwrap().block_on(run()).unwrap(); + /// ``` pub async fn cursus_id_projects( &self, req: FtApiCursusIdProjectsRequest, diff --git a/libft-api/src/api/group.rs b/libft-api/src/api/group.rs index 9848016..84f139a 100644 --- a/libft-api/src/api/group.rs +++ b/libft-api/src/api/group.rs @@ -11,34 +11,32 @@ //! # Example //! //! ```rust -//! use libft_api::prelude::*; -//! -//! async fn example() -> ClientResult<()> { -//! let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); -//! 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(()) -//! } +//! # use libft_api::prelude::*; +//! # async fn example() -> ClientResult<()> { +//! let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); +//! 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; diff --git a/libft-api/src/api/group/groups.rs b/libft-api/src/api/group/groups.rs index d1ba324..ec6f4be 100644 --- a/libft-api/src/api/group/groups.rs +++ b/libft-api/src/api/group/groups.rs @@ -63,13 +63,13 @@ where /// let session = client.open_session(token); /// /// // Get all groups with pagination - /// let groups = session + /// let res = session /// .groups( /// FtApiGroupsRequest::new() - /// .with_per_page(50) + /// .with_per_page(10) /// ) /// .await?; - /// println!("Found {} groups", groups.groups.len()); + /// println!("Found {} groups", res.groups.len()); /// /// Ok(()) /// } @@ -77,7 +77,11 @@ where pub async fn groups(&self, req: FtApiGroupsRequest) -> ClientResult { let url = "groups"; - let params = vec![to_param!(req, page), to_param!(req, per_page)]; + let params = vec![ + to_param!(req, page), + to_param!(req, per_page), + to_param!(req, user_id), + ]; self.http_session_api.http_get(url, ¶ms).await } @@ -91,7 +95,7 @@ where /// - `ClientResult`: Contains the created association details /// /// # Example - /// ```rust + /// ```rust,no_run /// use libft_api::prelude::*; /// /// async fn example() -> ClientResult<()> { @@ -162,7 +166,11 @@ mod tests { let session = client.open_session(token); session - .groups(FtApiGroupsRequest::new().with_per_page(1)) + .groups( + FtApiGroupsRequest::new() + .with_per_page(1) + .with_user_id(FtUserId::new(212750)), + ) .await .unwrap(); } diff --git a/libft-api/src/api/user.rs b/libft-api/src/api/user.rs index 0ebbd5a..679d91b 100644 --- a/libft-api/src/api/user.rs +++ b/libft-api/src/api/user.rs @@ -21,28 +21,29 @@ //! //! ```rust //! use libft_api::prelude::*; +//! use rvstruct::ValueStruct; //! -//! async fn example() -> ClientResult<()> { -//! let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); -//! let client = FtClient::new(FtClientReqwestConnector::new()); -//! let session = client.open_session(token); +//! # async fn run() -> ClientResult<()> { +//! let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); +//! 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 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(()) +//! // Get a specific user by ID +//! let user_response = session.users_id(FtApiUsersIdRequest::new(FtUserIdentifier::UserId(FtUserId::new(12345)))).await?; +//! if let Some(login) = &user_response.user.login { +//! println!("User login: {}", login.value()); //! } +//! +//! // Get a user's location data +//! let location_response = session.users_id_locations(FtApiUsersIdLocationsRequest::new(FtUserId::new(12345))).await?; +//! println!("Found {} location records", location_response.get_vec().len()); +//! # Ok(()) +//! # } +//! # tokio::runtime::Runtime::new().unwrap().block_on(run()).unwrap(); //! ``` mod users; diff --git a/libft-api/src/api/user/users.rs b/libft-api/src/api/user/users.rs index 5e96e44..ae6323a 100644 --- a/libft-api/src/api/user/users.rs +++ b/libft-api/src/api/user/users.rs @@ -61,32 +61,31 @@ where /// # Example /// ```rust /// use libft_api::prelude::*; - /// use crate::models::user::FtKind; /// - /// async fn example() -> ClientResult<()> { - /// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); - /// 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(()) - /// } + /// # async fn run() -> ClientResult<()> { + /// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()).await.unwrap(); + /// 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(()) + /// # } + /// # tokio::runtime::Runtime::new().unwrap().block_on(run()).unwrap(); /// ``` pub async fn users_post( &self, 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 7a955fd..b603c47 100644 --- a/libft-api/src/api/user/users_id_cursus_users.rs +++ b/libft-api/src/api/user/users_id_cursus_users.rs @@ -87,27 +87,26 @@ where /// # Example /// /// ```rust - /// use libft_api::{prelude::*, TEST_USER_YONDOO06_ID}; + /// use libft_api::{prelude::*, info::TEST_USER_YONDOO_ID}; /// - /// #[tokio::main] - /// async fn main() { - /// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) - /// .await - /// .unwrap(); + /// # async fn run() -> ClientResult<()> { + /// let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) + /// .await + /// .unwrap(); /// - /// let client = FtClient::new(FtClientReqwestConnector::with_connector( - /// reqwest::Client::new(), - /// )); + /// let client = FtClient::new(FtClientReqwestConnector::with_connector( + /// 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) - /// .with_per_page(30); + /// let req = FtApiUsersIdCursusUsersRequest::new(FtUserId::new(TEST_USER_YONDOO_ID)); /// - /// let response = session.users_id_cursus_users(req).await.unwrap(); + /// let response = session.users_id_cursus_users(req).await.unwrap(); /// - /// } + /// # Ok(()) + /// # } + /// # tokio::runtime::Runtime::new().unwrap().block_on(run()).unwrap(); /// ``` /// /// # Panics diff --git a/libft-api/src/auth.rs b/libft-api/src/auth.rs index b1aa584..d31eef4 100644 --- a/libft-api/src/auth.rs +++ b/libft-api/src/auth.rs @@ -85,9 +85,9 @@ impl 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")?; + pub fn build_from_env() -> Result { + let uid = std::env::var("FT_API_CLIENT_UID")?; + let secret = std::env::var("FT_API_CLIENT_SECRET")?; Ok(AuthInfo { uid, secret }) } @@ -168,7 +168,7 @@ pub enum TokenError { /// The token lifetime could not be parsed. TokenLifeTimeParsingFailed, /// The temporary token was not found. - TempTokenNotFound, + NoTempToken, /// An error occurred while building the token. BuildError(String), } @@ -194,7 +194,7 @@ impl FtApiToken { let tmpdir = Self::__get_tmp_path(); if !tmpdir.is_file() { - return Err(TokenError::TempTokenNotFound); + return Err(TokenError::NoTempToken); } let file = File::open(tmpdir)?; diff --git a/libft-api/src/common/error.rs b/libft-api/src/common/error.rs index 25c380b..180923b 100644 --- a/libft-api/src/common/error.rs +++ b/libft-api/src/common/error.rs @@ -6,6 +6,8 @@ use url::ParseError; use reqwest::StatusCode; +use crate::auth::TokenError; + #[macro_export] macro_rules! enum_into { ($vis:vis $enum_ty:ident $($enum_item:ident $(,)?)*) => { @@ -214,6 +216,31 @@ impl From for FtClientError { } } +impl From for FtClientError { + fn from(token_error: TokenError) -> Self { + match token_error { + TokenError::IOError(error) => { + FtClientError::SystemError(FtSystemError::new().with_cause(Box::new(error))) + } + TokenError::SerdeError(error) => { + FtClientError::ProtocolError(FtProtocolError::new(error)) + } + TokenError::TokenExpired + | TokenError::TokenLifeTimeParsingFailed + | TokenError::NoTempToken => { + FtClientError::ApiError(FtApiError::new("API token need to renew".to_string())) + } + TokenError::BuildError(error) => FtClientError::ApiError(FtApiError::new(error)), + } + } +} + +impl From for FtClientError { + fn from(value: std::env::VarError) -> Self { + FtClientError::SystemError(FtSystemError::new().with_cause(Box::new(value))) + } +} + impl From> for FtClientError { fn from(err: Box) -> Self { FtClientError::SystemError(FtSystemError::new().with_cause(err)) From e864ce5511bed79922c47697b1c5b27c94f330ff Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Tue, 21 Oct 2025 17:20:44 +0900 Subject: [PATCH 3/5] feat(bin): add piscine_users binary This commit introduces a new binary, `piscine_users`, for fetching users from a specific piscine session at the Gyeongsan campus. The binary parallelizes API requests to efficiently gather user data within a predefined date range and saves the results to a JSON file. --- libft-api/Cargo.toml | 4 +++ libft-api/bin/piscine_users.rs | 50 ++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 libft-api/bin/piscine_users.rs diff --git a/libft-api/Cargo.toml b/libft-api/Cargo.toml index 83bfba0..6541411 100644 --- a/libft-api/Cargo.toml +++ b/libft-api/Cargo.toml @@ -10,6 +10,10 @@ exclude = ["src/main.rs"] [lib] path="src/lib.rs" +[[bin]] +name = "piscine_users" +path = "bin/piscine_users.rs" + [[example]] name = "scroll" diff --git a/libft-api/bin/piscine_users.rs b/libft-api/bin/piscine_users.rs new file mode 100644 index 0000000..8dfffe9 --- /dev/null +++ b/libft-api/bin/piscine_users.rs @@ -0,0 +1,50 @@ +use std::{io::Write, sync::Arc}; + +use futures::FutureExt; +use libft_api::{info::ft_campus_id::GYEONGSAN, prelude::*}; +use tokio::task::JoinSet; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + let client = Arc::new(FtClient::new(FtClientReqwestConnector::new())); + + let req: ReqFn<_> = |session, page| { + async move { + session + .users( + FtApiUsersRequest::new() + .with_page(page) + .with_per_page(100) + .with_filter(vec![FtFilterOption::new( + FtFilterField::PrimaryCampusId, + vec![GYEONGSAN.to_string()], + )]) + .with_range(vec![FtRangeOption::new( + FtRangeField::CreatedAt, + vec!["2025-09-01".to_owned(), "2025-10-20".to_owned()], + )]), + ) + .await + } + .boxed() + }; + + let mut handles = JoinSet::new(); + for i in 1..=8 { + let client = Arc::clone(&client); + handles.spawn(async move { scroller(&client, 8, i, req).await }); + } + + let mut result = Vec::new(); + while let Some(res) = handles.join_next().await { + match res { + Ok(v) => result.extend(v), + Err(e) => tracing::error!("task failed: {e}"), + } + } + + let mut file = std::fs::File::create("pisciner.json").unwrap(); + file.write_all(serde_json::to_string_pretty(&result).unwrap().as_bytes()) + .unwrap(); +} From 7c824d68cb7a7fb1db33766395d9d75ccb114928 Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Wed, 22 Oct 2025 13:00:36 +0900 Subject: [PATCH 4/5] Fix doctest --- libft-api/src/common/client.rs | 40 +++++++++------------------------- libft-api/src/lib.rs | 6 ++--- libft-api/src/models.rs | 1 + libft-api/src/prelude.rs | 20 ----------------- 4 files changed, 14 insertions(+), 53 deletions(-) diff --git a/libft-api/src/common/client.rs b/libft-api/src/common/client.rs index 12800a5..b7b0864 100644 --- a/libft-api/src/common/client.rs +++ b/libft-api/src/common/client.rs @@ -9,27 +9,27 @@ 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?; @@ -52,7 +52,7 @@ 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. @@ -66,38 +66,18 @@ where } /// 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 @@ -107,7 +87,7 @@ where } /// 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. diff --git a/libft-api/src/lib.rs b/libft-api/src/lib.rs index 6e0c48f..e467325 100644 --- a/libft-api/src/lib.rs +++ b/libft-api/src/lib.rs @@ -7,9 +7,9 @@ //! //! ## Quick start //! ```rust,no_run -//! use libft_api::{prelude::*}; +//! use libft_api::prelude::{*, ft_campus_id::GYEONGSAN}; //! -//! # async fn run() -> libft_api::ClientResult<()> { +//! # async fn run() -> ClientResult<()> { //! let token = FtApiToken::try_get(AuthInfo::build_from_env()?).await?; //! let client = FtClient::new(FtClientReqwestConnector::new()); //! let session = client.open_session(token); @@ -19,7 +19,7 @@ //! ) //! .await?; //! for location in response.location { -//! println!("{} @ {}", location.user.login, location.host); +//! println!("{:?} @ {}", location.user.login, location.host); //! } //! # Ok(()) //! # } diff --git a/libft-api/src/models.rs b/libft-api/src/models.rs index f320550..5aba8d4 100644 --- a/libft-api/src/models.rs +++ b/libft-api/src/models.rs @@ -18,6 +18,7 @@ //! //! ```rust //! use libft_api::models::user::{FtUser, FtUserId}; +//! use rvstruct::ValueStruct; //! //! // Example of how a user model might be used //! fn process_user(user: FtUser) { diff --git a/libft-api/src/prelude.rs b/libft-api/src/prelude.rs index 4a8ca48..7b8abba 100644 --- a/libft-api/src/prelude.rs +++ b/libft-api/src/prelude.rs @@ -11,26 +11,6 @@ //! * 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::*; From a1a26230f67aab1d924da306d0e4f229f68dc464 Mon Sep 17 00:00:00 2001 From: Hyeokjin Doo Date: Wed, 22 Oct 2025 13:18:52 +0900 Subject: [PATCH 5/5] remove test --- libft-api-derive/src/lib.rs | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/libft-api-derive/src/lib.rs b/libft-api-derive/src/lib.rs index b056d81..47387e2 100644 --- a/libft-api-derive/src/lib.rs +++ b/libft-api-derive/src/lib.rs @@ -7,24 +7,6 @@ //! # 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; @@ -45,24 +27,6 @@ use syn::{ /// * 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);