use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write as _;
use serde::Deserialize;
use crate::category::{Category, CodeError, NationalCategory, Reference};
pub const NONE: &str = "none";
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(untagged)]
pub enum Declared {
Word(String),
Codes(Vec<String>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Mapping {
Categories(BTreeSet<Category>),
NoCategory,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Table {
Templates,
Archetypes,
}
impl Table {
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::Templates => "templates",
Self::Archetypes => "archetypes",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum MapError {
#[error("a key of the {} table is empty or holds a control character", .0.name())]
Key(Table),
#[error("{} key {key:?} maps to a word other than \"none\"", .table.name())]
Word {
table: Table,
key: String,
},
#[error("{} key {key:?} maps to no category; write \"none\" for data of no category", .table.name())]
Empty {
table: Table,
key: String,
},
#[error("{} key {key:?} maps to {code:?}, which is neither a priority category nor a declared national one", .table.name())]
Unknown {
table: Table,
key: String,
code: String,
},
#[error("the national category {code:?} is refused")]
National {
code: String,
#[source]
source: CodeError,
},
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CategoryMap {
national: BTreeSet<NationalCategory>,
templates: BTreeMap<String, Mapping>,
archetypes: BTreeMap<String, Mapping>,
digest: Option<String>,
}
impl CategoryMap {
pub fn declare(
national: &[String],
templates: &BTreeMap<String, Declared>,
archetypes: &BTreeMap<String, Declared>,
) -> Result<Self, MapError> {
let national = national
.iter()
.map(|code| {
code.parse::<NationalCategory>()
.map_err(|source| MapError::National {
code: code.clone(),
source,
})
})
.collect::<Result<BTreeSet<_>, _>>()?;
let read = |table: Table, declared: &BTreeMap<String, Declared>| {
declared
.iter()
.map(|(key, declared)| Ok((key.clone(), mapping(table, key, declared, &national)?)))
.collect::<Result<BTreeMap<_, _>, MapError>>()
};
Ok(Self {
templates: read(Table::Templates, templates)?,
archetypes: read(Table::Archetypes, archetypes)?,
national,
digest: None,
})
}
#[must_use]
pub fn with_digest(mut self, digest: String) -> Self {
self.digest = Some(digest);
self
}
#[must_use]
pub fn digest(&self) -> Option<&str> {
self.digest.as_deref()
}
#[must_use]
pub fn template(&self, template_id: &str) -> Option<&Mapping> {
self.templates.get(template_id)
}
#[must_use]
pub fn archetype(&self, archetype_id: &str) -> Option<&Mapping> {
self.archetypes.get(archetype_id)
}
#[must_use]
pub fn category(&self, reference: &str) -> Option<Category> {
named(reference, &self.national)
}
pub fn categories(&self) -> impl Iterator<Item = Category> + '_ {
Category::PRIORITY
.into_iter()
.chain(self.national.iter().cloned().map(Category::National))
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.templates.is_empty() && self.archetypes.is_empty()
}
#[must_use]
pub fn canonical(&self) -> String {
let mut text = String::new();
for national in &self.national {
let _written = writeln!(text, "national\t{}|{}", national.system(), national.code());
}
for (table, keys) in [
(Table::Templates, &self.templates),
(Table::Archetypes, &self.archetypes),
] {
for (key, mapping) in keys {
let value = match mapping {
Mapping::NoCategory => NONE.to_owned(),
Mapping::Categories(categories) => categories
.iter()
.map(Category::token)
.collect::<Vec<_>>()
.join(","),
};
let _written = writeln!(text, "{}\t{key}\t{value}", table.name());
}
}
text
}
}
fn mapping(
table: Table,
key: &str,
declared: &Declared,
national: &BTreeSet<NationalCategory>,
) -> Result<Mapping, MapError> {
if key.is_empty() || key.chars().any(char::is_control) {
return Err(MapError::Key(table));
}
let codes = match declared {
Declared::Word(word) if word == NONE => return Ok(Mapping::NoCategory),
Declared::Word(_) => {
return Err(MapError::Word {
table,
key: key.to_owned(),
});
}
Declared::Codes(codes) if codes.is_empty() => {
return Err(MapError::Empty {
table,
key: key.to_owned(),
});
}
Declared::Codes(codes) => codes,
};
codes
.iter()
.map(|code| {
named(code, national).ok_or_else(|| MapError::Unknown {
table,
key: key.to_owned(),
code: code.clone(),
})
})
.collect::<Result<BTreeSet<_>, _>>()
.map(Mapping::Categories)
}
fn named(reference: &str, national: &BTreeSet<NationalCategory>) -> Option<Category> {
let reference = Reference::read(reference);
reference.priority().or_else(|| {
national
.iter()
.find(|declared| reference.names(declared))
.map(|declared| Category::National(declared.clone()))
})
}