use std::fmt; use super::{EntityId, EntityKind}; /// Top-level error type. Every public API method returns `Result`. #[derive(Debug)] pub enum LumenError { /// Storage engine failure. Retry may succeed. Storage(StorageError), /// Entity not found. Caller should handle. NotFound { kind: EntityKind, id: EntityId }, /// Schema violation. Caller's fault — fix the input. Schema(SchemaError), /// Signal write failed durability check. Retry required. Durability(DurabilityError), /// Query malformed. Parse error with details. Query(QueryError), /// Internal invariant violated. This is a bug in Lumen. Internal(String), } impl fmt::Display for LumenError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Storage(e) => write!(f, "storage error: {e}"), Self::NotFound { kind, id } => write!(f, "{kind} {id} not found"), Self::Schema(e) => write!(f, "{e}"), Self::Durability(e) => write!(f, "durability error: {e}"), Self::Query(e) => write!(f, "query error: {e}"), Self::Internal(msg) => write!(f, "internal error: {msg}"), } } } impl std::error::Error for LumenError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::Storage(e) => Some(e), Self::Schema(e) => Some(e), Self::Durability(e) => Some(e), Self::Query(e) => Some(e), Self::NotFound { .. } | Self::Internal(_) => None, } } } impl From for LumenError { fn from(e: SchemaError) -> Self { Self::Schema(e) } } impl From for LumenError { fn from(e: StorageError) -> Self { Self::Storage(e) } } impl From for LumenError { fn from(e: DurabilityError) -> Self { Self::Durability(e) } } impl From for LumenError { fn from(e: QueryError) -> Self { Self::Query(e) } } /// Schema validation errors. /// /// `Eq` is manually implemented because f64 fields (from `Duration::as_secs_f64()`) /// are always non-NaN, making equality reflexive. #[derive(Debug, Clone, PartialEq)] pub enum SchemaError { DuplicateSignalName(String), InvalidSignalName(String), InvalidHalfLife { signal_name: String, half_life_secs: f64, }, InvalidLifetime { signal_name: String, lifetime_secs: f64, }, EmptyWindows { signal_name: String, }, VelocityWithoutWindows { signal_name: String, }, NoSignalsDefined, } impl Eq for SchemaError {} impl fmt::Display for SchemaError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::DuplicateSignalName(name) => { write!(f, "duplicate signal name: '{name}'") } Self::InvalidSignalName(name) => { write!(f, "invalid signal name: '{name}'") } Self::InvalidHalfLife { signal_name, half_life_secs, } => { write!( f, "signal '{signal_name}': invalid half-life: {half_life_secs}s" ) } Self::InvalidLifetime { signal_name, lifetime_secs, } => { write!( f, "signal '{signal_name}': invalid lifetime: {lifetime_secs}s" ) } Self::EmptyWindows { signal_name } => { write!( f, "signal '{signal_name}': non-permanent signal requires at least one window" ) } Self::VelocityWithoutWindows { signal_name } => { write!( f, "signal '{signal_name}': velocity requires at least one window" ) } Self::NoSignalsDefined => f.write_str("schema must define at least one signal"), } } } impl std::error::Error for SchemaError {} /// Re-exported from `crate::storage::StorageError`. pub use crate::storage::StorageError; /// Stub for Phase 1.2+. #[derive(Debug)] pub struct DurabilityError { pub message: String, } impl fmt::Display for DurabilityError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.message) } } impl std::error::Error for DurabilityError {} /// Stub for Milestone 2+. #[derive(Debug)] pub struct QueryError { pub message: String, } impl fmt::Display for QueryError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.message) } } impl std::error::Error for QueryError {} #[cfg(test)] mod tests { use super::*; #[test] fn lumen_error_display_not_found() { let e = LumenError::NotFound { kind: EntityKind::Item, id: EntityId::new(42), }; assert_eq!(e.to_string(), "item 42 not found"); } #[test] fn lumen_error_display_schema() { let e = LumenError::Schema(SchemaError::DuplicateSignalName("view".into())); assert!(e.to_string().contains("duplicate signal name")); } #[test] fn lumen_error_display_internal() { let e = LumenError::Internal("something broke".into()); assert!(e.to_string().contains("internal error")); } #[test] fn lumen_error_display_storage() { let e = LumenError::Storage(StorageError::Corruption { message: "disk full".into(), }); assert!(e.to_string().contains("disk full")); } #[test] fn lumen_error_display_durability() { let e = LumenError::Durability(DurabilityError { message: "fsync failed".into(), }); assert!(e.to_string().contains("fsync failed")); } #[test] fn lumen_error_display_query() { let e = LumenError::Query(QueryError { message: "parse error".into(), }); assert!(e.to_string().contains("parse error")); } #[test] fn lumen_error_source_schema() { use std::error::Error; let e = LumenError::Schema(SchemaError::NoSignalsDefined); assert!(e.source().is_some()); } #[test] fn lumen_error_source_internal_is_none() { use std::error::Error; let e = LumenError::Internal("bug".into()); assert!(e.source().is_none()); } #[test] fn lumen_error_source_not_found_is_none() { use std::error::Error; let e = LumenError::NotFound { kind: EntityKind::User, id: EntityId::new(1), }; assert!(e.source().is_none()); } #[test] fn schema_error_converts_to_lumen_error() { let schema_err = SchemaError::NoSignalsDefined; let lumen_err: LumenError = schema_err.into(); assert!(matches!( lumen_err, LumenError::Schema(SchemaError::NoSignalsDefined) )); } #[test] fn storage_error_converts_to_lumen_error() { let e = StorageError::Closed; let lumen_err: LumenError = e.into(); assert!(matches!(lumen_err, LumenError::Storage(_))); } #[test] fn durability_error_converts_to_lumen_error() { let e = DurabilityError { message: "test".into(), }; let lumen_err: LumenError = e.into(); assert!(matches!(lumen_err, LumenError::Durability(_))); } #[test] fn query_error_converts_to_lumen_error() { let e = QueryError { message: "test".into(), }; let lumen_err: LumenError = e.into(); assert!(matches!(lumen_err, LumenError::Query(_))); } #[test] fn schema_error_display_all_variants() { assert_eq!( SchemaError::DuplicateSignalName("view".into()).to_string(), "duplicate signal name: 'view'" ); assert_eq!( SchemaError::InvalidSignalName("BAD".into()).to_string(), "invalid signal name: 'BAD'" ); assert!( SchemaError::InvalidHalfLife { signal_name: "s".into(), half_life_secs: 0.0, } .to_string() .contains("invalid half-life") ); assert!( SchemaError::InvalidLifetime { signal_name: "s".into(), lifetime_secs: -1.0, } .to_string() .contains("invalid lifetime") ); assert!( SchemaError::EmptyWindows { signal_name: "s".into() } .to_string() .contains("requires at least one window") ); assert!( SchemaError::VelocityWithoutWindows { signal_name: "s".into() } .to_string() .contains("velocity requires") ); assert_eq!( SchemaError::NoSignalsDefined.to_string(), "schema must define at least one signal" ); } #[test] fn schema_error_eq() { assert_eq!( SchemaError::DuplicateSignalName("a".into()), SchemaError::DuplicateSignalName("a".into()) ); assert_ne!( SchemaError::DuplicateSignalName("a".into()), SchemaError::DuplicateSignalName("b".into()) ); assert_ne!( SchemaError::NoSignalsDefined, SchemaError::DuplicateSignalName("a".into()) ); } }