nodedb_array/schema/validation/
dims.rs1use std::collections::HashSet;
12
13use crate::error::{ArrayError, ArrayResult};
14use crate::schema::dim_spec::{DimSpec, DimType};
15use crate::types::domain::DomainBound;
16
17pub fn check(array: &str, dims: &[DimSpec]) -> ArrayResult<()> {
18 if dims.is_empty() {
19 return Err(ArrayError::InvalidSchema {
20 array: array.to_string(),
21 detail: "at least one dimension is required".to_string(),
22 });
23 }
24 let mut seen = HashSet::with_capacity(dims.len());
25 for d in dims {
26 if !seen.insert(d.name.as_str()) {
27 return Err(ArrayError::InvalidDim {
28 array: array.to_string(),
29 dim: d.name.clone(),
30 detail: "duplicate dimension name".to_string(),
31 });
32 }
33 check_bounds_match_type(array, d)?;
34 check_bound_order(array, d)?;
35 }
36 Ok(())
37}
38
39fn check_bounds_match_type(array: &str, d: &DimSpec) -> ArrayResult<()> {
40 let ok = matches!(
41 (&d.dtype, &d.domain.lo, &d.domain.hi),
42 (DimType::Int64, DomainBound::Int64(_), DomainBound::Int64(_))
43 | (
44 DimType::Float64,
45 DomainBound::Float64(_),
46 DomainBound::Float64(_),
47 )
48 | (
49 DimType::TimestampMs,
50 DomainBound::TimestampMs(_),
51 DomainBound::TimestampMs(_),
52 )
53 | (
54 DimType::String,
55 DomainBound::String(_),
56 DomainBound::String(_),
57 )
58 );
59 if !ok {
60 return Err(ArrayError::InvalidDim {
61 array: array.to_string(),
62 dim: d.name.clone(),
63 detail: "domain bound variant does not match declared dtype".to_string(),
64 });
65 }
66 Ok(())
67}
68
69fn check_bound_order(array: &str, d: &DimSpec) -> ArrayResult<()> {
70 let ordered = match (&d.domain.lo, &d.domain.hi) {
71 (DomainBound::Int64(lo), DomainBound::Int64(hi)) => lo <= hi,
72 (DomainBound::Float64(lo), DomainBound::Float64(hi)) => {
73 !lo.is_nan() && !hi.is_nan() && lo <= hi
74 }
75 (DomainBound::TimestampMs(lo), DomainBound::TimestampMs(hi)) => lo <= hi,
76 (DomainBound::String(lo), DomainBound::String(hi)) => lo <= hi,
77 _ => true,
78 };
79 if !ordered {
80 return Err(ArrayError::InvalidDim {
81 array: array.to_string(),
82 dim: d.name.clone(),
83 detail: "domain lo > hi (or non-finite float bound)".to_string(),
84 });
85 }
86 Ok(())
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use crate::types::domain::Domain;
93
94 fn int_dim(name: &str, lo: i64, hi: i64) -> DimSpec {
95 DimSpec::new(
96 name,
97 DimType::Int64,
98 Domain::new(DomainBound::Int64(lo), DomainBound::Int64(hi)),
99 )
100 }
101
102 #[test]
103 fn rejects_empty_dim_list() {
104 assert!(check("a", &[]).is_err());
105 }
106
107 #[test]
108 fn rejects_duplicate_dim_names() {
109 let dims = vec![int_dim("x", 0, 10), int_dim("x", 0, 10)];
110 assert!(check("a", &dims).is_err());
111 }
112
113 #[test]
114 fn rejects_lo_greater_than_hi() {
115 let dims = vec![int_dim("x", 10, 0)];
116 assert!(check("a", &dims).is_err());
117 }
118
119 #[test]
120 fn rejects_bound_type_mismatch() {
121 let d = DimSpec::new(
122 "x",
123 DimType::Int64,
124 Domain::new(DomainBound::Float64(0.0), DomainBound::Float64(10.0)),
125 );
126 assert!(check("a", &[d]).is_err());
127 }
128
129 #[test]
130 fn rejects_nan_float_bound() {
131 let d = DimSpec::new(
132 "x",
133 DimType::Float64,
134 Domain::new(DomainBound::Float64(f64::NAN), DomainBound::Float64(1.0)),
135 );
136 assert!(check("a", &[d]).is_err());
137 }
138
139 #[test]
140 fn accepts_well_formed_dims() {
141 let dims = vec![int_dim("chrom", 0, 24), int_dim("pos", 0, 300_000_000)];
142 assert!(check("a", &dims).is_ok());
143 }
144}