magnetar/magnetar_sdk/src/client/fetch.rs

278 lines
8.9 KiB
Rust

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