1use std::collections::{BTreeMap, BTreeSet};
21use std::fmt;
22
23use crate::category::Category;
24use crate::map::{CategoryMap, Mapping, Table};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
28#[non_exhaustive]
29pub enum Basis {
30 Returned,
32 Written,
34 Queried,
37 Construction,
39}
40
41impl Basis {
42 #[must_use]
44 pub fn code(self) -> &'static str {
45 match self {
46 Self::Returned => "returned",
47 Self::Written => "written",
48 Self::Queried => "queried",
49 Self::Construction => "construction",
50 }
51 }
52}
53
54#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
57pub struct RootObject {
58 pub template_id: Option<String>,
60 pub archetype_id: Option<String>,
62 pub version_uid: Option<String>,
64}
65
66impl fmt::Debug for RootObject {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 f.debug_struct("RootObject")
69 .field("template_id", &self.template_id.is_some())
70 .field("archetype_id", &self.archetype_id.is_some())
71 .field("version_uid", &self.version_uid.is_some())
72 .finish()
73 }
74}
75
76#[derive(Clone, Default, PartialEq, Eq)]
78pub struct Evidence {
79 reached: Option<Basis>,
80 objects: Vec<RootObject>,
81 queried_templates: BTreeSet<String>,
82 queried_archetypes: BTreeSet<String>,
83 every_root_bound: bool,
84 unrooted: bool,
85 no_category: bool,
86 unreadable: Option<Unreadable>,
87 constructed: BTreeSet<Category>,
88}
89
90#[derive(Debug, Clone, Default, PartialEq, Eq)]
92pub struct Queried {
93 pub templates: BTreeSet<String>,
95 pub archetypes: BTreeSet<String>,
97 pub every_root_bound: bool,
100}
101
102impl Evidence {
103 #[must_use]
106 pub fn reached(basis: Basis, objects: Vec<RootObject>) -> Self {
107 Self {
108 reached: Some(basis),
109 objects,
110 ..Self::default()
111 }
112 }
113
114 #[must_use]
117 pub fn no_category() -> Self {
118 Self {
119 no_category: true,
120 ..Self::default()
121 }
122 }
123
124 #[must_use]
127 pub fn unreadable(why: Unreadable) -> Self {
128 Self {
129 unreadable: Some(why),
130 ..Self::default()
131 }
132 }
133
134 #[must_use]
136 pub fn queried(mut self, queried: Queried) -> Self {
137 self.queried_templates = queried.templates;
138 self.queried_archetypes = queried.archetypes;
139 self.every_root_bound = queried.every_root_bound;
140 self
141 }
142
143 #[must_use]
147 pub fn with_unrooted(mut self) -> Self {
148 self.unrooted = true;
149 self
150 }
151
152 #[must_use]
157 pub fn constructed(mut self, category: Category) -> Self {
158 self.constructed.insert(category);
159 self
160 }
161
162 #[must_use]
164 pub fn objects(&self) -> &[RootObject] {
165 &self.objects
166 }
167}
168
169impl fmt::Debug for Evidence {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 f.debug_struct("Evidence")
172 .field("reached", &self.reached)
173 .field("objects", &self.objects.len())
174 .field("queried_templates", &self.queried_templates.len())
175 .field("queried_archetypes", &self.queried_archetypes.len())
176 .field("every_root_bound", &self.every_root_bound)
177 .field("unrooted", &self.unrooted)
178 .field("no_category", &self.no_category)
179 .field("unreadable", &self.unreadable.is_some())
180 .field("constructed", &self.constructed)
181 .finish()
182 }
183}
184
185#[derive(Debug, Clone, PartialEq, Eq)]
187#[non_exhaustive]
188pub enum Unclassified {
189 Unmapped,
191 NamedNothing,
193 Unbound,
196 Unreadable(Unreadable),
198}
199
200impl Unclassified {
201 #[must_use]
204 pub fn code(&self) -> &'static str {
205 match self {
206 Self::Unmapped => "unmapped",
207 Self::NamedNothing => "named-nothing",
208 Self::Unbound => "unbound",
209 Self::Unreadable(why) => why.code(),
210 }
211 }
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
216#[non_exhaustive]
217pub enum Unreadable {
218 Operation,
221 Format,
224 Body,
227 NoObject,
229}
230
231impl Unreadable {
232 #[must_use]
234 pub fn code(self) -> &'static str {
235 match self {
236 Self::Operation => "operation-not-read",
237 Self::Format => "format-not-read",
238 Self::Body => "body-not-read",
239 Self::NoObject => "no-object-returned",
240 }
241 }
242}
243
244#[derive(Clone, PartialEq, Eq)]
254pub struct Classification {
255 categories: BTreeMap<Category, BTreeSet<Basis>>,
256 no_category: bool,
257 unclassified: Option<Unclassified>,
258 templates: BTreeSet<String>,
259 archetypes: BTreeSet<String>,
260 versions: BTreeSet<String>,
261 unmapped: BTreeSet<String>,
262 digest: Option<String>,
263}
264
265impl fmt::Debug for Classification {
266 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267 f.debug_struct("Classification")
268 .field("categories", &self.categories)
269 .field("no_category", &self.no_category)
270 .field("unclassified", &self.unclassified.as_ref().map(|_| "set"))
271 .field("templates", &self.templates.len())
272 .field("archetypes", &self.archetypes.len())
273 .field("versions", &self.versions.len())
274 .field("unmapped", &self.unmapped.len())
275 .finish_non_exhaustive()
276 }
277}
278
279impl Classification {
280 #[must_use]
282 pub fn categories(&self) -> &BTreeMap<Category, BTreeSet<Basis>> {
283 &self.categories
284 }
285
286 #[must_use]
289 pub fn is_no_category(&self) -> bool {
290 self.no_category && self.categories.is_empty() && self.unclassified.is_none()
291 }
292
293 #[must_use]
296 pub fn unclassified(&self) -> Option<&Unclassified> {
297 self.unclassified.as_ref()
298 }
299
300 #[must_use]
302 pub fn templates(&self) -> &BTreeSet<String> {
303 &self.templates
304 }
305
306 #[must_use]
308 pub fn archetypes(&self) -> &BTreeSet<String> {
309 &self.archetypes
310 }
311
312 #[must_use]
314 pub fn versions(&self) -> &BTreeSet<String> {
315 &self.versions
316 }
317
318 #[must_use]
320 pub fn unmapped(&self) -> &BTreeSet<String> {
321 &self.unmapped
322 }
323
324 #[must_use]
326 pub fn digest(&self) -> Option<&str> {
327 self.digest.as_deref()
328 }
329
330 fn add(&mut self, mapping: &Mapping, basis: Basis) {
331 match mapping {
332 Mapping::NoCategory => self.no_category = true,
333 Mapping::Categories(categories) => {
334 for category in categories {
335 self.categories
336 .entry(category.clone())
337 .or_default()
338 .insert(basis);
339 }
340 }
341 }
342 }
343}
344
345impl CategoryMap {
346 #[must_use]
354 pub fn classify(&self, evidence: &Evidence) -> Classification {
355 let mut classified = Classification {
356 categories: BTreeMap::new(),
357 no_category: evidence.no_category,
358 unclassified: None,
359 templates: BTreeSet::new(),
360 archetypes: BTreeSet::new(),
361 versions: BTreeSet::new(),
362 unmapped: BTreeSet::new(),
363 digest: self.digest().map(str::to_owned),
364 };
365 if evidence.no_category {
366 return classified;
367 }
368 if let Some(why) = evidence.unreadable {
369 classified.unclassify(Unclassified::Unreadable(why));
370 }
371 if let Some(basis) = evidence.reached {
372 for object in &evidence.objects {
373 self.object(object, basis, &mut classified);
374 }
375 }
376 for category in &evidence.constructed {
377 classified
378 .categories
379 .entry(category.clone())
380 .or_default()
381 .insert(Basis::Construction);
382 }
383 if evidence.objects.is_empty() || evidence.unrooted {
384 self.queried(evidence, &mut classified);
385 }
386 if classified.categories.is_empty() && !classified.no_category {
389 classified.unclassify(Unclassified::NamedNothing);
390 }
391 classified
392 }
393
394 fn object(&self, object: &RootObject, basis: Basis, classified: &mut Classification) {
398 classified.templates.extend(object.template_id.clone());
399 classified.archetypes.extend(object.archetype_id.clone());
400 classified.versions.extend(object.version_uid.clone());
401 let template = object
402 .template_id
403 .as_deref()
404 .map(|template| self.template(template));
405 let archetype = object
406 .archetype_id
407 .as_deref()
408 .and_then(|archetype| self.archetype(archetype));
409 let mapping = match (template, archetype) {
410 (Some(Some(mapping)), _) | (None | Some(None), Some(mapping))
411 if !matches!((template, mapping), (Some(None), Mapping::NoCategory)) =>
412 {
413 Some(mapping)
414 }
415 _ => None,
416 };
417 if let Some(mapping) = mapping {
418 classified.add(mapping, basis);
419 return;
420 }
421 classified.unmapped.extend(
422 object
423 .template_id
424 .iter()
425 .chain(&object.archetype_id)
426 .cloned(),
427 );
428 classified.unclassify(Unclassified::Unmapped);
429 }
430
431 fn queried(&self, evidence: &Evidence, classified: &mut Classification) {
436 classified
437 .templates
438 .extend(evidence.queried_templates.iter().cloned());
439 classified
440 .archetypes
441 .extend(evidence.queried_archetypes.iter().cloned());
442 let (ids, table) = if evidence.queried_templates.is_empty() {
443 (&evidence.queried_archetypes, Table::Archetypes)
444 } else {
445 (&evidence.queried_templates, Table::Templates)
446 };
447 if ids.is_empty() {
448 classified.unclassify(Unclassified::NamedNothing);
449 return;
450 }
451 for id in ids {
452 let found = match table {
453 Table::Templates => self.template(id),
454 Table::Archetypes => self.archetype(id),
455 };
456 if let Some(mapping) = found {
457 classified.add(mapping, Basis::Queried);
458 } else {
459 classified.unmapped.insert(id.clone());
460 classified.unclassify(Unclassified::Unmapped);
461 }
462 }
463 if !evidence.every_root_bound {
464 classified.unclassify(Unclassified::Unbound);
465 }
466 }
467}
468
469impl Classification {
470 fn unclassify(&mut self, why: Unclassified) {
472 if self.unclassified.is_none() {
473 self.unclassified = Some(why);
474 }
475 }
476}