use rustc_hash::FxHashMap;
use serde_json::Value;
use std::collections::HashMap;
use thiserror::Error;
#[derive(Debug, Error)]
#[cfg_attr(feature = "diagnostics", derive(miette::Diagnostic))]
pub enum CollectionError {
#[error("Key '{key}' not found in collection")]
#[cfg_attr(
feature = "diagnostics",
diagnostic(code(weavegraph::collections::missing_key))
)]
MissingKey {
key: String,
},
#[error("Invalid type conversion for key '{key}': expected {expected}, found {found}")]
#[cfg_attr(
feature = "diagnostics",
diagnostic(code(weavegraph::collections::type_mismatch))
)]
TypeMismatch {
key: String,
expected: String,
found: String,
},
#[error("JSON operation failed: {source}")]
#[cfg_attr(
feature = "diagnostics",
diagnostic(code(weavegraph::collections::json))
)]
Json {
#[from]
source: serde_json::Error,
},
}
#[must_use]
#[inline]
pub fn new_extra_map() -> FxHashMap<String, Value> {
FxHashMap::default()
}
#[must_use]
#[inline]
pub fn new_extra_map_with_capacity(capacity: usize) -> FxHashMap<String, Value> {
FxHashMap::with_capacity_and_hasher(capacity, Default::default())
}
#[must_use]
pub fn extra_map_from_pairs<I, K, V>(pairs: I) -> FxHashMap<String, Value>
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<Value>,
{
pairs
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect()
}
#[must_use]
pub fn merge_extra_maps<'a, I>(maps: I) -> FxHashMap<String, Value>
where
I: IntoIterator<Item = &'a FxHashMap<String, Value>>,
{
let mut result = new_extra_map();
for map in maps {
result.extend(map.iter().map(|(k, v)| (k.clone(), v.clone())));
}
result
}
pub trait ExtraMapExt {
fn insert_string(&mut self, key: impl Into<String>, value: impl Into<String>);
fn insert_number(&mut self, key: impl Into<String>, value: impl Into<serde_json::Number>);
fn insert_bool(&mut self, key: impl Into<String>, value: bool);
fn insert_json<T: serde::Serialize>(
&mut self,
key: impl Into<String>,
value: T,
) -> Result<(), CollectionError>;
fn get_string(&self, key: &str) -> Result<String, CollectionError>;
fn get_number(&self, key: &str) -> Result<serde_json::Number, CollectionError>;
fn get_bool(&self, key: &str) -> Result<bool, CollectionError>;
fn get_typed<T: serde::de::DeserializeOwned>(&self, key: &str) -> Result<T, CollectionError>;
fn has_typed(&self, key: &str, expected_type: &str) -> bool;
}
impl ExtraMapExt for FxHashMap<String, Value> {
fn insert_string(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.insert(key.into(), Value::String(value.into()));
}
fn insert_number(&mut self, key: impl Into<String>, value: impl Into<serde_json::Number>) {
self.insert(key.into(), Value::Number(value.into()));
}
fn insert_bool(&mut self, key: impl Into<String>, value: bool) {
self.insert(key.into(), Value::Bool(value));
}
fn insert_json<T: serde::Serialize>(
&mut self,
key: impl Into<String>,
value: T,
) -> Result<(), CollectionError> {
let json_value = serde_json::to_value(value)?;
self.insert(key.into(), json_value);
Ok(())
}
fn get_string(&self, key: &str) -> Result<String, CollectionError> {
match self.get(key) {
Some(Value::String(s)) => Ok(s.clone()),
Some(other) => Err(CollectionError::TypeMismatch {
key: key.to_string(),
expected: "string".to_string(),
found: format!("{:?}", other),
}),
None => Err(CollectionError::MissingKey {
key: key.to_string(),
}),
}
}
fn get_number(&self, key: &str) -> Result<serde_json::Number, CollectionError> {
match self.get(key) {
Some(Value::Number(n)) => Ok(n.clone()),
Some(other) => Err(CollectionError::TypeMismatch {
key: key.to_string(),
expected: "number".to_string(),
found: format!("{:?}", other),
}),
None => Err(CollectionError::MissingKey {
key: key.to_string(),
}),
}
}
fn get_bool(&self, key: &str) -> Result<bool, CollectionError> {
match self.get(key) {
Some(Value::Bool(b)) => Ok(*b),
Some(other) => Err(CollectionError::TypeMismatch {
key: key.to_string(),
expected: "boolean".to_string(),
found: format!("{:?}", other),
}),
None => Err(CollectionError::MissingKey {
key: key.to_string(),
}),
}
}
fn get_typed<T: serde::de::DeserializeOwned>(&self, key: &str) -> Result<T, CollectionError> {
match self.get(key) {
Some(value) => serde_json::from_value(value.clone())
.map_err(|e| CollectionError::Json { source: e }),
None => Err(CollectionError::MissingKey {
key: key.to_string(),
}),
}
}
fn has_typed(&self, key: &str, expected_type: &str) -> bool {
matches!(
(self.get(key), expected_type),
(Some(Value::String(_)), "string")
| (Some(Value::Number(_)), "number")
| (Some(Value::Bool(_)), "bool")
| (Some(Value::Array(_)), "array")
| (Some(Value::Object(_)), "object")
| (Some(Value::Null), "null")
)
}
}
pub trait StringMapExt<V> {
fn get_or_default(&self, key: &str, default: V) -> V
where
V: Clone;
fn insert_or_update<F>(&mut self, key: String, value: V, update_fn: F)
where
F: FnOnce(&mut V);
}
impl<V> StringMapExt<V> for HashMap<String, V> {
fn get_or_default(&self, key: &str, default: V) -> V
where
V: Clone,
{
self.get(key).cloned().unwrap_or(default)
}
fn insert_or_update<F>(&mut self, key: String, value: V, update_fn: F)
where
F: FnOnce(&mut V),
{
self.entry(key).and_modify(update_fn).or_insert(value);
}
}
impl<V> StringMapExt<V> for FxHashMap<String, V> {
fn get_or_default(&self, key: &str, default: V) -> V
where
V: Clone,
{
self.get(key).cloned().unwrap_or(default)
}
fn insert_or_update<F>(&mut self, key: String, value: V, update_fn: F)
where
F: FnOnce(&mut V),
{
self.entry(key).and_modify(update_fn).or_insert(value);
}
}