1use std::collections::{BTreeMap, BTreeSet};
20use std::fmt::Write as _;
21
22use serde::Deserialize;
23
24use crate::category::{Category, CodeError, NationalCategory, Reference};
25
26pub const NONE: &str = "none";
28
29#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
32#[serde(untagged)]
33pub enum Declared {
34 Word(String),
36 Codes(Vec<String>),
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum Mapping {
44 Categories(BTreeSet<Category>),
46 NoCategory,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum Table {
53 Templates,
55 Archetypes,
57}
58
59impl Table {
60 #[must_use]
62 pub fn name(self) -> &'static str {
63 match self {
64 Self::Templates => "templates",
65 Self::Archetypes => "archetypes",
66 }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
72#[non_exhaustive]
73pub enum MapError {
74 #[error("a key of the {} table is empty or holds a control character", .0.name())]
76 Key(Table),
77 #[error("{} key {key:?} maps to a word other than \"none\"", .table.name())]
79 Word {
80 table: Table,
82 key: String,
84 },
85 #[error("{} key {key:?} maps to no category; write \"none\" for data of no category", .table.name())]
87 Empty {
88 table: Table,
90 key: String,
92 },
93 #[error("{} key {key:?} maps to {code:?}, which is neither a priority category nor a declared national one", .table.name())]
96 Unknown {
97 table: Table,
99 key: String,
101 code: String,
103 },
104 #[error("the national category {code:?} is refused")]
106 National {
107 code: String,
109 #[source]
111 source: CodeError,
112 },
113}
114
115#[derive(Debug, Clone, Default, PartialEq, Eq)]
117pub struct CategoryMap {
118 national: BTreeSet<NationalCategory>,
119 templates: BTreeMap<String, Mapping>,
120 archetypes: BTreeMap<String, Mapping>,
121 digest: Option<String>,
122}
123
124impl CategoryMap {
125 pub fn declare(
135 national: &[String],
136 templates: &BTreeMap<String, Declared>,
137 archetypes: &BTreeMap<String, Declared>,
138 ) -> Result<Self, MapError> {
139 let national = national
140 .iter()
141 .map(|code| {
142 code.parse::<NationalCategory>()
143 .map_err(|source| MapError::National {
144 code: code.clone(),
145 source,
146 })
147 })
148 .collect::<Result<BTreeSet<_>, _>>()?;
149 let read = |table: Table, declared: &BTreeMap<String, Declared>| {
150 declared
151 .iter()
152 .map(|(key, declared)| Ok((key.clone(), mapping(table, key, declared, &national)?)))
153 .collect::<Result<BTreeMap<_, _>, MapError>>()
154 };
155 Ok(Self {
156 templates: read(Table::Templates, templates)?,
157 archetypes: read(Table::Archetypes, archetypes)?,
158 national,
159 digest: None,
160 })
161 }
162
163 #[must_use]
166 pub fn with_digest(mut self, digest: String) -> Self {
167 self.digest = Some(digest);
168 self
169 }
170
171 #[must_use]
173 pub fn digest(&self) -> Option<&str> {
174 self.digest.as_deref()
175 }
176
177 #[must_use]
179 pub fn template(&self, template_id: &str) -> Option<&Mapping> {
180 self.templates.get(template_id)
181 }
182
183 #[must_use]
185 pub fn archetype(&self, archetype_id: &str) -> Option<&Mapping> {
186 self.archetypes.get(archetype_id)
187 }
188
189 #[must_use]
193 pub fn category(&self, reference: &str) -> Option<Category> {
194 named(reference, &self.national)
195 }
196
197 pub fn categories(&self) -> impl Iterator<Item = Category> + '_ {
200 Category::PRIORITY
201 .into_iter()
202 .chain(self.national.iter().cloned().map(Category::National))
203 }
204
205 #[must_use]
207 pub fn is_empty(&self) -> bool {
208 self.templates.is_empty() && self.archetypes.is_empty()
209 }
210
211 #[must_use]
216 pub fn canonical(&self) -> String {
217 let mut text = String::new();
218 for national in &self.national {
219 let _written = writeln!(text, "national\t{}|{}", national.system(), national.code());
220 }
221 for (table, keys) in [
222 (Table::Templates, &self.templates),
223 (Table::Archetypes, &self.archetypes),
224 ] {
225 for (key, mapping) in keys {
226 let value = match mapping {
227 Mapping::NoCategory => NONE.to_owned(),
228 Mapping::Categories(categories) => categories
229 .iter()
230 .map(Category::token)
231 .collect::<Vec<_>>()
232 .join(","),
233 };
234 let _written = writeln!(text, "{}\t{key}\t{value}", table.name());
235 }
236 }
237 text
238 }
239}
240
241fn mapping(
243 table: Table,
244 key: &str,
245 declared: &Declared,
246 national: &BTreeSet<NationalCategory>,
247) -> Result<Mapping, MapError> {
248 if key.is_empty() || key.chars().any(char::is_control) {
249 return Err(MapError::Key(table));
250 }
251 let codes = match declared {
252 Declared::Word(word) if word == NONE => return Ok(Mapping::NoCategory),
253 Declared::Word(_) => {
254 return Err(MapError::Word {
255 table,
256 key: key.to_owned(),
257 });
258 }
259 Declared::Codes(codes) if codes.is_empty() => {
260 return Err(MapError::Empty {
261 table,
262 key: key.to_owned(),
263 });
264 }
265 Declared::Codes(codes) => codes,
266 };
267 codes
268 .iter()
269 .map(|code| {
270 named(code, national).ok_or_else(|| MapError::Unknown {
271 table,
272 key: key.to_owned(),
273 code: code.clone(),
274 })
275 })
276 .collect::<Result<BTreeSet<_>, _>>()
277 .map(Mapping::Categories)
278}
279
280fn named(reference: &str, national: &BTreeSet<NationalCategory>) -> Option<Category> {
283 let reference = Reference::read(reference);
284 reference.priority().or_else(|| {
285 national
286 .iter()
287 .find(|declared| reference.names(declared))
288 .map(|declared| Category::National(declared.clone()))
289 })
290}