use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use crate::category::Category;
use crate::map::{CategoryMap, Mapping, Table};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Basis {
Returned,
Written,
Queried,
Construction,
}
impl Basis {
#[must_use]
pub fn code(self) -> &'static str {
match self {
Self::Returned => "returned",
Self::Written => "written",
Self::Queried => "queried",
Self::Construction => "construction",
}
}
}
#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct RootObject {
pub template_id: Option<String>,
pub archetype_id: Option<String>,
pub version_uid: Option<String>,
}
impl fmt::Debug for RootObject {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RootObject")
.field("template_id", &self.template_id.is_some())
.field("archetype_id", &self.archetype_id.is_some())
.field("version_uid", &self.version_uid.is_some())
.finish()
}
}
#[derive(Clone, Default, PartialEq, Eq)]
pub struct Evidence {
reached: Option<Basis>,
objects: Vec<RootObject>,
queried_templates: BTreeSet<String>,
queried_archetypes: BTreeSet<String>,
every_root_bound: bool,
unrooted: bool,
no_category: bool,
unreadable: Option<Unreadable>,
constructed: BTreeSet<Category>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Queried {
pub templates: BTreeSet<String>,
pub archetypes: BTreeSet<String>,
pub every_root_bound: bool,
}
impl Evidence {
#[must_use]
pub fn reached(basis: Basis, objects: Vec<RootObject>) -> Self {
Self {
reached: Some(basis),
objects,
..Self::default()
}
}
#[must_use]
pub fn no_category() -> Self {
Self {
no_category: true,
..Self::default()
}
}
#[must_use]
pub fn unreadable(why: Unreadable) -> Self {
Self {
unreadable: Some(why),
..Self::default()
}
}
#[must_use]
pub fn queried(mut self, queried: Queried) -> Self {
self.queried_templates = queried.templates;
self.queried_archetypes = queried.archetypes;
self.every_root_bound = queried.every_root_bound;
self
}
#[must_use]
pub fn with_unrooted(mut self) -> Self {
self.unrooted = true;
self
}
#[must_use]
pub fn constructed(mut self, category: Category) -> Self {
self.constructed.insert(category);
self
}
#[must_use]
pub fn objects(&self) -> &[RootObject] {
&self.objects
}
}
impl fmt::Debug for Evidence {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Evidence")
.field("reached", &self.reached)
.field("objects", &self.objects.len())
.field("queried_templates", &self.queried_templates.len())
.field("queried_archetypes", &self.queried_archetypes.len())
.field("every_root_bound", &self.every_root_bound)
.field("unrooted", &self.unrooted)
.field("no_category", &self.no_category)
.field("unreadable", &self.unreadable.is_some())
.field("constructed", &self.constructed)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Unclassified {
Unmapped,
NamedNothing,
Unbound,
Unreadable(Unreadable),
}
impl Unclassified {
#[must_use]
pub fn code(&self) -> &'static str {
match self {
Self::Unmapped => "unmapped",
Self::NamedNothing => "named-nothing",
Self::Unbound => "unbound",
Self::Unreadable(why) => why.code(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Unreadable {
Operation,
Format,
Body,
NoObject,
}
impl Unreadable {
#[must_use]
pub fn code(self) -> &'static str {
match self {
Self::Operation => "operation-not-read",
Self::Format => "format-not-read",
Self::Body => "body-not-read",
Self::NoObject => "no-object-returned",
}
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct Classification {
categories: BTreeMap<Category, BTreeSet<Basis>>,
no_category: bool,
unclassified: Option<Unclassified>,
templates: BTreeSet<String>,
archetypes: BTreeSet<String>,
versions: BTreeSet<String>,
unmapped: BTreeSet<String>,
digest: Option<String>,
}
impl fmt::Debug for Classification {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Classification")
.field("categories", &self.categories)
.field("no_category", &self.no_category)
.field("unclassified", &self.unclassified.as_ref().map(|_| "set"))
.field("templates", &self.templates.len())
.field("archetypes", &self.archetypes.len())
.field("versions", &self.versions.len())
.field("unmapped", &self.unmapped.len())
.finish_non_exhaustive()
}
}
impl Classification {
#[must_use]
pub fn categories(&self) -> &BTreeMap<Category, BTreeSet<Basis>> {
&self.categories
}
#[must_use]
pub fn is_no_category(&self) -> bool {
self.no_category && self.categories.is_empty() && self.unclassified.is_none()
}
#[must_use]
pub fn unclassified(&self) -> Option<&Unclassified> {
self.unclassified.as_ref()
}
#[must_use]
pub fn templates(&self) -> &BTreeSet<String> {
&self.templates
}
#[must_use]
pub fn archetypes(&self) -> &BTreeSet<String> {
&self.archetypes
}
#[must_use]
pub fn versions(&self) -> &BTreeSet<String> {
&self.versions
}
#[must_use]
pub fn unmapped(&self) -> &BTreeSet<String> {
&self.unmapped
}
#[must_use]
pub fn digest(&self) -> Option<&str> {
self.digest.as_deref()
}
fn add(&mut self, mapping: &Mapping, basis: Basis) {
match mapping {
Mapping::NoCategory => self.no_category = true,
Mapping::Categories(categories) => {
for category in categories {
self.categories
.entry(category.clone())
.or_default()
.insert(basis);
}
}
}
}
}
impl CategoryMap {
#[must_use]
pub fn classify(&self, evidence: &Evidence) -> Classification {
let mut classified = Classification {
categories: BTreeMap::new(),
no_category: evidence.no_category,
unclassified: None,
templates: BTreeSet::new(),
archetypes: BTreeSet::new(),
versions: BTreeSet::new(),
unmapped: BTreeSet::new(),
digest: self.digest().map(str::to_owned),
};
if evidence.no_category {
return classified;
}
if let Some(why) = evidence.unreadable {
classified.unclassify(Unclassified::Unreadable(why));
}
if let Some(basis) = evidence.reached {
for object in &evidence.objects {
self.object(object, basis, &mut classified);
}
}
for category in &evidence.constructed {
classified
.categories
.entry(category.clone())
.or_default()
.insert(Basis::Construction);
}
if evidence.objects.is_empty() || evidence.unrooted {
self.queried(evidence, &mut classified);
}
if classified.categories.is_empty() && !classified.no_category {
classified.unclassify(Unclassified::NamedNothing);
}
classified
}
fn object(&self, object: &RootObject, basis: Basis, classified: &mut Classification) {
classified.templates.extend(object.template_id.clone());
classified.archetypes.extend(object.archetype_id.clone());
classified.versions.extend(object.version_uid.clone());
let template = object
.template_id
.as_deref()
.map(|template| self.template(template));
let archetype = object
.archetype_id
.as_deref()
.and_then(|archetype| self.archetype(archetype));
let mapping = match (template, archetype) {
(Some(Some(mapping)), _) | (None | Some(None), Some(mapping))
if !matches!((template, mapping), (Some(None), Mapping::NoCategory)) =>
{
Some(mapping)
}
_ => None,
};
if let Some(mapping) = mapping {
classified.add(mapping, basis);
return;
}
classified.unmapped.extend(
object
.template_id
.iter()
.chain(&object.archetype_id)
.cloned(),
);
classified.unclassify(Unclassified::Unmapped);
}
fn queried(&self, evidence: &Evidence, classified: &mut Classification) {
classified
.templates
.extend(evidence.queried_templates.iter().cloned());
classified
.archetypes
.extend(evidence.queried_archetypes.iter().cloned());
let (ids, table) = if evidence.queried_templates.is_empty() {
(&evidence.queried_archetypes, Table::Archetypes)
} else {
(&evidence.queried_templates, Table::Templates)
};
if ids.is_empty() {
classified.unclassify(Unclassified::NamedNothing);
return;
}
for id in ids {
let found = match table {
Table::Templates => self.template(id),
Table::Archetypes => self.archetype(id),
};
if let Some(mapping) = found {
classified.add(mapping, Basis::Queried);
} else {
classified.unmapped.insert(id.clone());
classified.unclassify(Unclassified::Unmapped);
}
}
if !evidence.every_root_bound {
classified.unclassify(Unclassified::Unbound);
}
}
}
impl Classification {
fn unclassify(&mut self, why: Unclassified) {
if self.unclassified.is_none() {
self.unclassified = Some(why);
}
}
}