use crate::db_string::DbString;
pub type CoreResult<T> = Result<T, CoreError>;
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
#[non_exhaustive]
pub enum CoreError {
#[error("string too long: {got} bytes (max {max})")]
#[diagnostic(code(SLENE_C_002))]
StringTooLong {
got: usize,
max: u32,
},
#[error("constructed value too large: {got} elements (max {max})")]
#[diagnostic(code(SLENE_C_003))]
ConstructedValueTooLarge {
got: usize,
max: u32,
},
#[error("decimal precision exceeded: {got} significant digits (max {max})")]
#[diagnostic(code(SLENE_C_004))]
DecimalPrecisionExceeded {
got: u32,
max: u32,
},
#[error("vector must contain at least one component")]
#[diagnostic(code(SLENE_C_010))]
VectorEmpty,
#[error("vector dimension too large: {got} components (max {max})")]
#[diagnostic(code(SLENE_C_011))]
VectorTooLarge {
got: usize,
max: usize,
},
#[error("vector component {index} is not finite: {value}")]
#[diagnostic(code(SLENE_C_012))]
VectorComponentNotFinite {
index: usize,
value: f32,
},
#[error("vector dimensions do not match: lhs has {lhs} components, rhs has {rhs}")]
#[diagnostic(code(SLENE_C_013))]
VectorDimensionMismatch {
lhs: usize,
rhs: usize,
},
#[error("cosine distance is undefined for zero-norm vector on {side}")]
#[diagnostic(code(SLENE_C_014))]
VectorZeroNorm {
side: &'static str,
},
#[error("invalid identifier: zero is reserved as tombstone sentinel")]
#[diagnostic(code(SLENE_C_007))]
ZeroIdentifier,
#[error("compact property map key/value length mismatch: {keys} keys, {values} values")]
#[diagnostic(code(SLENE_C_008))]
CompactKeyValueLengthMismatch {
keys: usize,
values: usize,
},
#[error("overlapping {kind} diff: key {key} appears in both add/set and remove")]
#[diagnostic(code(SLENE_C_009))]
OverlappingDiff {
kind: &'static str,
key: DbString,
},
#[error("invalid JSON text: {message}")]
#[diagnostic(code(SLENE_C_015))]
JsonParse {
message: String,
},
#[error("invalid JSON Patch: {message}")]
#[diagnostic(code(SLENE_C_016))]
JsonPatch {
message: String,
},
}
impl CoreError {
#[must_use]
pub const fn gqlstatus(&self) -> &'static str {
match self {
Self::StringTooLong { .. } | Self::ConstructedValueTooLarge { .. } => "22G03",
Self::DecimalPrecisionExceeded { .. } | Self::VectorComponentNotFinite { .. } => {
"22003"
}
Self::VectorEmpty | Self::VectorTooLarge { .. } => "22G03",
Self::VectorDimensionMismatch { .. } => "22G04",
Self::VectorZeroNorm { .. } => "22012",
Self::ZeroIdentifier => "0G003",
Self::CompactKeyValueLengthMismatch { .. } => "0G008",
Self::OverlappingDiff { .. } => "0G009",
Self::JsonParse { .. } => "22018",
Self::JsonPatch { .. } => "22G03",
}
}
}
#[cfg(test)]
mod tests {
use miette::Diagnostic;
use rstest::rstest;
use super::*;
#[rstest]
#[case(CoreError::StringTooLong { got: 2, max: 1 }, "22G03", "SLENE_C_002")]
#[case(
CoreError::ConstructedValueTooLarge { got: 2, max: 1 },
"22G03",
"SLENE_C_003"
)]
#[case(
CoreError::DecimalPrecisionExceeded { got: 29, max: 28 },
"22003",
"SLENE_C_004"
)]
#[case(CoreError::VectorEmpty, "22G03", "SLENE_C_010")]
#[case(
CoreError::VectorTooLarge { got: 65_536, max: 65_535 },
"22G03",
"SLENE_C_011"
)]
#[case(
CoreError::VectorComponentNotFinite { index: 1, value: f32::INFINITY },
"22003",
"SLENE_C_012"
)]
#[case(
CoreError::VectorDimensionMismatch { lhs: 2, rhs: 3 },
"22G04",
"SLENE_C_013"
)]
#[case(
CoreError::VectorZeroNorm { side: "lhs" },
"22012",
"SLENE_C_014"
)]
#[case(CoreError::ZeroIdentifier, "0G003", "SLENE_C_007")]
#[case(
CoreError::CompactKeyValueLengthMismatch { keys: 2, values: 1 },
"0G008",
"SLENE_C_008"
)]
#[case(
CoreError::OverlappingDiff { kind: "label", key: crate::db_string("err.test.overlap").unwrap() },
"0G009",
"SLENE_C_009"
)]
#[case(
CoreError::JsonParse { message: "expected value".to_owned() },
"22018",
"SLENE_C_015"
)]
#[case(
CoreError::JsonPatch { message: "missing op".to_owned() },
"22G03",
"SLENE_C_016"
)]
fn gqlstatus_and_diagnostic_code_match(
#[case] error: CoreError,
#[case] gqlstatus: &str,
#[case] diagnostic_code: &str,
) {
assert_eq!(error.gqlstatus(), gqlstatus);
assert!(
crate::gqlstatus_name(gqlstatus).is_some(),
"GQLSTATUS code {gqlstatus} emitted by CoreError but not in ALL_GQLSTATUS_NAMES"
);
assert_eq!(
error.code().map(|code| code.to_string()).as_deref(),
Some(diagnostic_code)
);
}
#[test]
fn display_includes_structured_field_values() {
let error = CoreError::StringTooLong { got: 7, max: 3 };
let rendered = error.to_string();
assert!(rendered.contains('7'));
assert!(rendered.contains('3'));
}
}