Refactor session ID handling and improve error management
- Introduced `SessionId` newtype for validated session identifiers, ensuring safety against path traversal attacks. - Updated session repository methods to accept `SessionId` instead of raw strings, enhancing type safety. - Removed redundant error handling in repository methods by leveraging the new `Error` type from `zesdex_utils`. - Simplified atomic JSON write operations by eliminating unnecessary error conversions. - Enhanced integer casting with a new `CastOr` trait for safer narrowing conversions. - Removed deprecated error handling code and consolidated error types across the codebase. - Updated HTTP handlers to utilize the new session ID validation, improving overall robustness.
This commit is contained in:
@@ -58,29 +58,21 @@ impl<R: ConversationRepository> ConversationServiceImpl<R> {
|
||||
impl<R: ConversationRepository> ConversationService for ConversationServiceImpl<R> {
|
||||
/// Load a conversation from disk for the given session.
|
||||
///
|
||||
/// Flow: resolve session dir → delegate to repo.load() → wrap error with context.
|
||||
/// Flow: resolve session dir → delegate to repo.load().
|
||||
fn load_conversation(&self, session_id: &str) -> Result<Conversation, ServiceError> {
|
||||
tracing::debug!("loading conversation for session {session_id}");
|
||||
let dir = self.session_dir(session_id);
|
||||
self.repo.load(&dir).map_err(|e| {
|
||||
ServiceError::Other(format!(
|
||||
"failed to load conversation for session '{session_id}': {e}"
|
||||
))
|
||||
})
|
||||
self.repo.load(&dir).map_err(ServiceError::Repository)
|
||||
}
|
||||
|
||||
/// Persist a conversation to disk.
|
||||
///
|
||||
/// Flow: resolve session dir from conv.session_id → delegate to repo.save() → wrap error.
|
||||
/// Flow: resolve session dir from conv.session_id → delegate to repo.save().
|
||||
fn save_conversation(&self, conv: &Conversation) -> Result<(), ServiceError> {
|
||||
tracing::debug!("saving conversation for session {}", conv.session_id);
|
||||
let dir = self.session_dir(&conv.session_id);
|
||||
self.repo.save(&dir, conv).map_err(|e| {
|
||||
ServiceError::Other(format!(
|
||||
"failed to save conversation for session '{}': {e}",
|
||||
conv.session_id
|
||||
))
|
||||
})
|
||||
self.repo.save(&dir, conv)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a message to a conversation and persist immediately.
|
||||
@@ -94,11 +86,7 @@ impl<R: ConversationRepository> ConversationService for ConversationServiceImpl<
|
||||
tracing::debug!("adding message to session {}", conv.session_id);
|
||||
conv.push(msg); // append message to in-memory conversation
|
||||
let dir = self.session_dir(&conv.session_id);
|
||||
self.repo.save(&dir, conv).map_err(|e| {
|
||||
ServiceError::Other(format!(
|
||||
"failed to persist conversation after adding message for session '{}': {e}",
|
||||
conv.session_id
|
||||
))
|
||||
})
|
||||
self.repo.save(&dir, conv)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,31 +51,27 @@ impl<R: MemoryRepository> MemoryServiceImpl<R> {
|
||||
impl<R: MemoryRepository> MemoryService for MemoryServiceImpl<R> {
|
||||
/// List all stored memory names.
|
||||
///
|
||||
/// Flow: delegate to repo.list() → wrap error with context.
|
||||
/// Flow: delegate to repo.list().
|
||||
fn list_memories(&self) -> Result<Vec<String>, ServiceError> {
|
||||
tracing::debug!("listing memories from {:?}", self.memory_dir);
|
||||
self.repo.list(&self.memory_dir).map_err(|e| {
|
||||
ServiceError::Other(format!("failed to list memories: {e}"))
|
||||
})
|
||||
self.repo.list(&self.memory_dir).map_err(ServiceError::Repository)
|
||||
}
|
||||
|
||||
/// Persist a memory to disk.
|
||||
///
|
||||
/// Flow: delegate to repo.save() → wrap error with memory name context.
|
||||
/// Flow: delegate to repo.save().
|
||||
fn save_memory(&self, memory: &Memory) -> Result<(), ServiceError> {
|
||||
tracing::debug!("saving memory '{}'", memory.name);
|
||||
self.repo.save(&self.memory_dir, memory).map_err(|e| {
|
||||
ServiceError::Other(format!("failed to save memory '{}': {e}", memory.name))
|
||||
ServiceError::Repository(e)
|
||||
})
|
||||
}
|
||||
|
||||
/// Delete a memory by name.
|
||||
///
|
||||
/// Flow: delegate to repo.delete() → wrap error with memory name context.
|
||||
/// Flow: delegate to repo.delete().
|
||||
fn delete_memory(&self, name: &str) -> Result<(), ServiceError> {
|
||||
tracing::debug!("deleting memory '{name}'");
|
||||
self.repo.delete(&self.memory_dir, name).map_err(|e| {
|
||||
ServiceError::Other(format!("failed to delete memory '{name}': {e}"))
|
||||
})
|
||||
self.repo.delete(&self.memory_dir, name).map_err(ServiceError::Repository)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,9 +64,7 @@ impl<S: SettingsRepository, C: AppConfigRepository> SettingsService for Settings
|
||||
/// Flow: delegate to settings_repo.load() at base_dir.
|
||||
fn load_settings(&self) -> Result<Settings, ServiceError> {
|
||||
tracing::debug!("loading settings");
|
||||
self.settings_repo.load(&self.base_dir).map_err(|e| {
|
||||
ServiceError::Other(format!("failed to load settings: {e}"))
|
||||
})
|
||||
self.settings_repo.load(&self.base_dir).map_err(ServiceError::Repository)
|
||||
}
|
||||
|
||||
/// Save application settings to disk.
|
||||
@@ -74,9 +72,8 @@ impl<S: SettingsRepository, C: AppConfigRepository> SettingsService for Settings
|
||||
/// Flow: delegate to settings_repo.save() at base_dir.
|
||||
fn save_settings(&self, settings: &Settings) -> Result<(), ServiceError> {
|
||||
tracing::debug!("saving settings");
|
||||
self.settings_repo.save(&self.base_dir, settings).map_err(|e| {
|
||||
ServiceError::Other(format!("failed to save settings: {e}"))
|
||||
})
|
||||
self.settings_repo.save(&self.base_dir, settings)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update (or insert) a provider configuration in the app config.
|
||||
@@ -96,8 +93,7 @@ impl<S: SettingsRepository, C: AppConfigRepository> SettingsService for Settings
|
||||
.providers
|
||||
.insert(name.to_string(), config.clone());
|
||||
// Persist the modified app config
|
||||
self.app_config_repo.save(&self.base_dir, &app_config).map_err(|e| {
|
||||
ServiceError::Other(format!("failed to update provider '{name}': {e}"))
|
||||
})
|
||||
self.app_config_repo.save(&self.base_dir, &app_config)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,116 +1,41 @@
|
||||
//! Domain error types for the CMS crate.
|
||||
//!
|
||||
//! Typed error enums for repository and service operations.
|
||||
//! `From` impls connect `std::io::Error` and `serde_json::Error` into
|
||||
//! `RepositoryError`, and `RepositoryError` into `ServiceError`.
|
||||
//! The `RepositoryError` type is re-exported from `zesdex_utils::Error`,
|
||||
//! which has all needed variants: `NotFound`, `Conflict`, `Io`, `Serde`,
|
||||
//! `InvalidId`, `Other`, etc.
|
||||
//!
|
||||
//! `From` impls are generated by `thiserror::Error` derive macros.
|
||||
//! Anyhow's blanket `From<E: StdError + Send + Sync + 'static>`
|
||||
//! covers conversion to `anyhow::Error` for downstream code.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RepositoryError
|
||||
// RepositoryError (type alias)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Errors from repository / persistence operations in the CMS domain.
|
||||
#[derive(Debug)]
|
||||
pub enum RepositoryError {
|
||||
/// The requested entity does not exist.
|
||||
NotFound(String),
|
||||
/// A conflict occurred (e.g. duplicate entry).
|
||||
Conflict(String),
|
||||
/// An I/O error occurred during persistence.
|
||||
Io(std::io::Error),
|
||||
/// A serialisation / deserialisation error occurred.
|
||||
Serialization(serde_json::Error),
|
||||
/// The supplied identifier is invalid (e.g. path traversal attempt).
|
||||
InvalidId(String),
|
||||
/// An error that could not be downcast to a specific variant.
|
||||
Other(String),
|
||||
}
|
||||
/// Re-export shared repository error from `zesdex_utils`.
|
||||
pub use zesdex_utils::Error as RepositoryError;
|
||||
|
||||
impl RepositoryError {
|
||||
/// Convert an `anyhow::Error` to `RepositoryError` by attempting
|
||||
/// downcast to known inner types.
|
||||
pub fn from_anyhow(e: anyhow::Error) -> Self {
|
||||
if let Some(ioe) = e.downcast_ref::<std::io::Error>() {
|
||||
return RepositoryError::Io(std::io::Error::new(ioe.kind(), ioe.to_string()));
|
||||
}
|
||||
RepositoryError::Other(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RepositoryError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
RepositoryError::NotFound(msg) => write!(f, "not found: {msg}"),
|
||||
RepositoryError::Conflict(msg) => write!(f, "conflict: {msg}"),
|
||||
RepositoryError::Io(e) => write!(f, "I/O error: {e}"),
|
||||
RepositoryError::Serialization(e) => write!(f, "serialization error: {e}"),
|
||||
RepositoryError::InvalidId(msg) => write!(f, "invalid id: {msg}"),
|
||||
RepositoryError::Other(msg) => write!(f, "{msg}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RepositoryError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
RepositoryError::Io(e) => Some(e),
|
||||
RepositoryError::Serialization(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for RepositoryError {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
RepositoryError::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for RepositoryError {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
RepositoryError::Serialization(e)
|
||||
}
|
||||
}
|
||||
// `From<RepositoryError> for anyhow::Error` is covered by anyhow's blanket
|
||||
// `impl<E: StdError + Send + Sync + 'static> From<E> for Error` — no
|
||||
// explicit impl needed.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ServiceError
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Errors from service / use-case operations in the CMS domain.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ServiceError {
|
||||
/// A repository operation failed.
|
||||
Repository(RepositoryError),
|
||||
#[error("repository error: {0}")]
|
||||
Repository(#[from] RepositoryError),
|
||||
/// The provided input is invalid.
|
||||
#[error("invalid input: {0}")]
|
||||
InvalidInput(String),
|
||||
/// A generic error with a message.
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for ServiceError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ServiceError::Repository(e) => write!(f, "repository error: {e}"),
|
||||
ServiceError::InvalidInput(msg) => write!(f, "invalid input: {msg}"),
|
||||
ServiceError::Other(msg) => write!(f, "{msg}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ServiceError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
ServiceError::Repository(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RepositoryError> for ServiceError {
|
||||
fn from(e: RepositoryError) -> Self {
|
||||
ServiceError::Repository(e)
|
||||
}
|
||||
}
|
||||
// `From<ServiceError> for anyhow::Error` is covered by anyhow's blanket impl.
|
||||
|
||||
@@ -33,7 +33,6 @@ use super::dto::{MemoryCreateRequest, MemoryResponse, SettingsResponse, Settings
|
||||
/// Flow: load settings from service → convert to DTO → return.
|
||||
#[instrument(skip(service))]
|
||||
pub fn handle_get_settings<S: SettingsService>(service: &S) -> Result<SettingsResponse> {
|
||||
tracing::debug!("handling GET /settings");
|
||||
let settings = service.load_settings().context("failed to load settings")?;
|
||||
Ok(SettingsResponse::from(settings))
|
||||
}
|
||||
@@ -52,7 +51,6 @@ pub fn handle_update_settings<S: SettingsService>(
|
||||
service: &S,
|
||||
req: SettingsUpdateRequest,
|
||||
) -> Result<SettingsResponse> {
|
||||
tracing::debug!("handling PUT /settings");
|
||||
// Load current settings as baseline for partial update
|
||||
let mut settings: Settings = service
|
||||
.load_settings()
|
||||
@@ -134,7 +132,6 @@ pub fn handle_update_settings<S: SettingsService>(
|
||||
/// Flow: list slugs from service → map each to minimal MemoryResponse → return.
|
||||
#[instrument(skip(service))]
|
||||
pub fn handle_list_memories<M: MemoryService>(service: &M) -> Result<Vec<MemoryResponse>> {
|
||||
tracing::debug!("handling GET /memories");
|
||||
let slugs = service.list_memories().context("failed to list memories")?;
|
||||
|
||||
// We can't load individual memories without a load_memory method on the
|
||||
@@ -174,7 +171,6 @@ pub fn handle_create_memory<M: MemoryService>(
|
||||
service: &M,
|
||||
req: MemoryCreateRequest,
|
||||
) -> Result<MemoryResponse> {
|
||||
tracing::debug!("handling POST /memories for '{}'", req.name);
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
let memory = Memory {
|
||||
name: req.name,
|
||||
|
||||
@@ -156,8 +156,7 @@ impl AppConfigRepository for JsonAppConfigRepository {
|
||||
tracing::debug!("saving app_config to {base_dir:?}");
|
||||
std::fs::create_dir_all(base_dir)?;
|
||||
let path = base_dir.join("app_config.json");
|
||||
write_json_atomic(&path, config, None)
|
||||
.map_err(RepositoryError::from_anyhow)?;
|
||||
write_json_atomic(&path, config, None)?;
|
||||
tracing::debug!("app_config saved to '{}'", path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -46,8 +46,7 @@ impl ConversationRepository for JsonConversationRepository {
|
||||
tracing::debug!("saving conversation to {session_dir:?}");
|
||||
std::fs::create_dir_all(session_dir)?;
|
||||
let path = session_dir.join("conversation.json");
|
||||
write_json_atomic(&path, conversation, None)
|
||||
.map_err(RepositoryError::from_anyhow)?;
|
||||
write_json_atomic(&path, conversation, None)?;
|
||||
tracing::debug!("conversation saved to '{}'", path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -65,8 +65,7 @@ impl SettingsRepository for JsonSettingsRepository {
|
||||
fn save(&self, base_dir: &Path, settings: &Settings) -> Result<(), RepositoryError> {
|
||||
std::fs::create_dir_all(base_dir)?;
|
||||
let path = base_dir.join("settings.json");
|
||||
write_json_atomic(&path, settings, None)
|
||||
.map_err(RepositoryError::from_anyhow)?;
|
||||
write_json_atomic(&path, settings, None)?;
|
||||
tracing::debug!("settings saved to '{}'", path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user