use std::collections::HashMap;
use std::path::Path;
use crate::LoadError;
#[derive(Debug, Clone)]
pub struct StageIdResolver {
by_id: HashMap<i32, usize>,
ids: Vec<i32>,
}
impl StageIdResolver {
#[must_use]
pub fn from_study_stage_ids(ids: &[i32]) -> Self {
let by_id = ids.iter().enumerate().map(|(i, &id)| (id, i)).collect();
Self {
by_id,
ids: ids.to_vec(),
}
}
#[must_use]
pub fn resolve(&self, stage_id: i32) -> Option<usize> {
self.by_id.get(&stage_id).copied()
}
#[must_use]
pub fn id_at(&self, index: usize) -> Option<i32> {
self.ids.get(index).copied()
}
#[must_use]
pub fn study_stage_ids(&self) -> &[i32] {
&self.ids
}
#[must_use]
pub fn index_map(&self) -> &HashMap<i32, usize> {
&self.by_id
}
pub fn unresolved_stage_id_error(
&self,
path: impl AsRef<Path>,
field: impl Into<String>,
stage_id: i32,
) -> LoadError {
LoadError::SchemaError {
path: path.as_ref().to_path_buf(),
field: field.into(),
message: format!(
"stage_id {stage_id} resolves to no declared study stage; \
declared study stage ids: {:?}",
self.ids
),
}
}
}
#[cfg(test)]
#[allow(clippy::panic)]
mod tests {
use super::*;
#[test]
fn stage_id_resolver_round_trips_and_rejects() {
let resolver = StageIdResolver::from_study_stage_ids(&[-2, 0, 3, 7]);
assert_eq!(resolver.resolve(-2), Some(0));
assert_eq!(resolver.resolve(0), Some(1));
assert_eq!(resolver.resolve(3), Some(2));
assert_eq!(resolver.resolve(7), Some(3));
assert_eq!(resolver.resolve(1), None);
assert_eq!(resolver.resolve(2), None);
for (i, &id) in resolver.study_stage_ids().iter().enumerate() {
assert_eq!(resolver.id_at(i), Some(id));
assert_eq!(resolver.resolve(id), Some(i));
}
assert_eq!(resolver.id_at(4), None);
let one_based = StageIdResolver::from_study_stage_ids(&[1, 2, 3]);
assert_eq!(one_based.resolve(1), Some(0));
assert_eq!(one_based.resolve(0), None);
}
#[test]
fn stage_id_resolver_index_map_matches_resolve() {
let resolver = StageIdResolver::from_study_stage_ids(&[-2, 0, 3, 7]);
let map = resolver.index_map();
for &id in resolver.study_stage_ids() {
assert_eq!(Some(map[&id]), resolver.resolve(id));
}
assert_eq!(map.len(), resolver.study_stage_ids().len());
}
#[test]
fn unresolved_stage_id_error_names_file_value_and_declared_set() {
let resolver = StageIdResolver::from_study_stage_ids(&[0, 1]);
let err = resolver.unresolved_stage_id_error(
"scenarios/noise_openings.parquet",
"noise_openings[3].stage_id",
5,
);
match &err {
LoadError::SchemaError {
path,
field,
message,
} => {
assert!(
path.to_string_lossy()
.contains("scenarios/noise_openings.parquet"),
"path names the file: {path:?}"
);
assert_eq!(field, "noise_openings[3].stage_id");
assert!(message.contains('5'), "message names the value: {message}");
assert!(
message.contains("[0, 1]"),
"message names the declared set: {message}"
);
}
other => panic!("expected SchemaError, got {other:?}"),
}
}
}