QuerySignal is now controlled by a query type.

Added query types `SingleQuery` and `MultiQuery`.
Removed `QueryArraySignal`.
This commit is contained in:
Filipe Rodrigues 2025-05-23 02:09:11 +01:00
parent 8137a555f8
commit 08fc0bc9e5
Signed by: zenithsiz
SSH Key Fingerprint: SHA256:Mb5ppb3Sh7IarBO/sBTXLHbYEOz37hJAlslLQPPAPaU
11 changed files with 464 additions and 396 deletions

View File

@ -10,6 +10,7 @@
"popstate",
"qself",
"scopeguard",
"thiserror",
"unsize",
"zutil"
],

21
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<_>>()
})
})
}

View 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()
}
}

View 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
View File

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

View File

@ -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"),
])