use std::sync::Arc;
use axioval_ir::{ObjectId, SourceId};
use thiserror::Error;
use crate::{SnapshotBoundService, SourceSnapshot};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ClassificationAssignment {
pub system: Option<String>,
pub codes: Vec<Option<String>>,
}
impl ClassificationAssignment {
#[must_use]
pub fn matches(&self, system: &str, code: &str, include_descendants: bool) -> Option<bool> {
self.matches_code(system, |level| level == code, include_descendants)
}
#[must_use]
pub fn matches_code(
&self,
system: &str,
code: impl Fn(&str) -> bool,
include_descendants: bool,
) -> Option<bool> {
if !self.in_system(system)? {
return Some(false);
}
let levels = if include_descendants {
&self.codes[..]
} else {
&self.codes[..self.codes.len().min(1)]
};
Some(levels.iter().flatten().any(|level| code(level)))
}
#[must_use]
pub fn in_system(&self, system: &str) -> Option<bool> {
Some(self.system.as_deref()? == system)
}
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum ClassificationError {
#[error("classification service does not cover source `{0}`")]
UncoveredSource(SourceId),
#[error("object `{0}` is not in the classified source")]
UnknownObject(ObjectId),
#[error("classifications cannot be read exactly: {0}")]
Unreadable(String),
}
pub trait ClassificationService: Send + Sync {
fn source_snapshots(&self) -> &[SourceSnapshot];
fn classifications(
&self,
object: &ObjectId,
) -> Result<Vec<ClassificationAssignment>, ClassificationError>;
}
#[derive(Clone)]
pub struct ClassificationServiceHandle(Arc<dyn ClassificationService>);
impl ClassificationServiceHandle {
#[must_use]
pub fn new(service: Arc<dyn ClassificationService>) -> Self {
Self(service)
}
pub fn classifications(
&self,
object: &ObjectId,
) -> Result<Vec<ClassificationAssignment>, ClassificationError> {
if !self
.0
.source_snapshots()
.iter()
.any(|snapshot| *snapshot.source() == object.source)
{
return Err(ClassificationError::UncoveredSource(object.source.clone()));
}
self.0.classifications(object)
}
}
impl SnapshotBoundService for ClassificationServiceHandle {
fn source_snapshots(&self) -> &[SourceSnapshot] {
self.0.source_snapshots()
}
}
#[cfg(test)]
mod tests {
use super::ClassificationAssignment;
fn chain(system: Option<&str>, codes: &[Option<&str>]) -> ClassificationAssignment {
ClassificationAssignment {
system: system.map(str::to_owned),
codes: codes.iter().map(|code| code.map(str::to_owned)).collect(),
}
}
#[test]
fn the_leaf_matches_and_ancestors_only_with_descendants() {
let item = chain(Some("DIN 276"), &[Some("331"), Some("330"), Some("300")]);
assert_eq!(item.matches("DIN 276", "331", false), Some(true));
assert_eq!(item.matches("DIN 276", "330", false), Some(false));
assert_eq!(item.matches("DIN 276", "330", true), Some(true));
assert_eq!(item.matches("Uniclass", "331", true), Some(false));
}
#[test]
fn an_unknown_system_is_undecided_not_a_mismatch() {
let item = chain(None, &[Some("331")]);
assert_eq!(item.matches("DIN 276", "331", false), None);
}
#[test]
fn an_unstated_code_never_matches() {
let item = chain(Some("DIN 276"), &[None, Some("330")]);
assert_eq!(item.matches("DIN 276", "331", true), Some(false));
assert_eq!(item.matches("DIN 276", "330", false), Some(false));
}
}