use super::{into_request_config::IntoRequestConfig, Action};
use crate::{error::BoxError, web::IntoJsonResponse};
use http::{HeaderMap, HeaderName, HeaderValue, Method};
use std::{fmt::Debug, marker::PhantomData, ops::Deref, rc::Rc};
use web_sys::AbortSignal;
use yew::{hook, use_state, Callback, UseStateHandle};
#[derive(Debug, Clone)]
pub struct RequestOptions {
pub headers: HeaderMap,
pub method: Method,
}
impl RequestOptions {
pub fn new() -> Self {
RequestOptions {
headers: HeaderMap::new(),
method: Method::POST,
}
}
pub fn header<K, V>(mut self, key: K, value: V) -> Self
where
HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<BoxError>,
HeaderValue: TryFrom<V>,
<HeaderValue as TryFrom<V>>::Error: Into<BoxError>,
{
let name = <HeaderName as TryFrom<K>>::try_from(key)
.map_err(Into::into)
.expect("invalid header name");
let value = <HeaderValue as TryFrom<V>>::try_from(value)
.map_err(Into::into)
.expect("invalid header value");
self.headers.insert(name, value);
self
}
pub fn try_header<K, V>(mut self, key: K, value: V) -> Result<Self, BoxError>
where
HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<BoxError>,
HeaderValue: TryFrom<V>,
<HeaderValue as TryFrom<V>>::Error: Into<BoxError>,
{
let name = <HeaderName as TryFrom<K>>::try_from(key).map_err(Into::into)?;
let value = <HeaderValue as TryFrom<V>>::try_from(value).map_err(Into::into)?;
self.headers.insert(name, value);
Ok(self)
}
pub fn method(mut self, method: Method) -> Self {
self.method = method;
self
}
}
impl Default for RequestOptions {
fn default() -> Self {
RequestOptions::new()
}
}
#[allow(type_alias_bounds)]
type ActionData<A: Action> = <A::Response as IntoJsonResponse>::Data;
#[allow(type_alias_bounds)]
type ActionResult<A: Action> = crate::Result<ActionData<A>>;
pub struct UseActionRef<A: Action>(Rc<ActionResult<A>>);
impl<A: Action> Deref for UseActionRef<A> {
type Target = ActionResult<A>;
fn deref(&self) -> &Self::Target {
self.0.as_ref()
}
}
pub struct UseActionOptions<A: Action> {
on_complete: Option<Callback<UseActionRef<A>>>,
signal: Option<AbortSignal>,
}
impl<A: Action> UseActionOptions<A> {
pub fn new() -> Self {
Default::default()
}
pub fn on_complete<F>(mut self, f: F) -> Self
where
F: Fn(UseActionRef<A>) + 'static,
{
self.on_complete = Some(Callback::from(f));
self
}
pub fn signal(mut self, signal: AbortSignal) -> Self {
self.signal = Some(signal);
self
}
}
impl<A: Action> Debug for UseActionOptions<A> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UseActionOptions")
.field("on_complete", &self.on_complete)
.field("signal", &self.signal)
.finish()
}
}
impl<A: Action> Clone for UseActionOptions<A> {
fn clone(&self) -> Self {
Self {
on_complete: self.on_complete.clone(),
signal: self.signal.clone(),
}
}
}
impl<A: Action> Default for UseActionOptions<A> {
fn default() -> Self {
Self {
on_complete: Default::default(),
signal: Default::default(),
}
}
}
pub struct UseActionHandle<A, T>
where
A: Action,
{
loading: UseStateHandle<bool>,
result: UseStateHandle<Option<Rc<ActionResult<A>>>>,
options: UseActionOptions<A>,
_marker: PhantomData<T>,
}
impl<A, T> UseActionHandle<A, T>
where
A: Action,
T: IntoRequestConfig,
{
pub fn is_loading(&self) -> bool {
*self.loading
}
pub fn is_error(&self) -> bool {
match &*self.result {
Some(x) => x.is_err(),
None => false,
}
}
pub fn data(&self) -> Option<&<A::Response as IntoJsonResponse>::Data> {
self.result.as_deref().and_then(|x| x.as_ref().ok())
}
pub fn error(&self) -> Option<&BoxError> {
self.result.as_deref().and_then(|x| x.as_ref().err())
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(unused_variables)]
pub fn send(&self, obj: T) -> Result<(), BoxError> {
unreachable!("client only function")
}
#[cfg(target_arch = "wasm32")]
#[allow(unused_variables)]
pub fn send(&self, obj: T) -> Result<(), BoxError> {
self.send_with_options(obj, RequestOptions::new())
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(unused_variables)]
pub fn send_with_options(&self, obj: T, options: RequestOptions) -> Result<(), BoxError> {
unreachable!("client only function")
}
#[cfg(target_arch = "wasm32")]
pub fn send_with_options(&self, obj: T, options: RequestOptions) -> Result<(), BoxError> {
use crate::actions::into_request_config::RequestInitConfig;
use crate::utils::wasm::fetch_json;
use crate::error::JsError;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{Headers, RequestInit};
struct OnDrop<F: FnOnce()>(Option<F>);
impl<F: FnOnce()> Drop for OnDrop<F> {
fn drop(&mut self) {
if let Some(f) = self.0.take() {
f();
}
}
}
let loading = self.loading.clone();
loading.set(true);
let _guard = OnDrop(Some(move || loading.set(false)));
let result = self.result.clone();
let request_config = obj.into_request_config(&options)?;
let RequestInitConfig {
init,
search_params,
} = request_config;
let mut init = init.unwrap_or_else(|| RequestInit::new());
let headers = match js_sys::Reflect::get(&init, &JsValue::from("headers")) {
Ok(x) => {
if x.is_falsy() {
Headers::new().map_err(JsError::new)?
} else {
match x.dyn_into::<Headers>() {
Ok(headers) => headers,
Err(err) => {
log::debug!("failed to cast property `headers` to Headers type: {err:?}");
Headers::new().map_err(JsError::new)?
},
}
}
}
Err(err) => {
log::debug!("failed to get `RequestInit::headers`: {err:?}");
Headers::new().map_err(JsError::new)?
}
};
let mut last_name = None;
for (name, value) in options.headers {
if let Some(name) = name {
last_name = Some(name);
}
let key = last_name.as_ref().unwrap();
headers
.append(key.as_str(), value.to_str()?)
.map_err(JsError::new)?;
}
init.headers(&headers);
init.method(options.method.as_str());
init.signal(self.options.signal.as_ref());
let mut url = A::route().to_owned();
if let Some(search_params) = search_params {
url.push_str(&format!("?{search}", search = search_params.to_string()));
}
let request = web_sys::Request::new_with_str_and_init(&url, &init).map_err(JsError::new)?;
let on_complete = self.options.on_complete.clone();
wasm_bindgen_futures::spawn_local(async move {
let _guard = _guard;
let ret = Rc::new(fetch_json(request).await);
if let Some(on_complete) = on_complete {
on_complete.emit(UseActionRef(ret.clone()));
}
result.set(Some(ret));
});
Ok(())
}
}
impl<A, T> Clone for UseActionHandle<A, T>
where
A: Action,
{
fn clone(&self) -> Self {
Self {
loading: self.loading.clone(),
result: self.result.clone(),
options: self.options.clone(),
_marker: self._marker,
}
}
}
impl<A, T> PartialEq for UseActionHandle<A, T>
where
A: Action,
{
fn eq(&self, other: &Self) -> bool {
self.loading == other.loading && std::ptr::eq(&*self.result, &*other.result)
}
}
impl<A, T> Debug for UseActionHandle<A, T>
where
A: Action,
<A::Response as IntoJsonResponse>::Data: Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UseActionHandle")
.field("loading", &self.loading)
.field("result", &self.result)
.field("options", &self.options)
.finish()
}
}
#[hook]
pub fn use_action<A, T>() -> UseActionHandle<A, T>
where
A: Action,
T: IntoRequestConfig,
{
use_action_with_options(Default::default())
}
#[hook]
pub fn use_action_with_callback<'a, A, T, F>(on_complete: F) -> UseActionHandle<A, T>
where
A: Action,
T: IntoRequestConfig,
F: Fn(&crate::Result<<A::Response as IntoJsonResponse>::Data>) + 'static,
{
let options = UseActionOptions::new().on_complete(move |ret| {
on_complete(&*ret);
});
use_action_with_options(options)
}
#[hook]
pub fn use_action_with_options<A, T>(options: UseActionOptions<A>) -> UseActionHandle<A, T>
where
A: Action,
T: IntoRequestConfig,
{
let result = use_state(|| None);
let loading = use_state(|| false);
UseActionHandle {
result,
loading,
options,
_marker: PhantomData,
}
}