use crate::migration::StepResult;
use crate::migration::fields::Role;
use crate::observability::Disagreement;
use crate::reactive::FieldValue;
use crate::reactive::map::ReactiveMap;
use crate::store::{
OnDelete, OnUnreadable, OpenStruct, ReadRules, Rule, StoredAs, UnreadableEntries, WriteValue,
};
use crate::{
Field, MigrationContext, ReactiveMapKey, ReactiveMapValue, SignalSubscription, Store, StoreExt,
};
use amethystate_core::path::StorePath;
use indexmap::IndexMap;
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::collections::HashMap;
use std::fmt;
use std::marker::PhantomData;
use uuid::Uuid;
#[doc(hidden)]
pub struct Probe<T: ?Sized>(PhantomData<T>);
#[doc(hidden)]
pub trait AnyShape {
const ROLE: Role = Role::Field;
const OPTIONAL: bool = false;
}
impl<T: ?Sized> AnyShape for Probe<T> {}
impl<K, V> Probe<ReactiveMap<K, V>> {
pub const ROLE: Role = Role::Map;
pub const OPTIONAL: bool = false;
}
impl<T> Probe<Option<T>> {
pub const ROLE: Role = Role::Field;
pub const OPTIONAL: bool = true;
}
#[doc(hidden)]
pub trait Kind: Sized + 'static {
type Handle: Clone;
type Data;
type Seed;
type Peek;
fn build(
store: &Store,
at: StorePath,
seed: Self::Seed,
instance_id: Uuid,
rules: KindRules<Self>,
) -> Result<Self::Handle, OpenStruct>;
fn load_step(
ctx: &mut MigrationContext<'_>,
at: &[&str],
stored_as: StoredAs<Self>,
seed: impl FnOnce() -> Self::Seed,
) -> StepResult<Self::Data>;
fn save_step(
data: &Self::Data,
ctx: &mut MigrationContext<'_>,
at: &[&str],
stored_as: StoredAs<Self>,
) -> StepResult<()>;
fn load_plain(
store: &Store,
at: &StorePath,
stored_as: StoredAs<Self>,
rule: Option<Rule<Self>>,
policy: OnUnreadable,
seed: impl FnOnce() -> Self::Seed,
) -> Result<Self::Data, OpenStruct>;
fn judge_plain(
data: &mut Self::Data,
store: &Store,
at: &StorePath,
rule: Option<Rule<Self>>,
) -> Result<(), WriteValue>;
fn save_plain(
data: &Self::Data,
store: &Store,
at: &StorePath,
stored_as: StoredAs<Self>,
) -> Result<(), WriteValue>;
fn watch(
handle: &Self::Handle,
on_change: impl Fn() + Send + Sync + 'static,
) -> SignalSubscription;
fn peek(handle: &Self::Handle) -> Self::Peek;
fn disagreement(handle: &Self::Handle) -> Option<Disagreement>;
#[cfg(feature = "tauri")]
fn accepts(json: &str, stored_as: StoredAs<Self>) -> bool;
#[cfg(feature = "tauri")]
fn written(seed: Self::Seed, stored_as: StoredAs<Self>) -> Option<String>;
}
#[doc(hidden)]
pub struct KindRules<T> {
pub on_unreadable: OnUnreadable,
pub on_delete: OnDelete,
pub unreadable_entries: UnreadableEntries,
pub rule: Option<Rule<T>>,
pub stored_as: StoredAs<T>,
}
#[doc(hidden)]
pub struct EntryCount(pub usize);
impl fmt::Debug for EntryCount {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} entries", self.0)
}
}
impl<T: FieldValue> Kind for T {
type Handle = Field<T>;
type Data = T;
type Seed = T;
type Peek = T;
fn build(
store: &Store,
at: StorePath,
seed: T,
instance_id: Uuid,
rules: KindRules<T>,
) -> Result<Field<T>, OpenStruct> {
let read = ReadRules::new()
.on_unreadable(rules.on_unreadable)
.on_delete(rules.on_delete)
.stored_as(rules.stored_as);
let read = match rules.rule {
Some(rule) => read.rule(rule),
None => read,
};
crate::store::field_with_path_under(store, at, seed, instance_id, read)
}
fn load_step(
ctx: &mut MigrationContext<'_>,
at: &[&str],
stored_as: StoredAs<T>,
seed: impl FnOnce() -> T,
) -> StepResult<T> {
Ok(ctx.get_as::<T>(at, stored_as)?.unwrap_or_else(seed))
}
fn save_step(
data: &T,
ctx: &mut MigrationContext<'_>,
at: &[&str],
stored_as: StoredAs<T>,
) -> StepResult<()> {
ctx.set_as(at, data, stored_as)
}
fn load_plain(
store: &Store,
at: &StorePath,
stored_as: StoredAs<T>,
rule: Option<Rule<T>>,
policy: OnUnreadable,
seed: impl FnOnce() -> T,
) -> Result<T, OpenStruct> {
crate::store::load_declared(store, at, stored_as, rule, policy, seed)
}
fn judge_plain(
data: &mut T,
store: &Store,
at: &StorePath,
rule: Option<Rule<T>>,
) -> Result<(), WriteValue> {
crate::store::judge_declared(store, at, data, rule)
}
fn save_plain(
data: &T,
store: &Store,
at: &StorePath,
stored_as: StoredAs<T>,
) -> Result<(), WriteValue> {
crate::store::save_declared(store, at, data, stored_as)
}
fn watch(
handle: &Field<T>,
on_change: impl Fn() + Send + Sync + 'static,
) -> SignalSubscription {
handle.subscribe(move |_| on_change())
}
fn peek(handle: &Field<T>) -> T {
handle.get()
}
fn disagreement(handle: &Field<T>) -> Option<Disagreement> {
handle.__ame_disagreement()
}
#[cfg(feature = "tauri")]
fn accepts(json: &str, stored_as: StoredAs<T>) -> bool {
crate::tauri::accepts::<T>(json, stored_as)
}
#[cfg(feature = "tauri")]
fn written(seed: T, stored_as: StoredAs<T>) -> Option<String> {
crate::tauri::written::<T>(&seed, stored_as)
}
}
impl<K, V> Kind for ReactiveMap<K, V>
where
K: ReactiveMapKey,
V: ReactiveMapValue + Serialize + DeserializeOwned,
{
type Handle = ReactiveMap<K, V>;
type Data = IndexMap<K, V>;
type Seed = HashMap<K, V>;
type Peek = EntryCount;
fn build(
store: &Store,
at: StorePath,
seed: HashMap<K, V>,
instance_id: Uuid,
rules: KindRules<Self>,
) -> Result<Self, OpenStruct> {
Ok(crate::store::reactive_map_where::<K, V>(
store,
at,
seed,
instance_id,
rules.unreadable_entries,
rules.on_delete,
)?)
}
fn load_step(
ctx: &mut MigrationContext<'_>,
at: &[&str],
_stored_as: StoredAs<Self>,
_seed: impl FnOnce() -> HashMap<K, V>,
) -> StepResult<IndexMap<K, V>> {
ctx.scan_map::<K, V>(at)
}
fn save_step(
data: &IndexMap<K, V>,
ctx: &mut MigrationContext<'_>,
at: &[&str],
_stored_as: StoredAs<Self>,
) -> StepResult<()> {
ctx.delete_prefix(at)?;
let mut entries = ctx.scoped(at);
for (key, value) in data {
entries.set(key.as_ref(), value)?;
}
Ok(())
}
fn load_plain(
store: &Store,
at: &StorePath,
_stored_as: StoredAs<Self>,
_rule: Option<Rule<Self>>,
_policy: OnUnreadable,
_seed: impl FnOnce() -> HashMap<K, V>,
) -> Result<IndexMap<K, V>, OpenStruct> {
Ok(crate::store::load_map::<K, V>(store, at)?)
}
fn judge_plain(
_data: &mut IndexMap<K, V>,
_store: &Store,
_at: &StorePath,
_rule: Option<Rule<Self>>,
) -> Result<(), WriteValue> {
Ok(())
}
fn save_plain(
data: &IndexMap<K, V>,
store: &Store,
at: &StorePath,
_stored_as: StoredAs<Self>,
) -> Result<(), WriteValue> {
let kept: std::collections::HashSet<&str> = data.keys().map(|key| key.as_ref()).collect();
let stored = crate::store::StoreBackend::scan_keys(store, at)
.map_err(|why| WriteValue::from_store(at, why))?;
let gone: std::collections::BTreeSet<String> = stored
.iter()
.filter_map(|path| path.segments().nth(at.len()))
.map(|name| name.as_str().to_string())
.filter(|name| !kept.contains(name.as_str()))
.collect();
for name in gone {
let entry = crate::store::entry_path(at, &name);
crate::store::StoreBackend::delete_prefix(store, &entry)
.map_err(|why| WriteValue::from_store(&entry, why))?;
}
for (key, value) in data {
let entry = crate::store::entry_path(at, key.as_ref());
<Store as StoreExt>::set(store, &entry, value)?;
}
Ok(())
}
fn watch(handle: &Self, on_change: impl Fn() + Send + Sync + 'static) -> SignalSubscription {
handle.subscribe_any(move |_| on_change())
}
fn peek(handle: &Self) -> EntryCount {
EntryCount(handle.len())
}
fn disagreement(_handle: &Self) -> Option<Disagreement> {
None
}
#[cfg(feature = "tauri")]
fn accepts(json: &str, _stored_as: StoredAs<Self>) -> bool {
crate::tauri::accepts::<V>(json, StoredAs::default())
}
#[cfg(feature = "tauri")]
fn written(_seed: HashMap<K, V>, _stored_as: StoredAs<Self>) -> Option<String> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
fn handled_by<T: Kind>() -> PhantomData<T::Handle> {
PhantomData
}
type Widths = ReactiveMap<String, u64>;
#[test]
fn a_value_is_held_by_a_field_and_a_map_by_itself() {
let _: PhantomData<Field<u16>> = handled_by::<u16>();
let _: PhantomData<Field<Option<String>>> = handled_by::<Option<String>>();
let _: PhantomData<ReactiveMap<String, u64>> = handled_by::<ReactiveMap<String, u64>>();
}
#[test]
fn an_alias_is_answered_for_the_type_it_names() {
let _: PhantomData<ReactiveMap<String, u64>> = handled_by::<Widths>();
}
#[test]
fn a_map_shows_how_many_entries_it_holds() {
assert_eq!(format!("{:?}", EntryCount(3)), "3 entries");
}
}