mirror of
https://github.com/Zenithsiz/dynatos.git
synced 2026-02-03 18:13:04 +00:00
QuerySignal is now controlled by a query type.
Added query types `SingleQuery` and `MultiQuery`. Removed `QueryArraySignal`.
This commit is contained in:
parent
8137a555f8
commit
08fc0bc9e5
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -10,6 +10,7 @@
|
||||
"popstate",
|
||||
"qself",
|
||||
"scopeguard",
|
||||
"thiserror",
|
||||
"unsize",
|
||||
"zutil"
|
||||
],
|
||||
|
||||
21
Cargo.lock
generated
21
Cargo.lock
generated
@ -282,6 +282,7 @@ dependencies = [
|
||||
"dynatos-util",
|
||||
"extend",
|
||||
"js-sys",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
@ -919,6 +920,26 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.7.6"
|
||||
|
||||
@ -54,6 +54,7 @@ proc-macro2 = "1.0.94"
|
||||
quote = "1.0.40"
|
||||
scopeguard = "1.2.0"
|
||||
syn = "2.0.100"
|
||||
thiserror = "2.0.12"
|
||||
tokio = "1.44.2"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.19"
|
||||
|
||||
@ -17,6 +17,7 @@ anyhow = { workspace = true }
|
||||
duplicate = { workspace = true }
|
||||
extend = { workspace = true }
|
||||
js-sys = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
url = { workspace = true }
|
||||
wasm-bindgen = { workspace = true }
|
||||
|
||||
@ -1,13 +1,23 @@
|
||||
//! Routing for `dynatos`
|
||||
|
||||
// Features
|
||||
#![feature(proc_macro_hygiene, stmt_expr_attributes, let_chains, negative_impls)]
|
||||
#![feature(
|
||||
proc_macro_hygiene,
|
||||
stmt_expr_attributes,
|
||||
let_chains,
|
||||
negative_impls,
|
||||
type_alias_impl_trait,
|
||||
trait_alias
|
||||
)]
|
||||
|
||||
// Modules
|
||||
mod anchor;
|
||||
pub mod location;
|
||||
pub mod query_array_signal;
|
||||
pub mod query_signal;
|
||||
|
||||
// Exports
|
||||
pub use self::{anchor::anchor, location::Location, query_array_signal::QueryArraySignal, query_signal::QuerySignal};
|
||||
pub use self::{
|
||||
anchor::anchor,
|
||||
location::Location,
|
||||
query_signal::{MultiQuery, QuerySignal, SingleQuery},
|
||||
};
|
||||
|
||||
@ -1,248 +0,0 @@
|
||||
//! Query array signal
|
||||
|
||||
// TODO: Should we also return a `Loadable`?
|
||||
// Do we even need to exist? Could `QuerySignal`
|
||||
// just special case values of `Vec<T>` somehow?
|
||||
|
||||
// Imports
|
||||
use {
|
||||
crate::Location,
|
||||
core::{
|
||||
error::Error as StdError,
|
||||
mem,
|
||||
ops::{Deref, DerefMut},
|
||||
str::FromStr,
|
||||
},
|
||||
dynatos_reactive::{signal, Effect, Memo, Signal, SignalBorrow, SignalBorrowMut, SignalReplace, SignalSet},
|
||||
std::rc::Rc,
|
||||
zutil_cloned::cloned,
|
||||
};
|
||||
|
||||
/// Query signal
|
||||
#[derive(Debug)]
|
||||
pub struct QueryArraySignal<T> {
|
||||
/// Key
|
||||
key: Rc<str>,
|
||||
|
||||
/// Inner value
|
||||
inner: Signal<Vec<T>>,
|
||||
|
||||
/// Update effect.
|
||||
update_effect: Effect<dyn Fn()>,
|
||||
}
|
||||
|
||||
impl<T> QueryArraySignal<T> {
|
||||
/// Creates a new query signal for `key`.
|
||||
///
|
||||
/// Expects a context of type [`Location`](crate::Location).
|
||||
#[track_caller]
|
||||
pub fn new<K>(key: K) -> Self
|
||||
where
|
||||
T: FromStr + 'static,
|
||||
T::Err: StdError + Send + Sync + 'static,
|
||||
K: Into<Rc<str>>,
|
||||
{
|
||||
// Get the query values
|
||||
let key = key.into();
|
||||
let query_values = Memo::new({
|
||||
let key = Rc::clone(&key);
|
||||
move || {
|
||||
dynatos_context::with_expect::<Location, _, _>(|location| {
|
||||
location
|
||||
.borrow()
|
||||
.query_pairs()
|
||||
.filter_map(|(query, value)| (query == *key).then_some(value.into_owned()))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
let inner = Signal::new(vec![]);
|
||||
#[cloned(inner, key)]
|
||||
let update = Effect::new(move || {
|
||||
let values = query_values
|
||||
.borrow()
|
||||
.iter()
|
||||
.filter_map(|query_value| match query_value.parse::<T>() {
|
||||
Ok(value) => Some(value),
|
||||
Err(err) => {
|
||||
tracing::warn!(?key, value=?query_value, ?err, "Unable to parse query");
|
||||
None
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Then set it
|
||||
inner.set(values);
|
||||
});
|
||||
|
||||
Self {
|
||||
key,
|
||||
inner,
|
||||
update_effect: update,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for QueryArraySignal<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
key: Rc::clone(&self.key),
|
||||
inner: self.inner.clone(),
|
||||
update_effect: self.update_effect.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reference type for [`SignalBorrow`] impl
|
||||
#[derive(Debug)]
|
||||
pub struct BorrowRef<'a, T>(signal::BorrowRef<'a, Vec<T>>);
|
||||
|
||||
impl<T> Deref for BorrowRef<'_, T> {
|
||||
type Target = [T];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.0.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> SignalBorrow for QueryArraySignal<T> {
|
||||
type Ref<'a>
|
||||
= BorrowRef<'a, T>
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
#[track_caller]
|
||||
fn borrow(&self) -> Self::Ref<'_> {
|
||||
BorrowRef(self.inner.borrow())
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn borrow_raw(&self) -> Self::Ref<'_> {
|
||||
BorrowRef(self.inner.borrow_raw())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SignalReplace<Vec<T>> for QueryArraySignal<T>
|
||||
where
|
||||
T: ToString + 'static,
|
||||
{
|
||||
type Value = Vec<T>;
|
||||
|
||||
#[track_caller]
|
||||
fn replace(&self, new_value: Vec<T>) -> Self::Value {
|
||||
mem::replace(&mut self.borrow_mut(), new_value)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn replace_raw(&self, new_value: Vec<T>) -> Self::Value {
|
||||
mem::replace(&mut self.borrow_mut_raw(), new_value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the location on `Drop`
|
||||
// Note: We need this wrapper because `BorrowRefMut::value` must
|
||||
// already be dropped when we update the location, which we
|
||||
// can't do if we implement `Drop` on `BorrowRefMut`.
|
||||
#[derive(Debug)]
|
||||
struct UpdateLocationOnDrop<'a, T: ToString + 'static>(pub &'a QueryArraySignal<T>);
|
||||
|
||||
impl<T> Drop for UpdateLocationOnDrop<'_, T>
|
||||
where
|
||||
T: ToString + 'static,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
// Update the location
|
||||
// Note: We suppress the update, given that it won't change anything,
|
||||
// as we already have the latest value.
|
||||
// TODO: Force an update anyway just to ensure some consistency with `FromStr` + `ToString`?
|
||||
self.0.update_effect.suppressed(|| {
|
||||
dynatos_context::with_expect::<Location, _, _>(|location| {
|
||||
let mut location = location.borrow_mut();
|
||||
let mut queries = location
|
||||
.query_pairs()
|
||||
.into_owned()
|
||||
.filter(|(key, _)| *key != *self.0.key)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Note: We can't use a normal `borrow`, because that'd add us as a dependency to any
|
||||
// running effects, but that might cause loops since updating the location would
|
||||
// update us as well.
|
||||
for value in &*self.0.inner.borrow_raw() {
|
||||
queries.push(((*self.0.key).to_owned(), value.to_string()));
|
||||
}
|
||||
|
||||
location.query_pairs_mut().clear().extend_pairs(queries);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Reference type for [`SignalBorrowMut`] impl
|
||||
#[derive(Debug)]
|
||||
pub struct BorrowRefMut<'a, T>
|
||||
where
|
||||
T: ToString + 'static,
|
||||
{
|
||||
/// Value
|
||||
value: signal::BorrowRefMut<'a, Vec<T>>,
|
||||
|
||||
/// Update location on drop
|
||||
// Note: Must be dropped *after* `value`.
|
||||
_update_location_on_drop: Option<UpdateLocationOnDrop<'a, T>>,
|
||||
}
|
||||
|
||||
impl<T> Deref for BorrowRefMut<'_, T>
|
||||
where
|
||||
T: ToString,
|
||||
{
|
||||
type Target = Vec<T>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for BorrowRefMut<'_, T>
|
||||
where
|
||||
T: ToString,
|
||||
{
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl<T> SignalBorrowMut for QueryArraySignal<T>
|
||||
where
|
||||
T: ToString + 'static,
|
||||
{
|
||||
type RefMut<'a>
|
||||
= BorrowRefMut<'a, T>
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
#[track_caller]
|
||||
fn borrow_mut(&self) -> Self::RefMut<'_> {
|
||||
let value = self.inner.borrow_mut();
|
||||
BorrowRefMut {
|
||||
value,
|
||||
_update_location_on_drop: Some(UpdateLocationOnDrop(self)),
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn borrow_mut_raw(&self) -> Self::RefMut<'_> {
|
||||
// TODO: Should we be updating the location on drop?
|
||||
|
||||
let value = self.inner.borrow_mut_raw();
|
||||
BorrowRefMut {
|
||||
value,
|
||||
_update_location_on_drop: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> signal::SignalSetDefaultImpl for QueryArraySignal<T> {}
|
||||
impl<T> signal::SignalWithDefaultImpl for QueryArraySignal<T> {}
|
||||
impl<T> signal::SignalUpdateDefaultImpl for QueryArraySignal<T> {}
|
||||
@ -1,5 +1,12 @@
|
||||
//! Query signal
|
||||
|
||||
// Modules
|
||||
pub mod multi_query;
|
||||
pub mod single_query;
|
||||
|
||||
// Exports
|
||||
pub use self::{multi_query::MultiQuery, single_query::SingleQuery};
|
||||
|
||||
// Imports
|
||||
use {
|
||||
crate::Location,
|
||||
@ -7,90 +14,67 @@ use {
|
||||
fmt,
|
||||
mem,
|
||||
ops::{Deref, DerefMut},
|
||||
str::FromStr,
|
||||
},
|
||||
dynatos_loadable::Loadable,
|
||||
dynatos_reactive::{signal, Effect, Memo, Signal, SignalBorrow, SignalBorrowMut, SignalReplace, SignalSet},
|
||||
std::rc::Rc,
|
||||
zutil_cloned::cloned,
|
||||
};
|
||||
|
||||
/// Query signal
|
||||
pub struct QuerySignal<T, E = <T as FromStr>::Err> {
|
||||
/// Key
|
||||
key: Rc<str>,
|
||||
pub struct QuerySignal<T: QueryParse> {
|
||||
/// Query
|
||||
query: T,
|
||||
|
||||
/// Inner value
|
||||
inner: Signal<Loadable<T, E>>,
|
||||
inner: Signal<Option<T::Value>>,
|
||||
|
||||
/// Update effect.
|
||||
update_effect: Effect<dyn Fn()>,
|
||||
}
|
||||
|
||||
impl<T, E> QuerySignal<T, E> {
|
||||
/// Creates a new query signal for `key`.
|
||||
///
|
||||
/// Expects a context of type [`Location`](crate::Location).
|
||||
impl<T: QueryParse> QuerySignal<T> {
|
||||
/// Creates a new query signal with `query`.
|
||||
#[track_caller]
|
||||
pub fn new<K>(key: K) -> Self
|
||||
pub fn new(query: T) -> Self
|
||||
where
|
||||
T: FromStr + 'static,
|
||||
E: From<T::Err> + 'static,
|
||||
K: Into<Rc<str>>,
|
||||
// TODO: Remove this clone call by storing it inside
|
||||
// somewhere shared.
|
||||
T: QueryParse + Clone + 'static,
|
||||
T::Value: 'static,
|
||||
{
|
||||
// Get the query value
|
||||
let key = key.into();
|
||||
#[cloned(key)]
|
||||
let query_value = Memo::new(move || {
|
||||
dynatos_context::with_expect::<Location, _, _>(|location| {
|
||||
location
|
||||
.borrow()
|
||||
.query_pairs()
|
||||
.find_map(|(query, value)| (query == *key).then_some(value.into_owned()))
|
||||
})
|
||||
});
|
||||
|
||||
let inner = Signal::new(Loadable::Empty);
|
||||
#[cloned(inner)]
|
||||
let inner = Signal::new(None);
|
||||
#[cloned(query, inner)]
|
||||
let update = Effect::new(move || {
|
||||
let value = match query_value.borrow().as_ref() {
|
||||
Some(value) => match value.parse::<T>() {
|
||||
Ok(value) => Loadable::Loaded(value),
|
||||
Err(err) => Loadable::Err(err.into()),
|
||||
},
|
||||
None => Loadable::Empty,
|
||||
};
|
||||
|
||||
// Then set it
|
||||
let value = query.parse();
|
||||
inner.set(value);
|
||||
});
|
||||
|
||||
Self {
|
||||
key,
|
||||
query,
|
||||
inner,
|
||||
update_effect: update,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> Clone for QuerySignal<T, E> {
|
||||
impl<T: QueryParse + Clone> Clone for QuerySignal<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
key: Rc::clone(&self.key),
|
||||
query: self.query.clone(),
|
||||
inner: self.inner.clone(),
|
||||
update_effect: self.update_effect.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> fmt::Debug for QuerySignal<T, E>
|
||||
impl<T> fmt::Debug for QuerySignal<T>
|
||||
where
|
||||
T: fmt::Debug,
|
||||
E: fmt::Debug,
|
||||
T: QueryParse + fmt::Debug,
|
||||
T::Value: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("QuerySignal")
|
||||
.field("key", &self.key)
|
||||
.field("query", &self.query)
|
||||
.field("inner", &self.inner)
|
||||
.field("update_effect", &self.update_effect)
|
||||
.finish()
|
||||
@ -99,23 +83,23 @@ where
|
||||
|
||||
/// Reference type for [`SignalBorrow`] impl
|
||||
#[derive(Debug)]
|
||||
pub struct BorrowRef<'a, T, E = <T as FromStr>::Err>(signal::BorrowRef<'a, Loadable<T, E>>);
|
||||
pub struct BorrowRef<'a, T: QueryParse>(signal::BorrowRef<'a, Option<T::Value>>);
|
||||
|
||||
impl<T, E> Deref for BorrowRef<'_, T, E> {
|
||||
type Target = Loadable<T, E>;
|
||||
impl<T: QueryParse> Deref for BorrowRef<'_, T> {
|
||||
type Target = T::Value;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
self.0.as_ref().expect("Should have value")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> SignalBorrow for QuerySignal<T, E>
|
||||
impl<T> SignalBorrow for QuerySignal<T>
|
||||
where
|
||||
T: 'static,
|
||||
E: 'static,
|
||||
T: QueryParse,
|
||||
T::Value: 'static,
|
||||
{
|
||||
type Ref<'a>
|
||||
= BorrowRef<'a, T, E>
|
||||
= BorrowRef<'a, T>
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
@ -129,152 +113,129 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> SignalReplace<Loadable<T, E>> for QuerySignal<T, E>
|
||||
impl<T> SignalReplace<T::Value> for QuerySignal<T>
|
||||
where
|
||||
T: ToString + 'static,
|
||||
E: 'static,
|
||||
T: QueryParse + QueryWriteValue,
|
||||
T::Value: 'static,
|
||||
{
|
||||
type Value = Loadable<T, E>;
|
||||
type Value = T::Value;
|
||||
|
||||
#[track_caller]
|
||||
fn replace(&self, new_value: Loadable<T, E>) -> Self::Value {
|
||||
mem::replace(&mut self.borrow_mut(), new_value)
|
||||
fn replace(&self, new_value: T::Value) -> Self::Value {
|
||||
mem::replace(&mut *self.borrow_mut(), new_value)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn replace_raw(&self, new_value: Loadable<T, E>) -> Self::Value {
|
||||
mem::replace(&mut self.borrow_mut_raw(), new_value)
|
||||
fn replace_raw(&self, new_value: T::Value) -> Self::Value {
|
||||
mem::replace(&mut *self.borrow_mut_raw(), new_value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the location on `Drop`
|
||||
// Note: We need this wrapper because `BorrowRefMut::value` must
|
||||
// already be dropped when we update the location, which we
|
||||
// can't do if we implement `Drop` on `BorrowRefMut`.
|
||||
struct UpdateLocationOnDrop<'a, T: ToString + 'static, E: 'static = <T as FromStr>::Err>(pub &'a QuerySignal<T, E>);
|
||||
|
||||
impl<'a, T, E> fmt::Debug for UpdateLocationOnDrop<'a, T, E>
|
||||
impl<T, U> SignalSet<U> for QuerySignal<T>
|
||||
where
|
||||
T: ToString + 'static + fmt::Debug,
|
||||
E: 'static,
|
||||
T: QueryParse + QueryWriteValue,
|
||||
T::Value: 'static,
|
||||
U: Into<T::Value>,
|
||||
{
|
||||
fn set(&self, new_value: U) {
|
||||
*self.borrow_mut() = new_value.into();
|
||||
}
|
||||
|
||||
fn set_raw(&self, new_value: U) {
|
||||
*self.borrow_mut_raw() = new_value.into();
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes the query on `Drop`
|
||||
// Note: We need this wrapper because `BorrowRefMut::value` must
|
||||
// already be dropped when we update the query, which we
|
||||
// can't do if we implement `Drop` on `BorrowRefMut`.
|
||||
// TODO: Remove this once we implement the trigger stack.
|
||||
struct WriteQueryOnDrop<'a, T>(pub &'a QuerySignal<T>)
|
||||
where
|
||||
T: QueryParse + QueryWriteValue,
|
||||
T::Value: 'static;
|
||||
|
||||
impl<'a, T> fmt::Debug for WriteQueryOnDrop<'a, T>
|
||||
where
|
||||
T: QueryParse + QueryWriteValue,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("UpdateLocationOnDrop").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> Drop for UpdateLocationOnDrop<'_, T, E>
|
||||
impl<T> Drop for WriteQueryOnDrop<'_, T>
|
||||
where
|
||||
T: ToString + 'static,
|
||||
E: 'static,
|
||||
T: QueryParse + QueryWriteValue,
|
||||
T::Value: 'static,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
// Update the location
|
||||
// Note: We suppress the update, given that it won't change anything,
|
||||
// as we already have the latest value.
|
||||
// TODO: Force an update anyway just to ensure some consistency with `FromStr` + `ToString`?
|
||||
self.0.update_effect.suppressed(|| {
|
||||
dynatos_context::with_expect::<Location, _, _>(|location| {
|
||||
let mut location = location.borrow_mut();
|
||||
let mut added_query = false;
|
||||
let mut queries = location
|
||||
.query_pairs()
|
||||
.into_owned()
|
||||
.filter_map(|(key, value)| {
|
||||
// If it's another key, keep it
|
||||
if key != *self.0.key {
|
||||
return Some((key, value));
|
||||
}
|
||||
|
||||
// If we already added our query, this is a duplicate, so skip it
|
||||
if added_query {
|
||||
return None;
|
||||
}
|
||||
|
||||
// If it's our key, check what we should do
|
||||
match &*self.0.inner.borrow_raw() {
|
||||
Loadable::Loaded(value) => {
|
||||
added_query = true;
|
||||
Some(((*self.0.key).to_owned(), value.to_string()))
|
||||
},
|
||||
Loadable::Err(_) => {
|
||||
tracing::warn!(key=?self.0.key, "Cannot assign an error to a query value");
|
||||
None
|
||||
},
|
||||
Loadable::Empty => None,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// If we haven't added ours yet by now, add it at the end
|
||||
if !added_query {
|
||||
match &*self.0.inner.borrow_raw() {
|
||||
Loadable::Loaded(value) => queries.push(((*self.0.key).to_owned(), value.to_string())),
|
||||
Loadable::Err(_) => tracing::warn!(key=?self.0.key, "Cannot assign an error to a query value"),
|
||||
Loadable::Empty => (),
|
||||
}
|
||||
}
|
||||
|
||||
location.query_pairs_mut().clear().extend_pairs(queries);
|
||||
});
|
||||
let value = self.0.inner.borrow_raw();
|
||||
let value = value.as_ref().expect("Should have value");
|
||||
self.0.query.write(value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Reference type for [`SignalBorrowMut`] impl
|
||||
pub struct BorrowRefMut<'a, T, E = <T as FromStr>::Err>
|
||||
pub struct BorrowRefMut<'a, T>
|
||||
where
|
||||
T: ToString + 'static,
|
||||
E: 'static,
|
||||
T: QueryParse + QueryWriteValue,
|
||||
T::Value: 'static,
|
||||
{
|
||||
/// Value
|
||||
value: signal::BorrowRefMut<'a, Loadable<T, E>>,
|
||||
value: signal::BorrowRefMut<'a, Option<T::Value>>,
|
||||
|
||||
/// Update location on drop
|
||||
/// Write query on drop
|
||||
// Note: Must be dropped *after* `value`.
|
||||
update_location_on_drop: Option<UpdateLocationOnDrop<'a, T, E>>,
|
||||
write_query_on_drop: Option<WriteQueryOnDrop<'a, T>>,
|
||||
}
|
||||
|
||||
impl<'a, T, E> fmt::Debug for BorrowRefMut<'a, T, E>
|
||||
impl<'a, T> fmt::Debug for BorrowRefMut<'a, T>
|
||||
where
|
||||
T: ToString + fmt::Debug + 'static,
|
||||
E: fmt::Debug,
|
||||
T: QueryParse + QueryWriteValue,
|
||||
T::Value: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("BorrowRefMut")
|
||||
.field("value", &self.value)
|
||||
.field("update_location_on_drop", &self.update_location_on_drop)
|
||||
.field("update_location_on_drop", &self.write_query_on_drop)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> Deref for BorrowRefMut<'_, T, E>
|
||||
impl<T> Deref for BorrowRefMut<'_, T>
|
||||
where
|
||||
T: ToString,
|
||||
T: QueryParse + QueryWriteValue,
|
||||
{
|
||||
type Target = Loadable<T, E>;
|
||||
type Target = T::Value;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.value
|
||||
self.value.as_ref().expect("Should have value")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> DerefMut for BorrowRefMut<'_, T, E>
|
||||
impl<T> DerefMut for BorrowRefMut<'_, T>
|
||||
where
|
||||
T: ToString,
|
||||
T: QueryParse + QueryWriteValue,
|
||||
{
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.value
|
||||
self.value.as_mut().expect("Should have value")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> SignalBorrowMut for QuerySignal<T, E>
|
||||
impl<T> SignalBorrowMut for QuerySignal<T>
|
||||
where
|
||||
T: ToString + 'static,
|
||||
E: 'static,
|
||||
T: QueryParse + QueryWriteValue,
|
||||
T::Value: 'static,
|
||||
{
|
||||
type RefMut<'a>
|
||||
= BorrowRefMut<'a, T, E>
|
||||
= BorrowRefMut<'a, T>
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
@ -283,7 +244,7 @@ where
|
||||
let value = self.inner.borrow_mut();
|
||||
BorrowRefMut {
|
||||
value,
|
||||
update_location_on_drop: Some(UpdateLocationOnDrop(self)),
|
||||
write_query_on_drop: Some(WriteQueryOnDrop(self)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -293,11 +254,46 @@ where
|
||||
let value = self.inner.borrow_mut_raw();
|
||||
BorrowRefMut {
|
||||
value,
|
||||
update_location_on_drop: None,
|
||||
write_query_on_drop: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> signal::SignalSetDefaultImpl for QuerySignal<T, E> {}
|
||||
impl<T, E> signal::SignalWithDefaultImpl for QuerySignal<T, E> {}
|
||||
impl<T, E> signal::SignalUpdateDefaultImpl for QuerySignal<T, E> {}
|
||||
// Note: We want a broader set impl to allow setting `T`s in `Loadable<T, E>`s.
|
||||
impl<T: QueryParse> !signal::SignalSetDefaultImpl for QuerySignal<T> {}
|
||||
impl<T: QueryParse> signal::SignalWithDefaultImpl for QuerySignal<T> {}
|
||||
impl<T: QueryParse> signal::SignalUpdateDefaultImpl for QuerySignal<T> {}
|
||||
|
||||
|
||||
/// Query parse
|
||||
pub trait QueryParse {
|
||||
/// Value
|
||||
type Value;
|
||||
|
||||
/// Parses the value from the query
|
||||
fn parse(&self) -> Self::Value;
|
||||
}
|
||||
|
||||
/// Query write
|
||||
pub trait QueryWrite<T> {
|
||||
/// Writes the value back into the query
|
||||
fn write(&self, new_value: T);
|
||||
}
|
||||
|
||||
/// Alias for a query that can write a reference to it's own value type
|
||||
pub trait QueryWriteValue = QueryParse + for<'a> QueryWrite<&'a <Self as QueryParse>::Value>;
|
||||
|
||||
type QueriesFn = impl Fn() -> Vec<String>;
|
||||
|
||||
#[define_opaque(QueriesFn)]
|
||||
fn queries_memo(key: Rc<str>) -> Memo<Vec<String>, QueriesFn> {
|
||||
Memo::new(move || {
|
||||
dynatos_context::with_expect::<Location, _, _>(|location| {
|
||||
location
|
||||
.borrow()
|
||||
.query_pairs()
|
||||
.filter_map(|(query, value)| (query == *key).then_some(value.into_owned()))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
151
dynatos-router/src/query_signal/multi_query.rs
Normal file
151
dynatos-router/src/query_signal/multi_query.rs
Normal file
@ -0,0 +1,151 @@
|
||||
//! Multi query
|
||||
|
||||
// Imports
|
||||
use {
|
||||
super::{QueriesFn, QueryParse, QueryWrite},
|
||||
crate::Location,
|
||||
core::{error::Error as StdError, fmt, marker::PhantomData, str::FromStr},
|
||||
dynatos_reactive::{Memo, SignalBorrow, SignalBorrowMut},
|
||||
std::rc::Rc,
|
||||
};
|
||||
|
||||
/// Parses multiple values from the query
|
||||
pub struct MultiQuery<T> {
|
||||
/// The key to this query
|
||||
key: Rc<str>,
|
||||
|
||||
/// Queries with our key
|
||||
queries: Memo<Vec<String>, QueriesFn>,
|
||||
|
||||
/// Phantom
|
||||
_phantom: PhantomData<fn() -> T>,
|
||||
}
|
||||
|
||||
impl<T> MultiQuery<T> {
|
||||
/// Creates a new query
|
||||
pub fn new(key: impl Into<Rc<str>>) -> Self {
|
||||
let key = key.into();
|
||||
Self {
|
||||
key: Rc::clone(&key),
|
||||
queries: super::queries_memo(key),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the key to this query
|
||||
#[must_use]
|
||||
pub fn key(&self) -> &str {
|
||||
&self.key
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for MultiQuery<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
key: Rc::clone(&self.key),
|
||||
queries: self.queries.clone(),
|
||||
..*self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: FromStr> QueryParse for MultiQuery<T> {
|
||||
type Value = Result<Vec<T>, QueryParseError<T>>;
|
||||
|
||||
fn parse(&self) -> Self::Value {
|
||||
let queries = self.queries.borrow();
|
||||
queries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, value)| match value.parse::<T>() {
|
||||
Ok(value) => Ok(value),
|
||||
Err(err) => Err(QueryParseError {
|
||||
idx,
|
||||
value: value.clone(),
|
||||
err,
|
||||
}),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: FromStr<Err: StdError> + ToString> QueryWrite<&'_ Result<Vec<T>, QueryParseError<T>>> for MultiQuery<T> {
|
||||
fn write(&self, new_value: &Result<Vec<T>, QueryParseError<T>>) {
|
||||
match new_value {
|
||||
Ok(new_value) => self.write(&**new_value),
|
||||
Err(err) => tracing::warn!(?self.key, ?err, "Cannot assign an error to a query value"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: FromStr<Err: StdError> + ToString> QueryWrite<&[T]> for MultiQuery<T> {
|
||||
fn write(&self, new_value: &[T]) {
|
||||
dynatos_context::with_expect::<Location, _, _>(|location| {
|
||||
let mut location = location.borrow_mut();
|
||||
let mut added_query = false;
|
||||
let mut queries = vec![];
|
||||
for (key, value) in location.query_pairs().into_owned() {
|
||||
// If it's another key, keep it
|
||||
if key != *self.key {
|
||||
queries.push((key, value));
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we already added our query, this is a duplicate, so skip it
|
||||
if added_query {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If it's our key, add all values
|
||||
added_query = true;
|
||||
queries.extend(
|
||||
new_value
|
||||
.iter()
|
||||
.map(T::to_string)
|
||||
.map(|value| (self.key.to_string(), value)),
|
||||
);
|
||||
}
|
||||
|
||||
// If we haven't added ours yet by now, add it at the end
|
||||
if !added_query {
|
||||
queries.extend(
|
||||
new_value
|
||||
.iter()
|
||||
.map(T::to_string)
|
||||
.map(|value| (self.key.to_string(), value)),
|
||||
);
|
||||
}
|
||||
|
||||
location.query_pairs_mut().clear().extend_pairs(queries);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Error for `Vec<T>` impl of [`FromQuery`]
|
||||
#[derive(thiserror::Error)]
|
||||
#[error("Unable to parse argument {idx}: {value:?}")]
|
||||
pub struct QueryParseError<T: FromStr> {
|
||||
/// Index we were unable to parse
|
||||
idx: usize,
|
||||
|
||||
/// Value we were unable to parse
|
||||
value: String,
|
||||
|
||||
/// Inner error
|
||||
#[source]
|
||||
err: T::Err,
|
||||
}
|
||||
|
||||
impl<T> fmt::Debug for QueryParseError<T>
|
||||
where
|
||||
T: FromStr,
|
||||
T::Err: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("VecFromQueryError")
|
||||
.field("idx", &self.idx)
|
||||
.field("value", &self.value)
|
||||
.field("err", &self.err)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
114
dynatos-router/src/query_signal/single_query.rs
Normal file
114
dynatos-router/src/query_signal/single_query.rs
Normal file
@ -0,0 +1,114 @@
|
||||
//! Single query
|
||||
|
||||
// Imports
|
||||
use {
|
||||
super::{QueriesFn, QueryParse, QueryWrite},
|
||||
crate::Location,
|
||||
core::{error::Error as StdError, marker::PhantomData, str::FromStr},
|
||||
dynatos_loadable::Loadable,
|
||||
dynatos_reactive::{Memo, SignalBorrow, SignalBorrowMut},
|
||||
std::rc::Rc,
|
||||
};
|
||||
|
||||
/// Parses a singular value from the query
|
||||
pub struct SingleQuery<T> {
|
||||
/// The key to this query
|
||||
key: Rc<str>,
|
||||
|
||||
/// Queries with our key
|
||||
queries: Memo<Vec<String>, QueriesFn>,
|
||||
|
||||
/// Phantom
|
||||
_phantom: PhantomData<fn() -> T>,
|
||||
}
|
||||
|
||||
impl<T> SingleQuery<T> {
|
||||
/// Creates a new query
|
||||
pub fn new(key: impl Into<Rc<str>>) -> Self {
|
||||
let key = key.into();
|
||||
Self {
|
||||
key: Rc::clone(&key),
|
||||
queries: super::queries_memo(key),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the key to this query
|
||||
#[must_use]
|
||||
pub fn key(&self) -> &str {
|
||||
&self.key
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for SingleQuery<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
key: Rc::clone(&self.key),
|
||||
queries: self.queries.clone(),
|
||||
..*self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: FromStr> QueryParse for SingleQuery<T> {
|
||||
type Value = Loadable<T, T::Err>;
|
||||
|
||||
fn parse(&self) -> Self::Value {
|
||||
let queries = self.queries.borrow();
|
||||
let value = match &**queries {
|
||||
[] => return Loadable::Empty,
|
||||
[value] => value,
|
||||
[first, ref rest @ ..] => {
|
||||
tracing::warn!(?self.key, ?first, ?rest, "Ignoring duplicate queries, using first");
|
||||
first
|
||||
},
|
||||
};
|
||||
|
||||
value.parse::<T>().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: FromStr<Err: StdError> + ToString> QueryWrite<&'_ Loadable<T, T::Err>> for SingleQuery<T> {
|
||||
fn write(&self, new_value: &Loadable<T, T::Err>) {
|
||||
match new_value {
|
||||
Loadable::Empty => self.write(None),
|
||||
Loadable::Err(err) => tracing::warn!(?self.key, ?err, "Cannot assign an error to a query value"),
|
||||
Loadable::Loaded(new_value) => self.write(Some(new_value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: FromStr<Err: StdError> + ToString> QueryWrite<Option<&'_ T>> for SingleQuery<T> {
|
||||
fn write(&self, new_value: Option<&T>) {
|
||||
dynatos_context::with_expect::<Location, _, _>(|location| {
|
||||
let mut location = location.borrow_mut();
|
||||
let mut added_query = false;
|
||||
let mut queries = vec![];
|
||||
for (key, value) in location.query_pairs().into_owned() {
|
||||
// If it's another key, keep it
|
||||
if key != *self.key {
|
||||
queries.push((key, value));
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we already added our query, this is a duplicate, so skip it
|
||||
if added_query {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If it's our key, check what we should do
|
||||
if let Some(new_value) = new_value {
|
||||
added_query = true;
|
||||
queries.push((self.key.to_string(), new_value.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
// If we haven't added ours yet by now, add it at the end
|
||||
if !added_query && let Some(new_value) = new_value {
|
||||
queries.push((self.key.to_string(), new_value.to_string()));
|
||||
}
|
||||
|
||||
location.query_pairs_mut().clear().extend_pairs(queries);
|
||||
});
|
||||
}
|
||||
}
|
||||
21
examples/Cargo.lock
generated
21
examples/Cargo.lock
generated
@ -266,6 +266,7 @@ dependencies = [
|
||||
"dynatos-util",
|
||||
"extend",
|
||||
"js-sys",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
@ -890,6 +891,26 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.7"
|
||||
|
||||
@ -9,7 +9,7 @@ use {
|
||||
dynatos_html::{ev, html, EventTargetWithListener, NodeWithChildren, NodeWithText},
|
||||
dynatos_loadable::Loadable,
|
||||
dynatos_reactive::{SignalBorrowMut, SignalGetCloned, SignalSet},
|
||||
dynatos_router::{Location, QuerySignal},
|
||||
dynatos_router::{Location, QuerySignal, SingleQuery},
|
||||
tracing_subscriber::prelude::*,
|
||||
web_sys::HtmlElement,
|
||||
zutil_cloned::cloned,
|
||||
@ -47,7 +47,7 @@ fn run() -> Result<(), anyhow::Error> {
|
||||
|
||||
fn page() -> HtmlElement {
|
||||
// TODO: If we add `.with_loadable_default()`, use it again in this example.
|
||||
let query = QuerySignal::<i32>::new("a");
|
||||
let query = QuerySignal::new(SingleQuery::<i32>::new("a"));
|
||||
|
||||
html::div().with_children([
|
||||
#[cloned(query)]
|
||||
@ -70,7 +70,7 @@ fn page() -> HtmlElement {
|
||||
html::br(),
|
||||
html::button()
|
||||
.with_event_listener::<ev::Click>(move |_ev| {
|
||||
query.set(Loadable::Loaded(6));
|
||||
query.set(6);
|
||||
})
|
||||
.with_text("6"),
|
||||
])
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user