mod types;
use std::collections::HashMap;
use std::fs;
use std::sync::Arc;
use async_trait::async_trait;
use serde_json::Value;
pub use types::*;
use crate::error::{Result, TinyAgentsError};
impl InMemoryStore {
pub fn new() -> Self {
Self::default()
}
}
#[async_trait]
impl Store for InMemoryStore {
async fn get(&self, namespace: &str, key: &str) -> Result<Option<Value>> {
let data = self
.data
.lock()
.map_err(|e| TinyAgentsError::Validation(format!("store lock poisoned: {e}")))?;
Ok(data.get(namespace).and_then(|ns| ns.get(key)).cloned())
}
async fn put(&self, namespace: &str, key: &str, value: Value) -> Result<()> {
let mut data = self
.data
.lock()
.map_err(|e| TinyAgentsError::Validation(format!("store lock poisoned: {e}")))?;
data.entry(namespace.to_string())
.or_default()
.insert(key.to_string(), value);
Ok(())
}
async fn delete(&self, namespace: &str, key: &str) -> Result<()> {
let mut data = self
.data
.lock()
.map_err(|e| TinyAgentsError::Validation(format!("store lock poisoned: {e}")))?;
if let Some(ns) = data.get_mut(namespace) {
ns.remove(key);
}
Ok(())
}
async fn list(&self, namespace: &str) -> Result<Vec<String>> {
let data = self
.data
.lock()
.map_err(|e| TinyAgentsError::Validation(format!("store lock poisoned: {e}")))?;
Ok(data
.get(namespace)
.map(|ns| ns.keys().cloned().collect())
.unwrap_or_default())
}
}
impl FileStore {
pub fn new(root_dir: impl Into<std::path::PathBuf>) -> Self {
Self {
root_dir: root_dir.into(),
}
}
fn sanitize(name: &str) -> Result<()> {
if name.is_empty() {
return Err(TinyAgentsError::Validation(
"store namespace and key must not be empty".into(),
));
}
if name.bytes().all(|b| b == b'.') {
return Err(TinyAgentsError::Validation(format!(
"store name must not be all dots: {name:?} (path-traversal guard)"
)));
}
if name
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b'.')
{
Ok(())
} else {
Err(TinyAgentsError::Validation(format!(
"store name contains invalid characters: {name:?} \
(only ASCII alphanumerics, hyphens, underscores, dots allowed)"
)))
}
}
fn key_path(&self, namespace: &str, key: &str) -> std::path::PathBuf {
self.root_dir.join(namespace).join(format!("{key}.json"))
}
}
#[async_trait]
impl Store for FileStore {
async fn get(&self, namespace: &str, key: &str) -> Result<Option<Value>> {
Self::sanitize(namespace)?;
Self::sanitize(key)?;
let path = self.key_path(namespace, key);
if !path.exists() {
return Ok(None);
}
let bytes = fs::read(&path)
.map_err(|e| TinyAgentsError::Validation(format!("store read error: {e}")))?;
let value: Value = serde_json::from_slice(&bytes)?;
Ok(Some(value))
}
async fn put(&self, namespace: &str, key: &str, value: Value) -> Result<()> {
Self::sanitize(namespace)?;
Self::sanitize(key)?;
let dir = self.root_dir.join(namespace);
fs::create_dir_all(&dir)
.map_err(|e| TinyAgentsError::Validation(format!("store mkdir error: {e}")))?;
let path = dir.join(format!("{key}.json"));
let bytes = serde_json::to_vec_pretty(&value)?;
fs::write(&path, &bytes)
.map_err(|e| TinyAgentsError::Validation(format!("store write error: {e}")))?;
Ok(())
}
async fn delete(&self, namespace: &str, key: &str) -> Result<()> {
Self::sanitize(namespace)?;
Self::sanitize(key)?;
let path = self.key_path(namespace, key);
if path.exists() {
fs::remove_file(&path)
.map_err(|e| TinyAgentsError::Validation(format!("store delete error: {e}")))?;
}
Ok(())
}
async fn list(&self, namespace: &str) -> Result<Vec<String>> {
Self::sanitize(namespace)?;
let dir = self.root_dir.join(namespace);
if !dir.exists() {
return Ok(Vec::new());
}
let entries = fs::read_dir(&dir)
.map_err(|e| TinyAgentsError::Validation(format!("store readdir error: {e}")))?;
let mut keys = Vec::new();
for entry in entries {
let entry = entry
.map_err(|e| TinyAgentsError::Validation(format!("store entry error: {e}")))?;
let file_name = entry.file_name();
let name = file_name.to_string_lossy();
if let Some(stem) = name.strip_suffix(".json") {
keys.push(stem.to_string());
}
}
Ok(keys)
}
}
impl StoreRegistry {
pub fn new() -> Self {
Self {
stores: HashMap::new(),
default_store: Arc::new(InMemoryStore::new()),
}
}
pub fn register(&mut self, name: impl Into<String>, store: Arc<dyn Store>) -> &mut Self {
self.stores.insert(name.into(), store);
self
}
pub fn get(&self, name: &str) -> Option<Arc<dyn Store>> {
self.stores.get(name).cloned()
}
pub fn default_store(&self) -> Arc<dyn Store> {
Arc::clone(&self.default_store)
}
}
impl Default for StoreRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod test;