use crate::store::facts::{Key, Refused};
use crate::store::{OnUnreadable, OpenStruct, StorageError, Writer};
use amethystate_core::path::StorePath;
use error_stack::Report;
use std::any::{Any, TypeId, type_name};
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
pub type Rule<TValue> = fn(&mut TValue, &RuleContext) -> Result<(), Invalid>;
#[derive(Default)]
pub struct RuleContext {
values: HashMap<TypeId, Held>,
}
struct Held {
type_name: &'static str,
value: Arc<dyn Any + Send + Sync>,
}
impl RuleContext {
pub(crate) fn insert<T: Any + Send + Sync>(&mut self, value: T) {
self.values.insert(
TypeId::of::<T>(),
Held {
type_name: type_name::<T>(),
value: Arc::new(value),
},
);
}
pub fn get<T: Any + Send + Sync>(&self) -> Option<&T> {
self.values
.get(&TypeId::of::<T>())
.and_then(|held| held.value.downcast_ref::<T>())
}
pub fn require<T: Any + Send + Sync>(&self) -> Result<&T, Invalid> {
match self.get::<T>() {
Some(value) => Ok(value),
None => Err(Invalid::new(format!(
"no value provided for {}; {}. A declared rule, and a struct that \
opens its own way, are handed their values through StoreBuilder::context",
type_name::<T>(),
self.on_offer()
))),
}
}
fn on_offer(&self) -> String {
let mut names: Vec<&'static str> =
self.values.values().map(|held| held.type_name).collect();
names.sort_unstable();
if names.is_empty() {
"nothing was given".to_string()
} else {
format!("given: {}", names.join(", "))
}
}
}
impl fmt::Debug for RuleContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RuleContext")
.field("types", &self.on_offer())
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Invalid {
reason: Cow<'static, str>,
}
impl Invalid {
pub fn new(reason: impl Into<Cow<'static, str>>) -> Self {
Self {
reason: reason.into(),
}
}
pub fn reason(&self) -> &str {
&self.reason
}
}
impl fmt::Display for Invalid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.reason)
}
}
impl From<&'static str> for Invalid {
fn from(reason: &'static str) -> Self {
Self::new(reason)
}
}
impl From<String> for Invalid {
fn from(reason: String) -> Self {
Self::new(reason)
}
}
pub fn refused(path: &StorePath, invalid: &Invalid) -> Report<StorageError> {
Report::new(StorageError::Read)
.attach(Key(path.clone()))
.attach(Refused(invalid.reason().to_string()))
}
pub(crate) fn judged<TValue: serde::Serialize>(
rule: Rule<TValue>,
value: &mut TValue,
context: &RuleContext,
) -> Result<bool, Invalid> {
let before = serde_json::to_value(&*value).ok();
rule(value, context)?;
Ok(match (before, serde_json::to_value(&*value).ok()) {
(Some(before), Some(after)) => before != after,
_ => true,
})
}
pub(crate) fn load_declared<TValue>(
store: &crate::Store,
at: &StorePath,
stored_as: crate::store::traits::StoredAs<TValue>,
rule: Option<Rule<TValue>>,
policy: OnUnreadable,
default: impl FnOnce() -> TValue,
) -> Result<TValue, OpenStruct>
where
TValue: serde::de::DeserializeOwned + serde::Serialize + 'static,
{
let mut held = match crate::store::read_stored(store, at, stored_as) {
Ok(Some(held)) => held,
Ok(None) => return Ok(default()),
Err(why) => {
return match policy {
OnUnreadable::Refuse => Err(OpenStruct::WillNotRead {
at: at.clone(),
why: why.into(),
}),
OnUnreadable::UseDefault => {
tracing::error!(
target: "amethystate",
path = %at,
"what is stored will not read back as this field's type, so the field \
was loaded on its default: {why:?}"
);
Ok(default())
}
};
}
};
let Some(rule) = rule else {
return Ok(held);
};
match judged(rule, &mut held, store.context()) {
Ok(false) => Ok(held),
Ok(true) => {
crate::store::write_stored(store, at, &held, stored_as, Writer::judged(None))?;
Ok(held)
}
Err(invalid) => refused_or_default(at, invalid, policy, default()),
}
}
pub(crate) fn judge_declared<TValue>(
store: &crate::Store,
at: &StorePath,
value: &mut TValue,
rule: Option<Rule<TValue>>,
) -> Result<(), crate::store::WriteValue> {
let Some(rule) = rule else {
return Ok(());
};
rule(value, store.context()).map_err(|invalid| crate::store::WriteValue::Refused {
at: at.clone(),
said: invalid.reason().into(),
})
}
pub(crate) fn save_declared<TValue>(
store: &crate::Store,
at: &StorePath,
value: &TValue,
stored_as: crate::store::traits::StoredAs<TValue>,
) -> Result<(), crate::store::WriteValue>
where
TValue: serde::Serialize + 'static,
{
crate::store::write_stored(store, at, value, stored_as, Writer::judged(None))
.map_err(|why| crate::store::WriteValue::from_store(at, why))
}
pub fn refused_or_default<TValue>(
path: &StorePath,
invalid: Invalid,
policy: OnUnreadable,
default: TValue,
) -> Result<TValue, OpenStruct> {
match policy {
OnUnreadable::Refuse => Err(OpenStruct::Refused {
at: path.clone(),
said: Arc::from(invalid.reason()),
}),
OnUnreadable::UseDefault => {
tracing::error!(
target: "amethystate",
path = %path,
reason = %invalid,
"a declared rule refused the stored value, so the field was loaded on its default"
);
Ok(default)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn rebuilt(map: &mut HashMap<String, u32>, _cx: &RuleContext) -> Result<(), Invalid> {
*map = map
.iter()
.map(|(name, width)| (name.clone(), *width))
.collect();
Ok(())
}
fn clamped(map: &mut HashMap<(u8, u8), u32>, _cx: &RuleContext) -> Result<(), Invalid> {
map.values_mut()
.for_each(|width| *width = (*width).min(100));
Ok(())
}
#[test]
fn a_map_the_rule_rebuilds_with_the_same_entries_is_unchanged() {
let mut widths: HashMap<String, u32> = (0..16).map(|n| (format!("column{n}"), n)).collect();
assert_eq!(
judged(rebuilt, &mut widths, &RuleContext::default()).ok(),
Some(false)
);
}
#[test]
fn a_correction_to_a_value_json_cannot_hold_counts_as_a_change() {
let mut widths = HashMap::from([((0, 1), 500u32)]);
assert_eq!(
judged(clamped, &mut widths, &RuleContext::default()).ok(),
Some(true)
);
}
}