use core::any::{TypeId, type_name};
use core::ops::{Deref, DerefMut};
use crate::Environment;
use alloc::{collections::BTreeMap, format};
use anyhow::Error;
#[diagnostic::on_unimplemented(
message = "`{Self}` cannot be extracted from a WaterUI environment",
label = "expected a type implementing `Extractor`",
note = "Handler and `use_env` parameters must be extractors: `#[state]` makes an owned `Clone` type extractable from `.state(&value)` injections, `State<T>` wraps an injected value of a foreign type, `Use<T>` reads an environment value, `Option<E>` tolerates a missing one, tuples combine extractors, and `impl_extractor!` marks a `Clone` type installed as an environment value."
)]
pub trait Extractor: 'static + Sized {
fn extract(env: &Environment) -> Result<Self, Error>;
fn extract_from_action(env: &Environment, state: &mut ExtractionState) -> Result<Self, Error> {
let _ = state;
Self::extract(env)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Use<T: 'static>(pub T);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct State<T: 'static>(pub T);
#[derive(Debug, Clone, Default)]
pub struct ExtractionState {
positions: BTreeMap<TypeId, usize>,
}
impl ExtractionState {
#[must_use]
pub fn take_next<T: 'static>(&mut self) -> usize {
let position = self.positions.entry(TypeId::of::<T>()).or_insert(0);
let current = *position;
*position += 1;
current
}
}
impl Extractor for Environment {
fn extract(env: &Environment) -> Result<Self, Error> {
Ok(env.clone())
}
}
impl<T> Deref for Use<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for Use<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<T> Deref for State<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for State<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<T: Extractor> Extractor for Option<T> {
fn extract(env: &Environment) -> Result<Self, Error> {
Ok(<T as Extractor>::extract(env).ok())
}
fn extract_from_action(env: &Environment, state: &mut ExtractionState) -> Result<Self, Error> {
let snapshot = state.clone();
T::extract_from_action(env, state).map_or_else(
|_| {
*state = snapshot;
Ok(None)
},
|value| Ok(Some(value)),
)
}
}
impl<T: 'static + Clone> Extractor for Use<T> {
fn extract(env: &Environment) -> Result<Self, Error> {
env.get::<T>().map_or_else(
|| {
Err(Error::msg(format!(
"Environment value `{}` not found",
type_name::<T>()
)))
},
|value| Ok(Self(value.clone())),
)
}
}
impl<T: 'static + Clone> Extractor for State<T> {
fn extract(env: &Environment) -> Result<Self, Error> {
env.get::<Self>().map_or_else(
|| {
Err(Error::msg(format!(
"Environment state `{}` not found",
type_name::<T>()
)))
},
|value| Ok(value.clone()),
)
}
fn extract_from_action(env: &Environment, state: &mut ExtractionState) -> Result<Self, Error> {
let position = state.take_next::<Self>();
env.get_nth::<Self>(position).map_or_else(
|| {
Err(Error::msg(format!(
"Environment state `{}` not found at position {}",
type_name::<T>(),
position
)))
},
|value| Ok(value.clone()),
)
}
}
macro_rules! impl_tuple_extractor {
($($T:ident),+) => {
impl<$($T: Extractor),+> Extractor for ($($T,)+) {
fn extract(env: &Environment) -> Result<Self, Error> {
Ok(($($T::extract(env)?,)+))
}
fn extract_from_action(
env: &Environment,
state: &mut ExtractionState,
) -> Result<Self, Error> {
Ok(($($T::extract_from_action(env, state)?,)+))
}
}
};
}
impl_tuple_extractor!(A, B);
impl_tuple_extractor!(A, B, C);
impl_tuple_extractor!(A, B, C, D);
impl_tuple_extractor!(A, B, C, D, E);
impl_tuple_extractor!(A, B, C, D, E, F);
impl_tuple_extractor!(A, B, C, D, E, F, G);
impl_tuple_extractor!(A, B, C, D, E, F, G, H);
#[cfg(test)]
mod tests {
use super::*;
use crate::Environment;
use waterui_macros::state;
#[state]
#[derive(Clone)]
struct Editor {
title: &'static str,
}
#[test]
fn state_injected_owned_type_extracts_bare() {
let env = Environment::new().extending(State(Editor { title: "draft" }));
let editor = env.extract::<Editor>().expect("injected state extracts");
assert_eq!(editor.title, "draft");
}
#[test]
fn same_type_states_bind_positionally_and_share_positions() {
let env = Environment::new()
.extending(State(Editor { title: "second" }))
.extending(State(Editor { title: "first" }));
let mut state = ExtractionState::default();
let bare = Editor::extract_from_action(&env, &mut state).expect("first parameter");
let wrapped = <State<Editor> as Extractor>::extract_from_action(&env, &mut state)
.expect("second parameter");
assert_eq!(bare.title, "first");
assert_eq!(wrapped.0.title, "second");
}
}