#![forbid(unsafe_code)]
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
#[non_exhaustive]
pub enum IndexKind {
Standard = 0,
Unique = 1,
Each = 2,
Composite = 3,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct IndexSpec {
pub name: String,
pub kind: IndexKind,
pub key_paths: Vec<String>,
}
impl IndexSpec {
pub fn standard<N: Into<String>, P: Into<String>>(name: N, path: P) -> Result<Self> {
Self::scalar(IndexKind::Standard, name.into(), path.into())
}
pub fn unique<N: Into<String>, P: Into<String>>(name: N, path: P) -> Result<Self> {
Self::scalar(IndexKind::Unique, name.into(), path.into())
}
pub fn each<N: Into<String>, P: Into<String>>(name: N, path: P) -> Result<Self> {
Self::scalar(IndexKind::Each, name.into(), path.into())
}
pub fn composite<N: Into<String>>(name: N, paths: &[&str]) -> Result<Self> {
let owned: Vec<String> = paths.iter().map(|s| (*s).to_owned()).collect();
let spec = Self {
name: name.into(),
kind: IndexKind::Composite,
key_paths: owned,
};
spec.validate()?;
Ok(spec)
}
pub fn from_parts<N: Into<String>>(
name: N,
kind: IndexKind,
key_paths: Vec<String>,
) -> Result<Self> {
let spec = Self {
name: name.into(),
kind,
key_paths,
};
spec.validate()?;
Ok(spec)
}
pub fn validate(&self) -> Result<()> {
if self.name.is_empty() {
return Err(Error::InvalidArgument("index name must be non-empty"));
}
if self.key_paths.iter().any(String::is_empty) {
return Err(Error::InvalidArgument("index key path must be non-empty"));
}
match self.kind {
IndexKind::Standard | IndexKind::Unique | IndexKind::Each => {
if self.key_paths.len() != 1 {
return Err(Error::InvalidArgument(
"Standard/Unique/Each indexes require exactly one key path",
));
}
}
IndexKind::Composite => {
if self.key_paths.len() < 2 {
return Err(Error::InvalidArgument(
"Composite indexes require at least two key paths",
));
}
}
}
Ok(())
}
fn scalar(kind: IndexKind, name: String, path: String) -> Result<Self> {
let spec = Self {
name,
kind,
key_paths: vec![path],
};
spec.validate()?;
Ok(spec)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scalar_constructors_set_kind_and_single_path() {
let s = IndexSpec::standard("by_x", "x").expect("standard");
assert_eq!(s.kind, IndexKind::Standard);
assert_eq!(s.key_paths, vec!["x".to_owned()]);
let u = IndexSpec::unique("by_email", "email").expect("unique");
assert_eq!(u.kind, IndexKind::Unique);
assert_eq!(u.key_paths, vec!["email".to_owned()]);
let e = IndexSpec::each("by_tag", "tags").expect("each");
assert_eq!(e.kind, IndexKind::Each);
assert_eq!(e.key_paths, vec!["tags".to_owned()]);
}
#[test]
fn composite_requires_two_or_more_paths() {
let ok = IndexSpec::composite("by_ct", &["c", "t"]).expect("ok");
assert_eq!(ok.kind, IndexKind::Composite);
assert_eq!(ok.key_paths, vec!["c".to_owned(), "t".to_owned()]);
let err = IndexSpec::composite("by_one", &["only"]).expect_err("too few");
assert!(matches!(err, Error::InvalidArgument(_)));
}
#[test]
fn empty_name_or_path_rejected() {
let err = IndexSpec::standard("", "x").expect_err("empty name");
assert!(matches!(err, Error::InvalidArgument(_)));
let err = IndexSpec::standard("by_x", "").expect_err("empty path");
assert!(matches!(err, Error::InvalidArgument(_)));
let err = IndexSpec::composite("c", &["", "y"]).expect_err("empty middle path");
assert!(matches!(err, Error::InvalidArgument(_)));
}
#[test]
fn validate_idempotent() {
let s = IndexSpec::standard("by_x", "x").expect("ok");
s.validate().expect("re-validate");
s.validate().expect("re-validate again");
}
#[test]
fn postcard_round_trip() {
let s = IndexSpec::composite("by_ct", &["c", "t"]).expect("ok");
let bytes = postcard::to_allocvec(&s).expect("encode");
let back: IndexSpec = postcard::from_bytes(&bytes).expect("decode");
assert_eq!(s, back);
back.validate().expect("post-decode validate");
}
}