Replaced the WASM in SDK with a more sensible ts_rs setup

This commit is contained in:
Natty 2023-09-22 20:10:48 +02:00
parent 30e3da71e9
commit 7bffc5f16a
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
19 changed files with 295 additions and 812 deletions

235
Cargo.lock generated
View File

@ -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"

View File

@ -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"] }

View File

@ -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"] }

View File

@ -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<Dependency> {
fn dependencies() -> Vec<ts_rs::Dependency> {
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(),
]
}

View File

@ -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<O, ResponseError>
where
I: Serialize + DeserializeOwned + Send + 'static,
O: Serialize + DeserializeOwned + Send + 'static,
E: Endpoint<Request = I, Response = O> + 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::<ResponseError>(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::<O>(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<ApiClient> {
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<I: Serialize, E: Endpoint, O: DeserializeOwned>(
&self,
endpoint: &E,
data: &I,
path_params: &[(&str, &str)],
) -> Result<O, ResponseError>
where
I: Serialize + DeserializeOwned + Send + 'static,
O: Serialize + DeserializeOwned + Send + 'static,
E: Endpoint<Request = I, Response = O> + Send + 'static,
{
match &self.client {
ApiClientImpl::Fetch(client) => {
client.call::<I, O, E>(endpoint, data, &path_params).await
}
#[cfg(feature = "reqwest")]
ApiClientImpl::Reqwest(client) => {
client.call::<I, O, E>(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<JsValue, ResponseError> {
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::<HashMap<String, String>>(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::<Vec<(&str, &str)>>();
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,
})
}

View File

@ -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),
}

View File

@ -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<O, ResponseError>
where
I: Serialize + DeserializeOwned + Send + 'static,
O: Serialize + DeserializeOwned + Send + 'static,
E: Endpoint<Request = I, Response = O> + 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::<ResponseError>().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::<O>().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()),
})
}
}
}

View File

@ -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<impl Endpoint> {
match_from!(
(endpoint, method)
in [
crate::endpoints::user::GetUserById,
]
)
}

View File

@ -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<Self::Response> {
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<T> = <T as Endpoint>::Request;
pub type Res<T> = <T as Endpoint>::Response;

View File

@ -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<NoteListFilter>,
}
fn default_timeline_limit<const MIN: u64, const MAX: u64>() -> U64Range<MIN, MAX> {
15.try_into().unwrap()
}
#[derive(Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(transparent)]
pub struct GetTimelineRes(pub Vec<PackNoteFull>);
#[derive(Endpoint)]
#[endpoint(
endpoint = "/timeline",
method = Method::GET,
request = GetTimelineReq,
response = Vec::<PackNoteFull>,
)]
pub struct GetTimeline;

View File

@ -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<String>,
#[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;

View File

@ -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<T>(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<O, ResponseError>
where
I: Serialize + DeserializeOwned + Send + 'static,
O: Serialize + DeserializeOwned + Send + 'static,
E: Endpoint<Request = I, Response = O> + Send + 'static;
fn pack_from(val: Self::Input) -> Self;
}

View File

@ -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<Id> as id & Required<DriveFolderBase> 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<Id> as id & Required<DriveFolderBase> as folder & Required<DriveFolderParentExt> as parent
);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
@ -56,10 +57,10 @@ pub struct DriveFileBase {
pub sensitive: bool,
pub comment: Option<String>,
pub folder_id: Option<String>,
pub user_id: String,
pub user_id: Option<String>,
}
pack!(PackDriveFileBase, Id as id & DriveFileBase as file);
pack!(PackDriveFileBase, Required<Id> as id & Required<DriveFileBase> 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<Id> as id & Required<DriveFileBase> as file & Required<DriveFileFolderExt> 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<Id> as id & Required<DriveFileBase> as file & Required<DriveFileUserExt> as user
);
pack!(
PackDriveFileFull,
Id as id & DriveFileBase as file & DriveFileFolderExt as folder & DriveFileUserExt as user
Required<Id> as id & Required<DriveFileBase> as file & Required<DriveFileFolderExt> as folder & Required<DriveFileUserExt> as user
);

View File

@ -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<String>,
pub width: Option<i32>,
pub height: Option<i32>,
}
pack!(PackEmojiBase, Id as id & EmojiBase as emoji);
pack!(PackEmojiBase, Required<Id> as id & Required<EmojiBase> as emoji);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
#[repr(transparent)]
pub struct EmojiContext(pub Vec<String>);
pub struct EmojiContext(pub Vec<PackEmojiBase>);

View File

@ -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<DateTime<Utc>>),
TimeStart(DateTime<Utc>),
TimeEnd(DateTime<Utc>),
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
#[repr(transparent)]

View File

