use leptos::prelude::*;
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::fmt;
use std::marker::PhantomData;
use std::sync::Arc;
use thiserror::Error;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct StoreId {
type_id: TypeId,
instance_id: u64,
}
impl StoreId {
pub fn new<T: 'static>() -> Self {
Self {
type_id: TypeId::of::<T>(),
instance_id: 0,
}
}
pub fn with_instance<T: 'static>(instance_id: u64) -> Self {
Self {
type_id: TypeId::of::<T>(),
instance_id,
}
}
}
impl fmt::Debug for StoreId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StoreId")
.field("type_id", &self.type_id)
.field("instance_id", &self.instance_id)
.finish()
}
}
#[derive(Debug, Error)]
pub enum StoreError {
#[error("Store not found: {0}")]
NotFound(String),
#[error("Store already exists: {0}")]
AlreadyExists(String),
#[error("Invalid state transition: {0}")]
InvalidTransition(String),
#[error("Mutation failed: {0}")]
MutationFailed(String),
#[error("Context not available: {0}")]
ContextNotAvailable(String),
}
pub trait Store: Clone + Send + Sync + 'static {
type State: Clone + Send + Sync + 'static;
fn state(&self) -> ReadSignal<Self::State>;
fn id(&self) -> StoreId {
StoreId::new::<Self>()
}
fn name(&self) -> &'static str {
std::any::type_name::<Self>()
}
}
#[derive(Clone)]
pub struct ReadonlyStore<S: Store> {
inner: S,
}
impl<S: Store> ReadonlyStore<S> {
pub fn new(store: S) -> Self {
Self { inner: store }
}
pub fn get(&self) -> S::State {
self.inner.state().get()
}
pub fn with<U>(&self, f: impl FnOnce(&S::State) -> U) -> U {
self.inner.state().with(f)
}
pub fn inner(&self) -> &S {
&self.inner
}
}
pub trait Getter<State, Output> {
fn get(&self, state: &State) -> Output;
}
impl<State, Output, F> Getter<State, Output> for F
where
F: Fn(&State) -> Output,
{
fn get(&self, state: &State) -> Output {
self(state)
}
}
pub struct MutatorContext<'a, State> {
state: &'a mut State,
}
impl<'a, State> MutatorContext<'a, State> {
pub fn new(state: &'a mut State) -> Self {
Self { state }
}
pub fn state_mut(&mut self) -> &mut State {
self.state
}
pub fn state(&self) -> &State {
self.state
}
}
pub trait Mutator<State> {
fn mutate(&self, ctx: &mut MutatorContext<State>);
}
impl<State, F> Mutator<State> for F
where
F: Fn(&mut MutatorContext<State>),
{
fn mutate(&self, ctx: &mut MutatorContext<State>) {
self(ctx)
}
}
pub struct StoreBuilder<State> {
initial_state: Option<State>,
_marker: PhantomData<State>,
}
impl<State: Clone + Send + Sync + 'static> Default for StoreBuilder<State> {
fn default() -> Self {
Self::new()
}
}
impl<State: Clone + Send + Sync + 'static> StoreBuilder<State> {
pub fn new() -> Self {
Self {
initial_state: None,
_marker: PhantomData,
}
}
pub fn with_state(mut self, state: State) -> Self {
self.initial_state = Some(state);
self
}
pub fn build(self) -> RwSignal<State>
where
State: Default + Send + Sync,
{
let state = self.initial_state.unwrap_or_default();
RwSignal::new(state)
}
pub fn try_build(self) -> Result<RwSignal<State>, StoreError>
where
State: Send + Sync,
{
let state = self
.initial_state
.ok_or_else(|| StoreError::NotFound("Initial state not provided".to_string()))?;
Ok(RwSignal::new(state))
}
}
#[derive(Default)]
pub struct StoreRegistry {
stores: HashMap<StoreId, Arc<dyn Any + Send + Sync>>,
}
impl StoreRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register<S: Store + Send + Sync>(&mut self, store: S) -> Result<StoreId, StoreError> {
let id = store.id();
if self.stores.contains_key(&id) {
return Err(StoreError::AlreadyExists(store.name().to_string()));
}
self.stores.insert(id, Arc::new(store));
Ok(id)
}
pub fn get<S: Store + Send + Sync>(&self) -> Option<Arc<S>> {
let id = StoreId::new::<S>();
self.stores
.get(&id)
.and_then(|s| s.clone().downcast::<S>().ok())
}
pub fn unregister<S: Store>(&mut self) -> bool {
let id = StoreId::new::<S>();
self.stores.remove(&id).is_some()
}
pub fn contains<S: Store>(&self) -> bool {
let id = StoreId::new::<S>();
self.stores.contains_key(&id)
}
pub fn len(&self) -> usize {
self.stores.len()
}
pub fn is_empty(&self) -> bool {
self.stores.is_empty()
}
}
impl fmt::Debug for StoreRegistry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StoreRegistry")
.field("count", &self.stores.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone, Debug, Default, PartialEq)]
struct TestState {
count: i32,
name: String,
}
#[derive(Clone)]
struct TestStore {
state: RwSignal<TestState>,
}
impl Store for TestStore {
type State = TestState;
fn state(&self) -> ReadSignal<Self::State> {
self.state.read_only()
}
}
#[test]
fn test_store_id_creation() {
let id1 = StoreId::new::<TestStore>();
let id2 = StoreId::new::<TestStore>();
assert_eq!(id1, id2);
let id3 = StoreId::with_instance::<TestStore>(1);
assert_ne!(id1, id3);
}
#[test]
fn test_store_builder() {
let state: RwSignal<TestState> = StoreBuilder::new()
.with_state(TestState {
count: 42,
name: "test".to_string(),
})
.build();
assert_eq!(state.get().count, 42);
assert_eq!(state.get().name, "test");
}
#[test]
fn test_store_builder_default() {
let state: RwSignal<TestState> = StoreBuilder::new().build();
assert_eq!(state.get().count, 0);
assert_eq!(state.get().name, "");
}
#[test]
fn test_getter_closure() {
let state = TestState {
count: 10,
name: "Alice".to_string(),
};
let doubled = |s: &TestState| s.count * 2;
assert_eq!(doubled.get(&state), 20);
}
#[test]
fn test_mutator_closure() {
let mut state = TestState::default();
let mut ctx = MutatorContext::new(&mut state);
let increment = |ctx: &mut MutatorContext<TestState>| {
ctx.state_mut().count += 1;
};
increment.mutate(&mut ctx);
assert_eq!(ctx.state().count, 1);
}
#[test]
fn test_mutator_context() {
let mut state = TestState {
count: 5,
name: "Bob".to_string(),
};
{
let mut ctx = MutatorContext::new(&mut state);
ctx.state_mut().count = 10;
ctx.state_mut().name = "Charlie".to_string();
}
assert_eq!(state.count, 10);
assert_eq!(state.name, "Charlie");
}
#[test]
fn test_store_error_display() {
let err = StoreError::NotFound("TestStore".to_string());
assert_eq!(err.to_string(), "Store not found: TestStore");
let err = StoreError::AlreadyExists("TestStore".to_string());
assert_eq!(err.to_string(), "Store already exists: TestStore");
}
}