use crate::store::facts::{Key, Prefix, Refused};
use crate::store::{OnUnreadable, OpenStruct, StorageError};
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 Check<TValue> = fn(&mut TValue, &CheckContext) -> Result<(), Invalid>;
#[derive(Default)]
pub struct CheckContext {
values: HashMap<TypeId, Held>,
}
struct Held {
type_name: &'static str,
value: Arc<dyn Any + Send + Sync>,
}
impl CheckContext {
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 {}, so the check could not run; {}. \
StoreBuilder::context hands a value to every declared check",
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 CheckContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CheckContext")
.field("types", &self.on_offer())
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Invalid {
reason: Cow<'static, str>,
at: &'static [&'static str],
}
impl Invalid {
pub fn new(reason: impl Into<Cow<'static, str>>) -> Self {
Self {
reason: reason.into(),
at: &[],
}
}
pub fn at(mut self, fields: &'static [&'static str]) -> Self {
self.at = fields;
self
}
pub fn reason(&self) -> &str {
&self.reason
}
pub fn fields(&self) -> Option<&'static [&'static str]> {
if self.at.is_empty() {
None
} else {
Some(self.at)
}
}
}
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 fn refused_under(prefix: &StorePath, invalid: &Invalid) -> Report<StorageError> {
Report::new(StorageError::Read)
.attach(Prefix(prefix.clone()))
.attach(Refused(invalid.reason().to_string()))
}
pub fn refused_struct_or_kept(
prefix: &StorePath,
invalid: Invalid,
policy: OnUnreadable,
) -> Result<(), OpenStruct> {
match policy {
OnUnreadable::Refuse => Err(OpenStruct::Refused {
at: prefix.clone(),
said: Arc::from(invalid.reason()),
}),
OnUnreadable::UseDefault => {
tracing::error!(
target: "amethystate",
prefix = %prefix,
reason = %invalid,
"a declared check refused the loaded struct, so it was loaded as stored"
);
Ok(())
}
}
}
pub(crate) fn load_declared<TValue>(
store: &crate::Store,
at: &StorePath,
stored_as: crate::store::traits::StoredAs<TValue>,
check: Option<Check<TValue>>,
policy: OnUnreadable,
default: impl FnOnce() -> TValue,
) -> Result<TValue, OpenStruct>
where
TValue: serde::de::DeserializeOwned + '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(check) = check else {
return Ok(held);
};
match check(&mut held, store.context()) {
Ok(()) => Ok(held),
Err(invalid) => refused_or_default(at, invalid, policy, default()),
}
}
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)
.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 check refused the stored value, so the field was loaded on its default"
);
Ok(default)
}
}
}