278 lines
8.9 KiB
Rust
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,
|
|
})
|
|
}
|