From 7bffc5f16a0649c23cf3057b83ca99be0c7bcb33 Mon Sep 17 00:00:00 2001 From: Natty Date: Fri, 22 Sep 2023 20:10:48 +0200 Subject: [PATCH] Replaced the WASM in SDK with a more sensible ts_rs setup --- Cargo.lock | 235 +-------------------- Cargo.toml | 2 +- magnetar_sdk/Cargo.toml | 33 +-- magnetar_sdk/macros/src/lib.rs | 36 +++- magnetar_sdk/src/client/fetch.rs | 277 ------------------------- magnetar_sdk/src/client/mod.rs | 12 -- magnetar_sdk/src/client/reqwest.rs | 112 ---------- magnetar_sdk/src/endpoints/list.rs | 21 -- magnetar_sdk/src/endpoints/mod.rs | 24 +-- magnetar_sdk/src/endpoints/timeline.rs | 35 ++++ magnetar_sdk/src/endpoints/user.rs | 47 ++++- magnetar_sdk/src/lib.rs | 27 +-- magnetar_sdk/src/types/drive.rs | 15 +- magnetar_sdk/src/types/emoji.rs | 9 +- magnetar_sdk/src/types/mod.rs | 10 + magnetar_sdk/src/types/note.rs | 25 ++- magnetar_sdk/src/types/timeline.rs | 11 + magnetar_sdk/src/types/user.rs | 98 ++++----- magnetar_sdk/src/util_types.rs | 78 +++++++ 19 files changed, 295 insertions(+), 812 deletions(-) delete mode 100644 magnetar_sdk/src/client/fetch.rs delete mode 100644 magnetar_sdk/src/client/mod.rs delete mode 100644 magnetar_sdk/src/client/reqwest.rs delete mode 100644 magnetar_sdk/src/endpoints/list.rs create mode 100644 magnetar_sdk/src/endpoints/timeline.rs create mode 100644 magnetar_sdk/src/types/timeline.rs create mode 100644 magnetar_sdk/src/util_types.rs diff --git a/Cargo.lock b/Cargo.lock index 08b4f40..8c7c9e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -514,7 +514,7 @@ dependencies = [ [[package]] name = "ck" -version = "0.1.0" +version = "0.2.0" dependencies = [ "sea-orm", "serde", @@ -587,16 +587,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "795bc6e66a8e340f075fcf6227e417a2dc976b92b91f3cdc778bb858778b6747" -[[package]] -name = "core-foundation" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -755,15 +745,6 @@ dependencies = [ "serde", ] -[[package]] -name = "encoding_rs" -version = "0.8.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" -dependencies = [ - "cfg-if", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -840,21 +821,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.0" @@ -1225,19 +1191,6 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper", - "native-tls", - "tokio", - "tokio-native-tls", -] - [[package]] name = "iana-time-zone" version = "0.1.57" @@ -1334,12 +1287,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "ipnet" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" - [[package]] name = "is-terminal" version = "0.4.9" @@ -1537,19 +1484,12 @@ dependencies = [ name = "magnetar_sdk" version = "0.2.0" dependencies = [ - "async-trait", "chrono", "http", - "js-sys", "magnetar_sdk_macros", - "reqwest", "serde", - "serde-wasm-bindgen", "serde_json", "ts-rs", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", ] [[package]] @@ -1664,24 +1604,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "native-tls" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" -dependencies = [ - "lazy_static", - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nom" version = "7.1.3" @@ -1786,50 +1708,6 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" -[[package]] -name = "openssl" -version = "0.10.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.28", -] - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.90" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "ordered-float" version = "3.7.0" @@ -2262,43 +2140,6 @@ dependencies = [ "bytecheck", ] -[[package]] -name = "reqwest" -version = "0.11.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" -dependencies = [ - "base64 0.21.2", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "hyper", - "hyper-tls", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-native-tls", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg", -] - [[package]] name = "ring" version = "0.16.20" @@ -2451,15 +2292,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "schannel" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" -dependencies = [ - "windows-sys", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -2641,29 +2473,6 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" -[[package]] -name = "security-framework" -version = "2.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "serde" version = "1.0.180" @@ -2673,17 +2482,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-wasm-bindgen" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" -dependencies = [ - "js-sys", - "serde", - "wasm-bindgen", -] - [[package]] name = "serde_derive" version = "1.0.180" @@ -3335,16 +3133,6 @@ dependencies = [ "syn 2.0.28", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-stream" version = "0.1.14" @@ -3784,18 +3572,6 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "wasm-bindgen-macro" version = "0.2.87" @@ -3965,15 +3741,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - [[package]] name = "wyz" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 5ab99d9..fea805b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,7 +73,7 @@ cached = { workspace = true } chrono = { workspace = true } dotenvy = { workspace = true } -axum = { workspace = true, features = ["macros"] } +axum = { workspace = true, features = ["macros", "headers"] } headers = { workspace = true } hyper = { workspace = true, features = ["full"] } tokio = { workspace = true, features = ["full"] } diff --git a/magnetar_sdk/Cargo.toml b/magnetar_sdk/Cargo.toml index 73b4f32..da0d5df 100644 --- a/magnetar_sdk/Cargo.toml +++ b/magnetar_sdk/Cargo.toml @@ -3,44 +3,13 @@ name = "magnetar_sdk" version.workspace = true edition.workspace = true -[lib] -crate-type = ["rlib", "cdylib"] - -[features] -reqwest = ["dep:reqwest"] - -[target.'cfg(not(target_arch = "wasm32"))'.features] -default = ["reqwest"] - -[target.'cfg(target_arch = "wasm32")'.features] -default = [] - - [dependencies] magnetar_sdk_macros = { path = "./macros" } chrono = { workspace = true, features = ["serde"] } -reqwest = { workspace = true, features = ["json"], optional = true } http = { workspace = true } -async-trait = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -ts-rs = { workspace = true, features = ["chrono", "chrono-impl"] } - -[target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = { workspace = true } -wasm-bindgen-futures = { workspace = true } -serde-wasm-bindgen = { workspace = true } -chrono = { workspace = true, features = ["serde", "wasm-bindgen", "js-sys"] } -js-sys = { workspace = true } -web-sys = { workspace = true, features = [ - "Headers", - "Request", - "RequestInit", - "RequestMode", - "Response", - "Window", - "UrlSearchParams" -] } +ts-rs = { workspace = true, features = ["chrono", "chrono-impl"] } \ No newline at end of file diff --git a/magnetar_sdk/macros/src/lib.rs b/magnetar_sdk/macros/src/lib.rs index 7339dc9..34b07c1 100644 --- a/magnetar_sdk/macros/src/lib.rs +++ b/magnetar_sdk/macros/src/lib.rs @@ -2,11 +2,11 @@ use proc_macro::TokenStream; use std::collections::HashSet; use syn::parse::Parse; use syn::punctuated::Punctuated; -use syn::{Expr, ExprLit, ExprPath, Ident, Lit, Meta, MetaNameValue, Token}; +use syn::{Expr, ExprLit, ExprPath, Ident, Lit, Meta, MetaNameValue, Token, Type}; struct Field { name: Ident, - ty: Ident, + ty: Type, } struct PackInput { @@ -50,9 +50,17 @@ pub fn pack(item: TokenStream) -> TokenStream { let names = fields.iter().map(|f| &f.name); let types = fields.iter().map(|f| &f.ty); + let types_packed = fields.iter().map(|f| &f.ty); + + let names_packed = fields.iter().map(|f| &f.name); + let iota = (0..fields.len()).map(syn::Index::from); let export_path = format!("bindings/packed/{}.ts", struct_name); + let tuple = quote::quote! { + (#(#types_packed,)*) + }; + let expanded = quote::quote! { #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export, export_to = #export_path)] @@ -62,6 +70,18 @@ pub fn pack(item: TokenStream) -> TokenStream { pub #names: #types ),* } + + impl Packed for #struct_name { + type Input = #tuple; + + fn pack_from(from: #tuple) -> Self { + #struct_name { + #( + #names_packed: from.#iota + ),* + } + } + } }; TokenStream::from(expanded) @@ -172,6 +192,10 @@ pub fn derive_endpoint(item: TokenStream) -> TokenStream { let response = response.expect("missing response attribute"); let ts_path = format!("bindings/endpoints/{}.ts", name); + let export_name = Ident::new( + &format!("export_bindings_{}", struct_name.to_string().to_lowercase()), + struct_name.span(), + ); let expanded = quote::quote! { impl Default for #struct_name { @@ -191,7 +215,7 @@ pub fn derive_endpoint(item: TokenStream) -> TokenStream { #[cfg(test)] #[test] - fn export_bindings_getuserbyid() { + fn #export_name() { #struct_name::export().expect("could not export type"); } @@ -219,10 +243,10 @@ pub fn derive_endpoint(item: TokenStream) -> TokenStream { #struct_name::NAME.to_string() } - fn dependencies() -> Vec { + fn dependencies() -> Vec { vec![ - Dependency::from_ty::<<#struct_name as Endpoint>::Request>().unwrap(), - Dependency::from_ty::<<#struct_name as Endpoint>::Response>().unwrap(), + ts_rs::Dependency::from_ty::<<#struct_name as Endpoint>::Request>().unwrap(), + ts_rs::Dependency::from_ty::<<#struct_name as Endpoint>::Response>().unwrap(), ] } diff --git a/magnetar_sdk/src/client/fetch.rs b/magnetar_sdk/src/client/fetch.rs deleted file mode 100644 index a8cae60..0000000 --- a/magnetar_sdk/src/client/fetch.rs +++ /dev/null @@ -1,277 +0,0 @@ -use crate::client::ApiClientImpl; -use crate::endpoints::list::match_endpoint; -use crate::endpoints::{Endpoint, ErrorKind, ResponseError}; -use crate::Client; -use http::Method; -use serde::de::DeserializeOwned; -use serde::Serialize; -use std::collections::HashMap; -use wasm_bindgen::prelude::*; - -pub struct FetchClient { - base_url: String, - application: String, -} - -impl FetchClient { - pub fn new(base_url: &str, application: &str) -> Self { - FetchClient { - base_url: base_url.to_string(), - application: application.to_string(), - } - } -} - -#[async_trait::async_trait(?Send)] -impl Client for FetchClient { - fn base_url(&self) -> &str { - &self.base_url - } - - async fn call<'a, I, O, E>( - &self, - endpoint: &E, - data: &I, - path_params: &impl AsRef<[(&'a str, &'a str)]>, - ) -> Result - where - I: Serialize + DeserializeOwned + Send + 'static, - O: Serialize + DeserializeOwned + Send + 'static, - E: Endpoint + Send + 'static, - { - let url = endpoint.template_path(self.base_url(), path_params); - let url = if E::METHOD == Method::GET { - let search = web_sys::UrlSearchParams::new_with_str_sequence_sequence( - &serde_wasm_bindgen::to_value(data).unwrap(), - ) - .unwrap(); - - format!("{}?{}", url, search.to_string()) - } else { - url - }; - - let mut opts = web_sys::RequestInit::new(); - opts.method(E::METHOD.as_str()); - if E::METHOD != Method::GET { - opts.body(Some(&serde_wasm_bindgen::to_value(data).unwrap())); - } - let headers = web_sys::Headers::new().unwrap(); - headers.set("Content-Type", "application/json").unwrap(); - headers.set("User-Agent", &self.application).unwrap(); - opts.headers(&headers); - - let req = web_sys::Request::new_with_str_and_init(&url, &opts).unwrap(); - - let js_response = wasm_bindgen_futures::JsFuture::from( - web_sys::window() - .ok_or_else(|| ResponseError { - kind: ErrorKind::Other, - code: "FetchClient:NoWindow".to_string(), - message: "No window object".to_string(), - status: None, - })? - .fetch_with_request(&req), - ) - .await - .map_err(|e| ResponseError { - kind: ErrorKind::Other, - code: "FetchClient:FetchFail".to_string(), - message: format!("{:#?}", e), - status: None, - })?; - - let response = web_sys::Response::from(js_response); - let status = response.status(); - - if (400..=599).contains(&status) { - let body = wasm_bindgen_futures::JsFuture::from(response.json().map_err(|e| { - ResponseError { - kind: ErrorKind::Other, - code: "FetchClient:ResponseErrorPromiseFail".to_string(), - message: format!("{:#?}", e), - status: Some(status), - } - })?) - .await - .map_err(|e| ResponseError { - kind: ErrorKind::ApiError, - code: "FetchClient:ResponseErrorFail".to_string(), - message: format!("{:#?}", e), - status: Some(status), - })?; - - let data = serde_wasm_bindgen::from_value::(body).map_err(|e| { - ResponseError { - kind: ErrorKind::ApiError, - code: "FetchClient:ResponseErrorDeserializeFail".to_string(), - message: format!("{:#?}", e), - status: Some(status), - } - })?; - - return Err(data); - } else if (200..=299).contains(&status) { - if status == 204 { - return if let Some(val) = E::default_response() { - Ok(val) - } else { - Err(ResponseError { - kind: ErrorKind::ApiError, - code: "FetchClient:ResponseError204".to_string(), - message: "Response is empty".to_string(), - status: Some(status), - }) - }; - } - - let body = wasm_bindgen_futures::JsFuture::from(response.json().map_err(|e| { - ResponseError { - kind: ErrorKind::Other, - code: "FetchClient:ResponseJsonPromiseFail".to_string(), - message: format!("{:#?}", e), - status: None, - } - })?) - .await - .map_err(|e| ResponseError { - kind: ErrorKind::Other, - code: "FetchClient:ResponseJsonFail".to_string(), - message: format!("{:#?}", e), - status: None, - })?; - - let data = serde_wasm_bindgen::from_value::(body).map_err(|e| ResponseError { - kind: ErrorKind::Other, - code: "FetchClient:ResponseErrorDeserializeFail".to_string(), - message: e.to_string(), - status: None, - })?; - - return Ok(data); - } - - Err(ResponseError { - kind: ErrorKind::ApiError, - code: "FetchClient:ApiUnknownStatusError".to_string(), - message: response.status().to_string(), - status: Some(response.status()), - }) - } -} - -#[wasm_bindgen(getter_with_clone)] -pub struct ApiClientOptions { - base_url: String, - application: String, -} - -#[wasm_bindgen] -pub struct ApiClient { - client: ApiClientImpl, -} - -#[wasm_bindgen] -impl ApiClient { - #[wasm_bindgen(constructor)] - pub fn new(implementation: &str, options: ApiClientOptions) -> Option { - match implementation { - "fetch" => Some(Self { - client: ApiClientImpl::Fetch(FetchClient::new( - &options.base_url, - &options.application, - )), - }), - #[cfg(feature = "reqwest")] - "reqwest" => Some(Self { - client: ApiClientImpl::Reqwest(crate::client::reqwest::ReqwestClient::new( - &options.base_url, - &options.application, - )), - }), - _ => None, - } - } - - async fn call( - &self, - endpoint: &E, - data: &I, - path_params: &[(&str, &str)], - ) -> Result - where - I: Serialize + DeserializeOwned + Send + 'static, - O: Serialize + DeserializeOwned + Send + 'static, - E: Endpoint + Send + 'static, - { - match &self.client { - ApiClientImpl::Fetch(client) => { - client.call::(endpoint, data, &path_params).await - } - #[cfg(feature = "reqwest")] - ApiClientImpl::Reqwest(client) => { - client.call::(endpoint, data, &path_params).await - } - } - } -} - -#[wasm_bindgen(getter_with_clone)] -#[derive(Debug, Clone)] -pub struct EndpointLike { - pub path: String, - pub method: String, -} - -#[wasm_bindgen] -pub async fn api_call( - client: &ApiClient, - endpoint_like: &EndpointLike, - data: JsValue, - path_params: JsValue, -) -> Result { - let method = Method::try_from(endpoint_like.method.as_str()).map_err(|e| ResponseError { - kind: ErrorKind::Other, - code: "Bindgen::ParseMethod".to_string(), - message: e.to_string(), - status: None, - })?; - - let endpoint = match_endpoint(&endpoint_like.path, &method).ok_or_else(|| ResponseError { - kind: ErrorKind::Other, - code: "Bindgen::MatchEndpoint".to_string(), - message: format!( - "No endpoint `{}` with method `{}` found", - endpoint_like.path, method - ), - status: None, - })?; - - let input = serde_wasm_bindgen::from_value(data).map_err(|e| ResponseError { - kind: ErrorKind::Other, - code: "Bindgen::DeserializeInput".to_string(), - message: e.to_string(), - status: None, - })?; - - let path_params = serde_wasm_bindgen::from_value::>(path_params) - .map_err(|e| ResponseError { - kind: ErrorKind::Other, - code: "Bindgen::DeserializePathParams".to_string(), - message: e.to_string(), - status: None, - })?; - - let path_params_borrow = path_params - .iter() - .map(|(k, v)| (k.as_str(), v.as_str())) - .collect::>(); - - serde_wasm_bindgen::to_value(&client.call(&endpoint, &input, &path_params_borrow).await?) - .map_err(|e| ResponseError { - kind: ErrorKind::Other, - code: "Bindgen::SerializeOutput".to_string(), - message: e.to_string(), - status: None, - }) -} diff --git a/magnetar_sdk/src/client/mod.rs b/magnetar_sdk/src/client/mod.rs deleted file mode 100644 index 6cedeae..0000000 --- a/magnetar_sdk/src/client/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[cfg(feature = "reqwest")] -pub mod reqwest; - -#[cfg(target_arch = "wasm32")] -pub mod fetch; - -pub enum ApiClientImpl { - #[cfg(feature = "reqwest")] - Reqwest(reqwest::ReqwestClient), - #[cfg(target_arch = "wasm32")] - Fetch(fetch::FetchClient), -} diff --git a/magnetar_sdk/src/client/reqwest.rs b/magnetar_sdk/src/client/reqwest.rs deleted file mode 100644 index a8930dd..0000000 --- a/magnetar_sdk/src/client/reqwest.rs +++ /dev/null @@ -1,112 +0,0 @@ -use crate::endpoints::{Endpoint, ErrorKind, ResponseError}; -use crate::Client; -use http::header::{CONTENT_TYPE, USER_AGENT}; -use http::{HeaderMap, HeaderValue, Method, StatusCode}; -use serde::de::DeserializeOwned; -use serde::Serialize; - -pub struct ReqwestClient { - client: reqwest::Client, - base_url: String, -} - -impl ReqwestClient { - pub fn new(base_url: &str, application: &str) -> Self { - let mut headers = HeaderMap::new(); - headers.insert( - CONTENT_TYPE, - HeaderValue::from_str("application/json").unwrap(), - ); - headers.insert(USER_AGENT, HeaderValue::from_str(application).unwrap()); - - let client_builder = reqwest::ClientBuilder::new().default_headers(headers); - - #[cfg(not(target_arch = "wasm32"))] - let client_builder = { client_builder.https_only(true) }; - - let client = client_builder.build().unwrap(); - - ReqwestClient { - client, - base_url: base_url.to_string(), - } - } -} - -#[async_trait::async_trait(?Send)] -impl Client for ReqwestClient { - fn base_url(&self) -> &str { - &self.base_url - } - - async fn call<'a, I, O, E>( - &self, - endpoint: &E, - data: &I, - path_params: &impl AsRef<[(&'a str, &'a str)]>, - ) -> Result - where - I: Serialize + DeserializeOwned + Send + 'static, - O: Serialize + DeserializeOwned + Send + 'static, - E: Endpoint + Send + 'static, - { - let url = endpoint.template_path(self.base_url(), path_params); - - let req = self.client.request(E::METHOD, &url); - - let req = if E::METHOD == Method::GET { - req.query(&data) - } else { - req.json(&data) - }; - - let response = req.send().await.map_err(|e| ResponseError { - kind: ErrorKind::Other, - code: "ReqwestClient:Fail".to_string(), - message: e.to_string(), - status: None, - })?; - - let status = response.status(); - if status.is_client_error() || status.is_server_error() { - match response.json::().await { - Ok(res) => Err(res), - Err(e) => Err(ResponseError { - kind: ErrorKind::ApiError, - code: "ReqwestClient:ApiGenericError".to_string(), - message: e.to_string(), - status: Some(status.as_u16()), - }), - } - } else if status.is_success() { - if status == StatusCode::NO_CONTENT.as_u16() { - return if let Some(val) = E::default_response() { - Ok(val) - } else { - Err(ResponseError { - kind: ErrorKind::ApiError, - code: "ReqwestClient:ResponseError204".to_string(), - message: "Response is empty".to_string(), - status: Some(status.as_u16()), - }) - }; - } - - let data = response.json::().await.map_err(|e| ResponseError { - kind: ErrorKind::ApiError, - code: "ReqwestClient:JsonError".to_string(), - message: e.to_string(), - status: Some(status.as_u16()), - })?; - - Ok(data) - } else { - Err(ResponseError { - kind: ErrorKind::ApiError, - code: "ReqwestClient:ApiUnknownStatusError".to_string(), - message: status.to_string(), - status: Some(status.as_u16()), - }) - } - } -} diff --git a/magnetar_sdk/src/endpoints/list.rs b/magnetar_sdk/src/endpoints/list.rs deleted file mode 100644 index 572e446..0000000 --- a/magnetar_sdk/src/endpoints/list.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::endpoints::Endpoint; - -macro_rules! match_from { - (($endpoint:expr, $method:expr) in [$($e:ty,)*]) => { - match ($endpoint, $method) { - $( - (<$e as Endpoint>::ENDPOINT, &<$e as Endpoint>::METHOD) => Some(<$e as Default>::default()), - )* - _ => None, - } - }; -} - -pub(crate) fn match_endpoint(endpoint: &str, method: &http::Method) -> Option { - match_from!( - (endpoint, method) - in [ - crate::endpoints::user::GetUserById, - ] - ) -} diff --git a/magnetar_sdk/src/endpoints/mod.rs b/magnetar_sdk/src/endpoints/mod.rs index 8d37ace..f77198d 100644 --- a/magnetar_sdk/src/endpoints/mod.rs +++ b/magnetar_sdk/src/endpoints/mod.rs @@ -1,4 +1,4 @@ -pub(crate) mod list; +pub mod timeline; pub mod user; use http::Method; @@ -6,10 +6,6 @@ use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use ts_rs::TS; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::prelude::*; - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter_with_clone))] #[derive(Clone, Debug, Serialize, Deserialize, TS)] pub struct ResponseError { pub kind: ErrorKind, @@ -18,7 +14,6 @@ pub struct ResponseError { pub message: String, } -#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] #[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, TS)] pub enum ErrorKind { #[default] @@ -40,18 +35,7 @@ pub trait Endpoint { type Request: Serialize + DeserializeOwned + Send + Sync + 'static; type Response: Serialize + DeserializeOwned + Send + Sync + 'static; - - fn default_response() -> Option { - None - } - - fn template_path<'a>(&self, base_path: &str, var: &impl AsRef<[(&'a str, &'a str)]>) -> String { - let mut path_suffix = Self::ENDPOINT.to_string(); - - for (key, value) in var.as_ref() { - path_suffix = path_suffix.replace(&format!(":{}", key), value); - } - - format!("{}{}", base_path, path_suffix) - } } + +pub type Req = ::Request; +pub type Res = ::Response; diff --git a/magnetar_sdk/src/endpoints/timeline.rs b/magnetar_sdk/src/endpoints/timeline.rs new file mode 100644 index 0000000..17e1f9c --- /dev/null +++ b/magnetar_sdk/src/endpoints/timeline.rs @@ -0,0 +1,35 @@ +use crate::endpoints::Endpoint; +use crate::types::note::{NoteListFilter, PackNoteFull}; +use crate::util_types::U64Range; +use http::Method; +use magnetar_sdk_macros::Endpoint; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +// Get timeline notes +#[derive(Serialize, Deserialize, TS)] +#[ts(export)] +pub struct GetTimelineReq { + #[serde(default = "default_timeline_limit")] + pub limit: U64Range<1, 100>, + #[serde(flatten)] + pub filter: Option, +} + +fn default_timeline_limit() -> U64Range { + 15.try_into().unwrap() +} + +#[derive(Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(transparent)] +pub struct GetTimelineRes(pub Vec); + +#[derive(Endpoint)] +#[endpoint( + endpoint = "/timeline", + method = Method::GET, + request = GetTimelineReq, + response = Vec::, +)] +pub struct GetTimeline; diff --git a/magnetar_sdk/src/endpoints/user.rs b/magnetar_sdk/src/endpoints/user.rs index e3261e7..df488fc 100644 --- a/magnetar_sdk/src/endpoints/user.rs +++ b/magnetar_sdk/src/endpoints/user.rs @@ -2,23 +2,54 @@ use crate::endpoints::Endpoint; use http::Method; use magnetar_sdk_macros::Endpoint; use serde::{Deserialize, Serialize}; -use ts_rs::{Dependency, TS}; +use ts_rs::TS; -use crate::types::user::PackUserProfileFull; +use crate::types::user::{PackUserMaybeAll, PackUserSelfMaybeAll}; +// Get self +#[derive(Serialize, Deserialize, TS)] +#[ts(export)] +pub struct UserSelfReq { + #[serde(default)] + pub profile: bool, + #[serde(default)] + pub pins: bool, + #[serde(default)] + pub detail: bool, + #[serde(default)] + pub secrets: bool, +} + +#[derive(Endpoint)] +#[endpoint( + endpoint = "/users/@self/overview/info", + method = Method::GET, + request = UserSelfReq, + response = PackUserSelfMaybeAll +)] +pub struct GetUserSelf; + +// Get user by id #[derive(Serialize, Deserialize, TS)] #[ts(export)] pub struct UserByIdReq { - id: String, - #[serde(skip_serializing_if = "Option::is_none")] - detail: Option, + #[serde(default)] + pub profile: bool, + #[serde(default)] + pub pins: bool, + #[serde(default)] + pub detail: bool, + #[serde(default)] + pub relation: bool, + #[serde(default)] + pub auth: bool, } -#[derive(Endpoint, Deserialize)] +#[derive(Endpoint)] #[endpoint( - endpoint = "/users/:user_id", + endpoint = "/users/:user_id/info", method = Method::GET, request = UserByIdReq, - response = PackUserProfileFull + response = PackUserMaybeAll )] pub struct GetUserById; diff --git a/magnetar_sdk/src/lib.rs b/magnetar_sdk/src/lib.rs index a772769..a12afe6 100644 --- a/magnetar_sdk/src/lib.rs +++ b/magnetar_sdk/src/lib.rs @@ -1,23 +1,16 @@ -pub mod client; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + pub mod endpoints; pub mod types; +pub mod util_types; -use crate::endpoints::Endpoint; -use endpoints::ResponseError; -use serde::{de::DeserializeOwned, Serialize}; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS)] +#[repr(transparent)] +pub struct Required(pub T); -#[async_trait::async_trait(?Send)] -pub trait Client { - fn base_url(&self) -> &str; +pub trait Packed: 'static { + type Input: 'static; - async fn call<'a, I, O, E>( - &self, - endpoint: &E, - data: &I, - path_params: &impl AsRef<[(&'a str, &'a str)]>, - ) -> Result - where - I: Serialize + DeserializeOwned + Send + 'static, - O: Serialize + DeserializeOwned + Send + 'static, - E: Endpoint + Send + 'static; + fn pack_from(val: Self::Input) -> Self; } diff --git a/magnetar_sdk/src/types/drive.rs b/magnetar_sdk/src/types/drive.rs index 6d58394..87a8a93 100644 --- a/magnetar_sdk/src/types/drive.rs +++ b/magnetar_sdk/src/types/drive.rs @@ -1,3 +1,4 @@ +use crate::{Packed, Required}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use ts_rs::TS; @@ -29,7 +30,7 @@ pub struct DriveFolderBase { pub user_id: String, } -pack!(PackDriveFolderBase, Id as id & DriveFolderBase as folder); +pack!(PackDriveFolderBase, Required as id & Required as folder); #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] @@ -39,7 +40,7 @@ pub struct DriveFolderParentExt { pack!( PackDriveFolderWithParent, - Id as id & DriveFolderBase as folder & DriveFolderParentExt as parent + Required as id & Required as folder & Required as parent ); #[derive(Clone, Debug, Deserialize, Serialize, TS)] @@ -56,10 +57,10 @@ pub struct DriveFileBase { pub sensitive: bool, pub comment: Option, pub folder_id: Option, - pub user_id: String, + pub user_id: Option, } -pack!(PackDriveFileBase, Id as id & DriveFileBase as file); +pack!(PackDriveFileBase, Required as id & Required as file); #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] @@ -69,7 +70,7 @@ pub struct DriveFileFolderExt { pack!( PackDriveFileWithFolder, - Id as id & DriveFileBase as file & DriveFileFolderExt as folder + Required as id & Required as file & Required as folder ); #[derive(Clone, Debug, Deserialize, Serialize, TS)] @@ -80,10 +81,10 @@ pub struct DriveFileUserExt { pack!( PackDriveFileWithUser, - Id as id & DriveFileBase as file & DriveFileUserExt as user + Required as id & Required as file & Required as user ); pack!( PackDriveFileFull, - Id as id & DriveFileBase as file & DriveFileFolderExt as folder & DriveFileUserExt as user + Required as id & Required as file & Required as folder & Required as user ); diff --git a/magnetar_sdk/src/types/emoji.rs b/magnetar_sdk/src/types/emoji.rs index 5bcb8a3..1be8f1b 100644 --- a/magnetar_sdk/src/types/emoji.rs +++ b/magnetar_sdk/src/types/emoji.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; use crate::types::Id; +use crate::{Packed, Required}; use magnetar_sdk_macros::pack; #[derive(Clone, Debug, Deserialize, Serialize, TS)] @@ -9,14 +10,14 @@ use magnetar_sdk_macros::pack; pub struct EmojiBase { pub shortcode: String, pub url: String, - pub static_url: String, - pub visible_in_picker: bool, pub category: Option, + pub width: Option, + pub height: Option, } -pack!(PackEmojiBase, Id as id & EmojiBase as emoji); +pack!(PackEmojiBase, Required as id & Required as emoji); #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] #[repr(transparent)] -pub struct EmojiContext(pub Vec); +pub struct EmojiContext(pub Vec); diff --git a/magnetar_sdk/src/types/mod.rs b/magnetar_sdk/src/types/mod.rs index e52aeb0..09ec8c0 100644 --- a/magnetar_sdk/src/types/mod.rs +++ b/magnetar_sdk/src/types/mod.rs @@ -1,11 +1,21 @@ pub mod drive; pub mod emoji; pub mod note; +pub mod timeline; pub mod user; +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use std::ops::RangeInclusive; use ts_rs::TS; +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +pub enum RangeFilter { + TimeRange(RangeInclusive>), + TimeStart(DateTime), + TimeEnd(DateTime), +} + #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] #[repr(transparent)] diff --git a/magnetar_sdk/src/types/note.rs b/magnetar_sdk/src/types/note.rs index ad09fc1..233863e 100644 --- a/magnetar_sdk/src/types/note.rs +++ b/magnetar_sdk/src/types/note.rs @@ -1,6 +1,7 @@ use crate::types::emoji::EmojiContext; use crate::types::user::{PackUserBase, UserBase}; use crate::types::Id; +use crate::{Packed, Required}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use ts_rs::TS; @@ -9,13 +10,21 @@ use crate::types::drive::PackDriveFileBase; use magnetar_sdk_macros::pack; +#[derive(Copy, Clone, Default, Debug, Deserialize, Serialize, TS)] +#[ts(export)] +pub struct NoteListFilter { + show_renotes: Option, + show_replies: Option, + show_files_only: Option, + uncwed_sensitive: Option, +} + #[derive(Copy, Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] pub enum NoteVisibility { Public, Home, Followers, - Specified, Direct, } @@ -36,7 +45,7 @@ pub struct PollBase { pub options: Vec, } -pack!(PackPollBase, Id as id & PollBase as poll); +pack!(PackPollBase, Required as id & Required as poll); #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] @@ -58,7 +67,7 @@ pub struct NoteBase { pub local_only: bool, } -pack!(PackNoteBase, Id as id & NoteBase as note); +pack!(PackNoteBase, Required as id & Required as note); #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] @@ -69,19 +78,19 @@ pub struct NoteAttachmentExt { pack!( PackNoteWithAttachments, - Id as id & NoteBase as note & NoteAttachmentExt as attachment + Required as id & Required as note & Required as attachment ); #[derive(Clone, Debug, Deserialize, Serialize, TS)] pub struct NoteDetailExt { - pub poll: Option, + pub poll: Option, pub parent_note: Option>, pub renoted_note: Option>, } pack!( PackNoteFull, - Id as id & NoteBase as note & NoteAttachmentExt as attachment & NoteDetailExt as detail + Required as id & Required as note & Required as attachment & Required as detail ); #[derive(Clone, Debug, Deserialize, Serialize, TS)] @@ -90,7 +99,7 @@ pub struct ReactionBase { pub user_id: String, } -pack!(PackReactionBase, Id as id & ReactionBase as reaction); +pack!(PackReactionBase, Required as id & Required as reaction); #[derive(Clone, Debug, Deserialize, Serialize, TS)] pub struct ReactionUserExt { @@ -99,5 +108,5 @@ pub struct ReactionUserExt { pack!( PackReactionWithUser, - Id as id & ReactionBase as reaction & ReactionUserExt as user + Required as id & Required as reaction & Required as user ); diff --git a/magnetar_sdk/src/types/timeline.rs b/magnetar_sdk/src/types/timeline.rs new file mode 100644 index 0000000..db653dd --- /dev/null +++ b/magnetar_sdk/src/types/timeline.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS)] +pub enum TimelineType { + Home, + Timeline, + Recommended, + Hybrid, + Global, +} diff --git a/magnetar_sdk/src/types/user.rs b/magnetar_sdk/src/types/user.rs index 676a76f..f827e64 100644 --- a/magnetar_sdk/src/types/user.rs +++ b/magnetar_sdk/src/types/user.rs @@ -1,22 +1,26 @@ use crate::types::emoji::EmojiContext; -use crate::types::note::PackNoteBase; use crate::types::{Id, NotificationSettings}; +use crate::{Packed, Required}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use ts_rs::TS; use magnetar_sdk_macros::pack; -#[derive(Clone, Debug, Deserialize, Serialize, TS)] +use super::note::PackNoteWithAttachments; + +#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)] #[ts(export)] pub enum AvatarDecoration { + #[default] None, CatEars, } -#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)] #[ts(export)] pub enum SpeechTransform { + #[default] None, Cat, } @@ -42,29 +46,29 @@ pub struct UserBase { pub avatar_blurhash: Option, pub avatar_color: Option, pub avatar_decoration: AvatarDecoration, - pub admin: bool, - pub moderator: bool, - pub bot: bool, + pub is_admin: bool, + pub is_moderator: bool, + pub is_bot: bool, pub emojis: EmojiContext, } -pack!(PackUserBase, Id as id & UserBase as user); +pack!(PackUserBase, Required as id & Required as user); #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] pub struct UserProfileExt { - pub locked: bool, - pub silenced: bool, - pub suspended: bool, + pub is_locked: bool, + pub is_silenced: bool, + pub is_suspended: bool, pub description: Option, pub location: Option, pub birthday: Option>, pub fields: Vec, - pub follower_count: u64, - pub following_count: u64, - pub note_count: u64, + pub follower_count: Option, + pub following_count: Option, + pub note_count: Option, pub url: Option, @@ -75,34 +79,19 @@ pub struct UserProfileExt { pub banner_color: Option, pub banner_blurhash: Option, - pub public_reactions: bool, + pub has_public_reactions: bool, } -pack!( - PackUserProfile, - Id as id & UserBase as user & UserProfileExt as profile -); - #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] pub struct UserProfilePinsEx { - pub pinned_notes: Vec, + pub pinned_notes: Vec, // pub pinned_page: Option, } -pack!( - PackUserProfileFull, - Id as id - & UserBase as user - & UserProfileExt as profile - & UserProfilePinsEx as pins - & UserRelationExt as relation -); - #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] pub struct UserRelationExt { - pub last_fetched_at: Option>, pub follows_you: bool, pub you_follow: bool, pub blocks_you: bool, @@ -111,28 +100,14 @@ pub struct UserRelationExt { pub mute_renotes: bool, } -pack!( - PackUserRelation, - Id as id & UserBase as user & UserRelationExt as relation -); -pack!( - PackUserProfileRelation, - Id as id & UserBase as user & UserProfileExt as profile & UserRelationExt as relation -); - #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] pub struct UserAuthOverviewExt { - pub two_factor_enabled: bool, - pub use_passwordless_login: bool, - pub security_keys: bool, + pub has_two_factor_enabled: bool, + pub has_passwordless_login: bool, + pub has_security_keys: bool, } -pack!( - PackUserAuthOverview, - Id as id & UserBase as user & UserAuthOverviewExt as auth -); - #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] pub struct UserSelfExt { @@ -154,12 +129,13 @@ pub struct UserSelfExt { pack!( PackUserSelf, - Id as id & UserBase as user & UserSelfExt as self_info + Required as id & Required as user & Required as self_info ); #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] pub struct UserDetailExt { + pub last_fetched_at: Option>, pub uri: Option, pub updated_at: Option>, } @@ -168,20 +144,36 @@ pub struct UserDetailExt { #[ts(export)] pub struct SecurityKeyBase { pub name: String, - pub created_at: DateTime, pub last_used_at: Option>, } -pack!(PackSecurityKeyBase, Id as id & SecurityKeyBase as key); +pack!(PackSecurityKeyBase, Required as id & Required as key); #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] pub struct UserSecretsExt { pub email: Option, pub email_verified: bool, - pub security_keys: Vec, + pub security_keys: Vec, } + pack!( - PackUserSelfAll, - Id as id & UserBase as user & UserSelfExt as self_info & UserSecretsExt as secrets + PackUserMaybeAll, + Required as id + & Required as user + & Option as profile + & Option as pins + & Option as detail + & Option as relation + & Option as auth +); + +pack!( + PackUserSelfMaybeAll, + Required as id + & Required as user + & Option as profile + & Option as pins + & Option as detail + & Option as secrets ); diff --git a/magnetar_sdk/src/util_types.rs b/magnetar_sdk/src/util_types.rs new file mode 100644 index 0000000..220c207 --- /dev/null +++ b/magnetar_sdk/src/util_types.rs @@ -0,0 +1,78 @@ +use serde::Serialize; +use ts_rs::TS; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct U64Range(u64); + +impl TS for U64Range { + const EXPORT_TO: Option<&'static str> = Some("bindings/util/u64_range.ts"); + + fn decl() -> String { + ::decl() + } + + fn name() -> String { + ::name() + } + + fn dependencies() -> Vec { + vec![ts_rs::Dependency::from_ty::().unwrap()] + } + + fn transparent() -> bool { + true + } +} + +impl Serialize for U64Range { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_u64(self.0) + } +} + +impl<'de, const MIN: u64, const MAX: u64> serde::Deserialize<'de> for U64Range { + fn deserialize>(deserializer: D) -> Result { + let v = u64::deserialize(deserializer)?; + if v < MIN || v > MAX { + return Err(serde::de::Error::custom("out of range")); + } + + Ok(U64Range(v)) + } +} + +impl std::ops::Deref for U64Range { + type Target = u64; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From> for u64 { + fn from(v: U64Range) -> Self { + v.0 + } +} + +impl std::fmt::Display for U64Range { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct OutOfRange; + +impl TryFrom for U64Range { + type Error = OutOfRange; + + fn try_from(value: u64) -> Result { + if value < MIN || value > MAX { + return Err(OutOfRange); + } + + Ok(U64Range(value)) + } +}