@ -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<bool>,
show_replies: Option<bool>,
show_files_only: Option<bool>,
uncwed_sensitive: Option<bool>,
}
#[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<PollChoice>,
}
pack!(PackPollBase, Id as id & PollBase as poll);
pack!(PackPollBase, Required<Id> as id & Required<PollBase> 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<Id> as id & Required<NoteBase> 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<Id> as id & Required<NoteBase> as note & Required<NoteAttachmentExt> as attachment
);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
pub struct NoteDetailExt {
pub poll: Option<PollBase>,
pub poll: Option<PackPollBase>,
pub parent_note: Option<Box<NoteBase>>,
pub renoted_note: Option<Box<NoteBase>>,
}
pack!(
PackNoteFull,
Id as id & NoteBase as note & NoteAttachmentExt as attachment & NoteDetailExt as detail
Required<Id> as id & Required<NoteBase> as note & Required<NoteAttachmentExt> as attachment & Required<NoteDetailExt> 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<Id> as id & Required<ReactionBase> 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<Id> as id & Required<ReactionBase> as reaction & Required<ReactionUserExt> as user
);

View File

@ -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,
}

View File

@ -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<String>,
pub avatar_color: Option<String>,
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<Id> as id & Required<UserBase> 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<String>,
pub location: Option<String>,
pub birthday: Option<DateTime<Utc>>,
pub fields: Vec<ProfileField>,
pub follower_count: u64,
pub following_count: u64,
pub note_count: u64,
pub follower_count: Option<u64>,
pub following_count: Option<u64>,
pub note_count: Option<u64>,
pub url: Option<String>,
@ -75,34 +79,19 @@ pub struct UserProfileExt {
pub banner_color: Option<String>,
pub banner_blurhash: Option<String>,
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<PackNoteBase>,
pub pinned_notes: Vec<PackNoteWithAttachments>,
// pub pinned_page: Option<Page>,
}
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<DateTime<Utc>>,
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<Id> as id & Required<UserBase> as user & Required<UserSelfExt> as self_info
);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct UserDetailExt {
pub last_fetched_at: Option<DateTime<Utc>>,
pub uri: Option<String>,
pub updated_at: Option<DateTime<Utc>>,
}
@ -168,20 +144,36 @@ pub struct UserDetailExt {
#[ts(export)]
pub struct SecurityKeyBase {
pub name: String,
pub created_at: DateTime<Utc>,
pub last_used_at: Option<DateTime<Utc>>,
}
pack!(PackSecurityKeyBase, Id as id & SecurityKeyBase as key);
pack!(PackSecurityKeyBase, Required<Id> as id & Required<SecurityKeyBase> as key);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct UserSecretsExt {
pub email: Option<String>,
pub email_verified: bool,
pub security_keys: Vec<SecurityKeyBase>,
pub security_keys: Vec<PackSecurityKeyBase>,
}
pack!(
PackUserSelfAll,
Id as id & UserBase as user & UserSelfExt as self_info & UserSecretsExt as secrets
PackUserMaybeAll,
Required<Id> as id
& Required<UserBase> as user
& Option<UserProfileExt> as profile
& Option<UserProfilePinsEx> as pins
& Option<UserDetailExt> as detail
& Option<UserRelationExt> as relation
& Option<UserAuthOverviewExt> as auth
);
pack!(
PackUserSelfMaybeAll,
Required<Id> as id
& Required<UserBase> as user
& Option<UserProfileExt> as profile
& Option<UserProfilePinsEx> as pins
& Option<UserDetailExt> as detail
& Option<UserSecretsExt> as secrets
);

View File

@ -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<const MIN: u64, const MAX: u64>(u64);
impl<const MIN: u64, const MAX: u64> TS for U64Range<MIN, MAX> {
const EXPORT_TO: Option<&'static str> = Some("bindings/util/u64_range.ts");
fn decl() -> String {
<u64 as TS>::decl()
}
fn name() -> String {
<u64 as TS>::name()
}
fn dependencies() -> Vec<ts_rs::Dependency> {
vec![ts_rs::Dependency::from_ty::<u64>().unwrap()]
}
fn transparent() -> bool {
true
}
}
impl<const MIN: u64, const MAX: u64> Serialize for U64Range<MIN, MAX> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_u64(self.0)
}
}
impl<'de, const MIN: u64, const MAX: u64> serde::Deserialize<'de> for U64Range<MIN, MAX> {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let v = u64::deserialize(deserializer)?;
if v < MIN || v > MAX {
return Err(serde::de::Error::custom("out of range"));
}
Ok(U64Range(v))
}
}
impl<const MIN: u64, const MAX: u64> std::ops::Deref for U64Range<MIN, MAX> {
type Target = u64;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<const MIN: u64, const MAX: u64> From<U64Range<MIN, MAX>> for u64 {
fn from(v: U64Range<MIN, MAX>) -> Self {
v.0
}
}
impl<const MIN: u64, const MAX: u64> std::fmt::Display for U64Range<MIN, MAX> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Clone, Copy)]
pub struct OutOfRange;
impl<const MIN: u64, const MAX: u64> TryFrom<u64> for U64Range<MIN, MAX> {
type Error = OutOfRange;
fn try_from(value: u64) -> Result<Self, Self::Error> {
if value < MIN || value > MAX {
return Err(OutOfRange);
}
Ok(U64Range(value))
}
}