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. 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..b056d81 100644 --- a/libft-api-derive/src/lib.rs +++ b/libft-api-derive/src/lib.rs @@ -1,14 +1,143 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right +//! 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; +use quote::quote; +use syn::{ + parse_macro_input, spanned::Spanned, Data, DeriveInput, Error, Fields, GenericArgument, + 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); + 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 = 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", + ) + })? + } + _ => { + 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", + )) + } + }; + + 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 } + } + }) } -#[cfg(test)] -mod tests { - use super::*; +fn is_vec(ty: &Type) -> bool { + matches!( + ty, + Type::Path(tp) + if tp.path.segments.last().map(|s| s.ident == "Vec").unwrap_or(false) + ) +} - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); +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()); + } + } + } + } } + None } diff --git a/libft-api/Cargo.toml b/libft-api/Cargo.toml index 78f1d83..83bfba0 100644 --- a/libft-api/Cargo.toml +++ b/libft-api/Cargo.toml @@ -10,62 +10,22 @@ 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="exam_resubscribe" -path="bin/exam_resubscribe.rs" - -# [[bin]] -# name="location_stat" -# path="bin/location_stat.rs" +[[example]] +name = "scroll" [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", "test-util"] } +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/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/bin/blackholed.rs b/libft-api/bin/blackholed.rs deleted file mode 100644 index d0b0c3a..0000000 --- a/libft-api/bin/blackholed.rs +++ /dev/null @@ -1,197 +0,0 @@ -use std::{ops::ControlFlow, sync::Arc, time::Duration}; - -use libft_api::{campus_id::*, prelude::*}; -use tokio::{sync::Semaphore, 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 permit = Arc::new(Semaphore::new(thread_num)); - 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(()) = 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(()) = 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(()); - } - } - 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, 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(()) -} diff --git a/libft-api/bin/campus_users.rs b/libft-api/bin/campus_users.rs deleted file mode 100644 index a99281d..0000000 --- a/libft-api/bin/campus_users.rs +++ /dev/null @@ -1,207 +0,0 @@ -use std::{io::Write, ops::ControlFlow, sync::Arc, time::Duration}; - -use chrono::Utc; -use libft_api::prelude::*; -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 = 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, - ] - .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!( - "/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 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(), - )?; - - 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.clone()) - .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_session(&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![GYEONGSAN.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 156eec7..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.clone()) - .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.clone()).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 16da4de..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 fa76b16..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.clone()).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 abd7c04..0000000 --- a/libft-api/bin/get_user_ext.rs +++ /dev/null @@ -1,98 +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 users_task = JoinSet::new(); - for id in ids { - 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; - } - } - }); - } - - 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())?; - - 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"); - } - - println!("Output written to: {}", file_path); - 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) => { - 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 f6218e6..0000000 --- a/libft-api/bin/journals.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::{sync::Arc, time::Duration}; - -use libft_api::{campus_id::GYEONGSAN, prelude::*, FT_PISCINE_CURSUS_ID}; -use tokio::{sync::Semaphore, task, time::sleep}; - -#[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)); - - 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()], - )]), - ) - .await; - - 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(); - } -} diff --git a/libft-api/bin/location_stat.rs b/libft-api/bin/location_stat.rs deleted file mode 100644 index 8039b41..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.clone()) -// .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 b9e06c2..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 c2493ac..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 1a433ef..0000000 --- a/libft-api/bin/teams.rs +++ /dev/null @@ -1,202 +0,0 @@ -use std::{io::Write, 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 0778f34..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 8551fe4..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/examples/scroll.rs b/libft-api/examples/scroll.rs new file mode 100644 index 0000000..125a8c8 --- /dev/null +++ b/libft-api/examples/scroll.rs @@ -0,0 +1,56 @@ +use std::{io::Write, sync::Arc}; + +use futures::FutureExt; +use libft_api::{info::ft_campus_id::SEOUL, prelude::*}; +use tokio::task::JoinSet; +use tracing::info_span; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + info_span!("main"); + let client = Arc::new(FtClient::with_ratelimits( + FtClientReqwestConnector::new(), + 8, + 14000, + )); + let thread_num = 8; + let mut handles = JoinSet::new(); + + 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 + } + .boxed() + }; + + 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..a6765ed 100644 --- a/libft-api/src/api.rs +++ b/libft-api/src/api.rs @@ -1,11 +1,75 @@ -mod campus; -mod cursus; -mod exam; -mod group; -mod project; -mod project_session; -mod project_user; -mod scale_team; -mod user; +//! 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. +//! +//! 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; +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; + +/// 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/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 48c2c99..5e644e2 100644 --- a/libft-api/src/api/campus/campus_id.rs +++ b/libft-api/src/api/campus/campus_id.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, - FtCampus, FtCampusId, FtClientHttpConnector, FtClientSession, FtFilterOption, FtRangeOption, - FtSortOption, -}; - #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiCampusIdRequest { pub campus_id: Option, @@ -17,16 +14,54 @@ 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, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +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, @@ -67,11 +102,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(); @@ -79,7 +114,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..f0561a9 100644 --- a/libft-api/src/api/campus/campus_id_journals.rs +++ b/libft-api/src/api/campus/campus_id_journals.rs @@ -2,11 +2,10 @@ 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::*; +use crate::to_param; + +use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiCampusIdJournalsRequest { @@ -17,20 +16,41 @@ pub struct FtApiCampusIdJournalsRequest { pub sort: Option>, pub range: Option>, pub filter: Option>, - pub page: Option, + pub page: Option, pub per_page: Option, } -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiCampusIdJournalsResponse { pub journals: Vec, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +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, @@ -72,11 +92,14 @@ where #[cfg(test)] mod tests { - use crate::{campus_id::GYEONGSAN, prelude::*}; + + use crate::info::ft_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(); @@ -84,7 +107,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..2aebc18 100644 --- a/libft-api/src/api/campus/campus_id_locations.rs +++ b/libft-api/src/api/campus/campus_id_locations.rs @@ -1,13 +1,10 @@ +use crate::prelude::*; +use crate::to_param; +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,16 +16,65 @@ 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, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +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, @@ -68,11 +114,12 @@ where #[cfg(test)] mod tests { - use crate::{campus_id::GYEONGSAN, prelude::*}; + use super::*; + use crate::info::ft_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(); @@ -80,7 +127,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..3fe5092 100644 --- a/libft-api/src/api/campus/campus_id_users.rs +++ b/libft-api/src/api/campus/campus_id_users.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, - FtCampusId, FtClientHttpConnector, FtClientSession, FtFilterOption, FtRangeOption, - FtSortOption, FtUser, FtUserId, -}; - #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiCampusIdUsersRequest { pub campus_id: FtCampusId, @@ -18,16 +15,64 @@ 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, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +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, @@ -65,11 +110,12 @@ where #[cfg(test)] mod tests { - use crate::{campus_id::GYEONGSAN, prelude::*}; + use super::*; + use crate::info::ft_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(); @@ -77,7 +123,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..4a05a21 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, @@ -23,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, @@ -68,7 +116,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(); @@ -76,7 +124,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.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/cursus/cursus_id_projects.rs b/libft-api/src/api/cursus/cursus_id_projects.rs index bc9bf38..0f78de0 100644 --- a/libft-api/src/api/cursus/cursus_id_projects.rs +++ b/libft-api/src/api/cursus/cursus_id_projects.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, FtProject, FtProjectId, - FtRangeOption, FtSortOption, -}; +use crate::prelude::*; +use crate::to_param; +use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiCursusIdProjectsRequest { @@ -18,16 +16,53 @@ 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, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +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, @@ -66,13 +101,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(); @@ -80,7 +112,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.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 f4dbb6b..e72ad0c 100644 --- a/libft-api/src/api/exam/exams.rs +++ b/libft-api/src/api/exam/exams.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, FtExam, FtExamId, FtExamUser, FtFilterOption, - FtRangeOption, FtSortOption, FtUserId, -}; +use crate::prelude::*; +use crate::to_param; +use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiExamsRequest { @@ -26,7 +24,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, @@ -42,30 +40,42 @@ impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { - /// ``` - /// #[tokio::test] - /// async fn post_exams() { - /// let token = FtApiToken::build(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. /// - /// let client = FtClient::new(FtClientReqwestConnector::with_connector( - /// reqwest::Client::new(), - /// )); + /// # 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 session = client.open_session(&token); + /// # Returns + /// - `ClientResult`: Contains a vector of `FtExam` objects /// - /// let res = session - /// .exams_users_post( - /// FtApiExamsUsersPostRequest::new(FtApiExamsUsersPostBody { - /// user_id: FtUserId::new(212_750), - /// }), - /// FtExamId::new(22085), + /// # 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 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 { @@ -99,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, @@ -112,13 +157,13 @@ where #[cfg(test)] mod tests { - use crate::*; + use super::*; #[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(); @@ -126,7 +171,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.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/group/groups.rs b/libft-api/src/api/group/groups.rs index 0c71a49..7f0d1d4 100644 --- a/libft-api/src/api/group/groups.rs +++ b/libft-api/src/api/group/groups.rs @@ -1,9 +1,9 @@ use rsb_derive::Builder; use serde::{Deserialize, Serialize}; -use crate::{ - to_param, ClientResult, FtClientHttpConnector, FtClientSession, FtGroup, FtGroupId, FtUserId, -}; +use crate::prelude::*; +use crate::to_param; +use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiGroupsRequest { @@ -30,16 +30,50 @@ 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, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +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, @@ -60,36 +123,36 @@ where #[cfg(test)] mod tests { - use crate::*; + 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::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 + // .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() { - let token = FtApiToken::build(AuthInfo::build_from_env().unwrap()) + let token = FtApiToken::try_get(AuthInfo::build_from_env().unwrap()) .await .unwrap(); @@ -97,7 +160,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/prelude.rs b/libft-api/src/api/prelude.rs index 8adcfb3..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::*; @@ -8,7 +41,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.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/project_data.rs b/libft-api/src/api/project/project_data.rs index 6180f6a..7ec976e 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,13 +16,13 @@ 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, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -65,13 +63,13 @@ where #[cfg(test)] mod tests { - use crate::*; + use super::*; #[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(); @@ -79,7 +77,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..f130ef9 100644 --- a/libft-api/src/api/project/projects.rs +++ b/libft-api/src/api/project/projects.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, FtProject, FtProjectId, - FtRangeOption, FtSortOption, -}; +use crate::prelude::*; +use crate::to_param; +use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiProjectRequest { @@ -18,16 +16,54 @@ 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, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +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"; @@ -63,13 +99,13 @@ where #[cfg(test)] mod tests { - use crate::*; + use super::*; #[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(); @@ -77,7 +113,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..aa24ec4 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,13 +15,13 @@ 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, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -66,13 +63,13 @@ where #[cfg(test)] mod tests { - use crate::*; + use super::*; #[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(); @@ -80,7 +77,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.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_session/project_sessions_id_scale_teams.rs b/libft-api/src/api/project_session/project_sessions_id_scale_teams.rs index d396b83..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,11 +1,9 @@ +use crate::prelude::*; +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, @@ -16,7 +14,7 @@ pub struct FtApiProjectSessionsScaleTeamsRequest { pub project_session_id: FtProjectSessionId, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -36,11 +34,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(); @@ -50,7 +48,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..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,13 +1,11 @@ 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::*; +use crate::to_param; +use libft_api_derive::HasVector; -#[derive(Debug, Serialize, Deserialize, Builder)] +#[derive(Debug, Serialize, Deserialize, Builder, HasVector)] #[serde(transparent)] pub struct FtApiProjectSessionsTeamsResponse { pub teams: Vec, @@ -23,7 +21,7 @@ pub struct FtApiProjectSessionsTeamsRequest { pub per_page: Option, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -64,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(); @@ -82,14 +78,14 @@ 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()); } #[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(); @@ -97,7 +93,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.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/project_user/projects_users.rs b/libft-api/src/api/project_user/projects_users.rs index 0ee20b1..4377cfb 100644 --- a/libft-api/src/api/project_user/projects_users.rs +++ b/libft-api/src/api/project_user/projects_users.rs @@ -1,8 +1,9 @@ +use crate::prelude::*; +use crate::to_param; +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,13 +31,13 @@ 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, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -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(); @@ -101,8 +102,8 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); - let project_ids = ALL_INNER_SUBJECTS_ID + let session = client.open_session(token); + let project_ids = COMMON_CORE_SUBJECTS .into_iter() .map(|id| id.to_string()) .collect(); 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/scale_team/scale_teams.rs b/libft-api/src/api/scale_team/scale_teams.rs index 5b61e28..3ac6b1a 100644 --- a/libft-api/src/api/scale_team/scale_teams.rs +++ b/libft-api/src/api/scale_team/scale_teams.rs @@ -1,7 +1,9 @@ +use crate::prelude::*; +use crate::to_param; +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 +14,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,13 +31,13 @@ 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, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -86,15 +88,14 @@ where #[cfg(test)] mod tests { - use campus_id::GYEONGSAN; + use crate::info::ft_campus_id::GYEONGSAN; use super::*; - use crate::*; #[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(); @@ -102,8 +103,8 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); - let res = session + let session = client.open_session(token); + 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.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 2e69647..d6d23c7 100644 --- a/libft-api/src/api/user/users.rs +++ b/libft-api/src/api/user/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 FtApiUsersPostRequest { @@ -26,7 +28,7 @@ pub struct FtApiUsersRequest { pub sort: Option>, pub range: Option>, pub filter: Option>, - pub page: Option, + pub page: Option, pub per_page: Option, } @@ -36,16 +38,56 @@ 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<'a, FCHC> FtClientSession<'a, FCHC> +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, @@ -55,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(); @@ -90,11 +183,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(); @@ -102,36 +194,9 @@ 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()); } - - // #[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 f65cdd7..e6dec9b 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,13 +16,62 @@ pub struct FtApiUsersIdRequest { #[derive(Debug, Serialize, Deserialize, Builder)] #[serde(transparent)] pub struct FtApiUsersIdResponse { - pub user: FtUserExt, + pub user: FtUser, } 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/{}", @@ -104,11 +113,11 @@ 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(); @@ -116,7 +125,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..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 @@ -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, FtCorrectionPointHistory, FtFilterOption, - FtRangeOption, FtSortOption, FtUserId, -}; +use crate::prelude::*; +use crate::to_param; +use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiUsersIdCorrectionPointHistoricsRequest { @@ -17,13 +15,13 @@ 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, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -65,11 +63,11 @@ where #[cfg(test)] 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(); @@ -77,7 +75,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..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 @@ -1,8 +1,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)] @@ -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<'a, FCHC> FtClientSession<'a, FCHC> +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() { @@ -60,7 +50,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(); @@ -68,15 +58,14 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); - let res = session + let session = client.open_session(token); + let _ = session .users_id_correction_points_add(FtApiUsersIdCorrectionPointsAddRequest { id: FtUserId::new(crate::info::TEST_USER_YONDOO_ID), 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 57a50b8..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,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 FtApiUsersIdCursusUsersRequest { @@ -26,7 +28,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, @@ -89,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(); /// @@ -97,7 +99,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 +160,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 } @@ -171,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(); // @@ -179,7 +181,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 { @@ -200,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(); @@ -208,7 +210,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..88e48e9 100644 --- a/libft-api/src/api/user/users_id_locations.rs +++ b/libft-api/src/api/user/users_id_locations.rs @@ -1,11 +1,9 @@ -use std::collections::HashMap; - -use chrono::Days; -use chrono::NaiveDate; 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 FtApiUsersIdLocationsRequest { @@ -17,7 +15,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, @@ -64,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(); @@ -77,7 +75,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..13b2987 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 { @@ -61,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(); @@ -74,7 +73,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..91e7714 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,10 @@ +use crate::prelude::*; +use crate::to_param; +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,13 +18,13 @@ 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, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -74,11 +71,11 @@ 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(); @@ -86,8 +83,8 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); - let res = session + let session = client.open_session(token); + 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 abd3a3c..7763be0 100644 --- a/libft-api/src/api/user/users_id_teams.rs +++ b/libft-api/src/api/user/users_id_teams.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, FtProjectId, - FtProjectSessionId, FtRangeOption, FtSortOption, FtTeam, FtUserId, -}; +use crate::prelude::*; +use crate::to_param; +use libft_api_derive::HasVector; #[derive(Debug, Serialize, Deserialize, Builder)] pub struct FtApiUsersIdTeamsRequest { @@ -20,13 +18,13 @@ 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, } -impl<'a, FCHC> FtClientSession<'a, FCHC> +impl FtClientSession<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -72,11 +70,11 @@ 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(); @@ -84,8 +82,8 @@ mod tests { reqwest::Client::new(), )); - let session = client.open_session(&token); - let res = session + let session = client.open_session(token); + 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 684410b..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,16 +17,74 @@ use std::{ use chrono::{DateTime, TimeZone, Utc}; use serde::{Deserialize, Serialize}; +// 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, } 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")?; @@ -27,6 +93,14 @@ 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"), @@ -38,6 +112,12 @@ 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, @@ -48,22 +128,19 @@ 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) } } -#[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")] @@ -77,14 +154,25 @@ 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), + /// 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 { fn from(err: io::Error) -> Self { TokenError::IOError(err) @@ -124,6 +212,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); @@ -140,6 +233,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)?; @@ -147,6 +245,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/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.rs b/libft-api/src/common.rs index 1ec009c..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; @@ -6,3 +30,9 @@ mod error; pub use param::*; 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 bb1d617..12800a5 100644 --- a/libft-api/src/common/client.rs +++ b/libft-api/src/common/client.rs @@ -4,44 +4,119 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; use url::Url; -use crate::{FtApiToken, FtClientError, FtClientReqwestConnector}; - +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 FCHC: FtClientHttpConnector + Send, { pub http_api: FtClientHttpApi, + pub meta: HeaderMetaData, } +/// 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 FCHC: FtClientHttpConnector + Send, { + /// The HTTP connector. 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, 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>, } +/// 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 FCHC: FtClientHttpConnector + Send, { - token: &'a FtApiToken, + token: FtApiToken, pub client: &'a FtClient, } @@ -53,19 +128,24 @@ 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, token: &'a FtApiToken, + ratelimiter: &'a HeaderMetaData, ) -> BoxFuture<'a, ClientResult> 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, token: &'a FtApiToken, + ratelimiter: &'a HeaderMetaData, params: &'p PT, ) -> BoxFuture<'a, ClientResult> where @@ -78,11 +158,12 @@ 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(), } } + /// Send an HTTP POST request to the given URI. fn http_post_uri<'a, RQ, RS>( &'a self, full_uri: Url, @@ -93,6 +174,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, @@ -109,6 +191,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, @@ -119,6 +202,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, @@ -135,6 +219,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, @@ -145,6 +230,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, @@ -161,6 +247,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()?) } @@ -170,13 +257,24 @@ 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)), + meta: HeaderMetaData::new(RateLimiter::new(2, 1200)), + } + } + + /// 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)), + meta: HeaderMetaData::new(RateLimiter::new(secondly, hourly)), } } - pub fn open_session<'a>(&'a self, token: &'a FtApiToken) -> FtClientSession<'a, FCHC> { + /// 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",); @@ -200,7 +298,7 @@ where } } -impl<'a, FCHC> FtClientHttpSessionApi<'a, FCHC> +impl FtClientHttpSessionApi<'_, FCHC> where FCHC: FtClientHttpConnector + Send + Sync, { @@ -211,7 +309,7 @@ where self.client .http_api .connector - .http_get_uri(full_uri, self.token) + .http_get_uri(full_uri, &self.token, &self.client.meta) .await } @@ -228,7 +326,7 @@ where self.client .http_api .connector - .http_get(method_relative_uri, self.token, params) + .http_get(method_relative_uri, &self.token, &self.client.meta, params) .await } @@ -244,7 +342,7 @@ where self.client .http_api .connector - .http_post(method_relative_uri, self.token, request) + .http_post(method_relative_uri, &self.token, request) .await } @@ -256,7 +354,7 @@ where self.client .http_api .connector - .http_post_uri(full_uri, self.token, request) + .http_post_uri(full_uri, &self.token, request) .await } @@ -272,7 +370,7 @@ where self.client .http_api .connector - .http_delete(method_relative_uri, self.token, request) + .http_delete(method_relative_uri, &self.token, request) .await } @@ -284,7 +382,7 @@ where self.client .http_api .connector - .http_delete_uri(full_uri, self.token, request) + .http_delete_uri(full_uri, &self.token, request) .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/paginator.rs b/libft-api/src/common/paginator.rs new file mode 100644 index 0000000..0607d7e --- /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 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/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)) diff --git a/libft-api/src/common/ratelimiter.rs b/libft-api/src/common/ratelimiter.rs new file mode 100644 index 0000000..b100013 --- /dev/null +++ b/libft-api/src/common/ratelimiter.rs @@ -0,0 +1,396 @@ +use reqwest::header::HeaderMap; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use tokio::time::{sleep_until, Instant}; + +#[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_u64 = |name: &str| -> Option { + headers + .get(name) + .and_then(|v| v.to_str().ok())? + .parse() + .ok() + }; + + if let Some(total) = parse_u64("x-total") { + *self.total_page.lock().unwrap() = total; + } + 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 { + inner: Arc>, +} + +impl RateLimiter { + pub fn new(per_second_limit: u64, hourly_limit: u64) -> 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(1), + hour_reset: now + Duration::from_secs(3600), + retry_after_until: None, + }; + Self { + inner: Arc::new(Mutex::new(inner)), + } + } + + /// 헤더 기반 갱신: 한 번만 락 잡고 끝냄 + 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() + }; + + let mut st = self.inner.lock().unwrap(); + + 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)); + } + } + + /// 요청 전 호출: 락은 매우 짧게만 잡고, 대기는 락 밖에서 수행 + 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 { + st.retry_after_until = None; // 만료됨 + Control::Recheck + } + } else { + // 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) + } + } + }; + + match decision { + Control::Permit => return, // 바로 진행 + Control::Sleep(deadline) => sleep_until(deadline).await, + Control::Recheck => {} // 즉시 루프 재검사 + } + } + } +} + +enum Control { + Permit, + Sleep(Instant), + Recheck, +} + +#[cfg(test)] +mod tests { + use super::*; + 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)), + } + } + } + + /// 제한 이내에서는 대기 없이 통과 + #[tokio::test(start_paused = true)] + async fn test_can_acquire_within_limit() { + 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; + } + // 가상 시간은 전진하지 않음(슬립이 없었단 뜻) + assert_eq!(Instant::now() - t0, Duration::from_millis(0)); + } + + /// 초당 제한 초과 시 다음 윈도우까지 정확히 대기 + #[tokio::test(start_paused = true)] + async fn test_waits_when_per_second_limit_exceeded() { + 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 } + }); + + ttime::advance(Duration::from_millis(999)).await; + assert!(!j.is_finished(), "아직 1초 미만이므로 완료되면 안됨"); + + ttime::advance(Duration::from_millis(1)).await; // 총 1s + j.await.unwrap(); + } + + /// 짧은 윈도우(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; + + let j = tokio::spawn({ + let l = limiter.clone(); + async move { l.acquire().await } + }); + + ttime::advance(Duration::from_millis(199)).await; + assert!(!j.is_finished(), "아직 200ms 전이므로 대기해야 함"); + + ttime::advance(Duration::from_millis(1)).await; // 200ms 도달 + j.await.unwrap(); + } + + /// 동시 접근 시 초당 한 배치씩 처리되는지(배칭) 확인 + #[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)); + + let mut handles = Vec::new(); + for _ in 0..32 { + let l = limiter.clone(); + handles.push(tokio::spawn(async move { l.acquire().await })); + } + + // 스케줄링 기회 부여 + tokio::task::yield_now().await; + assert_eq!( + handles.iter().filter(|h| h.is_finished()).count(), + 8, + "첫 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(); + } + } + + /// 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"), + ); + limiter.update_from_headers(&headers); + + let j = tokio::spawn({ + let l = limiter.clone(); + async move { l.acquire().await } + }); + + ttime::advance(Duration::from_millis(299)).await; + assert!(!j.is_finished()); + ttime::advance(Duration::from_millis(1)).await; // 300ms 경과 + j.await.unwrap(); + } + + /// (테스트용) 시간당 윈도우를 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(); + } + + /// 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; + + // 서버가 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 total = *meta.total_page.lock().unwrap(); + assert_eq!(total, 42); + } +} diff --git a/libft-api/src/connector.rs b/libft-api/src/connector.rs index 5bdb76f..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; @@ -5,14 +37,13 @@ use reqwest::{ header::{self, AUTHORIZATION}, Client, RequestBuilder, StatusCode, }; -use tracing::{debug, info, Span}; +use tracing::{debug, info}; use url::Url; -use crate::{ - map_serde_error, ClientResult, FtApiToken, FtClientError, FtClientHttpApiUri, - FtClientHttpConnector, FtEnvelopeMessage, FtHttpError, FtRateLimitError, FtReqwestError, -}; +use crate::auth::FtApiToken; +use crate::common::*; +/// A client for the 42 API that uses `reqwest` as the underlying HTTP client. pub struct FtClientReqwestConnector { reqwest_connector: Client, ft_api_url: String, @@ -24,18 +55,14 @@ impl Default for FtClientReqwestConnector { } } -#[derive(Debug, Clone)] -pub struct FtClientApiCallContext<'a> { - pub tracing_span: &'a Span, - 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 { @@ -44,6 +71,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 { @@ -57,10 +85,14 @@ impl FtClientReqwestConnector { &'a self, reqwest: RequestBuilder, url: Url, + meta: Option<&'a HeaderMetaData>, ) -> ClientResult where RS: for<'de> serde::de::Deserialize<'de>, { + if let Some(meta) = meta { + meta.ratelimiter.acquire().await; + } let url_str = url.to_string(); info!(ft_url = url_str, "Sending HTTP request to"); let http_res = reqwest @@ -68,19 +100,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(meta) = meta { + meta.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 +141,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 +149,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 +168,7 @@ impl FtClientHttpConnector for FtClientReqwestConnector { &'a self, full_uri: url::Url, token: &'a FtApiToken, + ratelimiter: &'a HeaderMetaData, ) -> futures::prelude::future::BoxFuture<'a, ClientResult> where RS: for<'de> serde::de::Deserialize<'de> + Send + 'a, @@ -150,7 +180,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 +204,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 +227,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 +250,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/info.rs b/libft-api/src/info.rs index 0460b48..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; @@ -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 212941e..6e0c48f 100644 --- a/libft-api/src/lib.rs +++ b/libft-api/src/lib.rs @@ -1,17 +1,64 @@ -#![warn(clippy::pedantic)] -#![feature(concat_idents)] +// #![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. +//! +//! ## 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. +//! * `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. +#![feature(macro_metavar_expr_concat)] +#![allow(unexpected_cfgs)] -pub use api::*; -mod api; -pub use auth::*; -mod auth; -pub use common::*; +pub mod api; +pub mod models; + +pub mod auth; mod common; -pub use models::*; -mod models; -pub use connector::*; -mod connector; -pub use axum_support::*; -mod axum_support; -pub use info::*; -mod info; + +pub mod info; +pub mod prelude; + +pub mod connector; diff --git a/libft-api/src/models.rs b/libft-api/src/models.rs index fadd034..f320550 100644 --- a/libft-api/src/models.rs +++ b/libft-api/src/models.rs @@ -1,95 +1,55 @@ -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); - } -} +//! 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; +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/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..81365dd 100644 --- a/libft-api/src/models/locations.rs +++ b/libft-api/src/models/locations.rs @@ -1,8 +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}; -use crate::models::{FtCampusId, FtDateTimeUtc, FtUser}; - +/// 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, @@ -14,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/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 e174ddf..aa19642 100644 --- a/libft-api/src/models/user.rs +++ b/libft-api/src/models/user.rs @@ -1,36 +1,53 @@ +//! 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}; -use crate::{FtDateTimeFixedOffset, FtDateTimeUtc, FtHost, FtImage, FtUrl}; - +/// 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 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, diff --git a/libft-api/src/prelude.rs b/libft-api/src/prelude.rs new file mode 100644 index 0000000..4a8ca48 --- /dev/null +++ b/libft-api/src/prelude.rs @@ -0,0 +1,40 @@ +//! 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::*; +pub use crate::connector::FtClientReqwestConnector; +pub use crate::info::*; +pub use crate::models::prelude::*;