1use std::collections::{BTreeMap, BTreeSet};
25use std::fmt;
26
27use crate::category::Category;
28use crate::map::{CategoryMap, Mapping, Table};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
32#[non_exhaustive]
33pub enum Basis {
34 Returned,
36 Written,
38 Queried,
41 Construction,
43 ResourceKind,
46}
47
48impl Basis {
49 #[must_use]
51 pub fn code(self) -> &'static str {
52 match self {
53 Self::Returned => "returned",
54 Self::Written => "written",
55 Self::Queried => "queried",
56 Self::Construction => "construction",
57 Self::ResourceKind => "resource-kind",
58 }
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
66#[non_exhaustive]
67pub enum ResourceKind {
68 Ehr,
70 EhrStatus,
72 EhrAccess,
74 Folder,
76 ItemTag,
78 RevisionHistory,
80 Other(String),
82}
83
84impl ResourceKind {
85 #[must_use]
88 pub fn code(&self) -> &str {
89 match self {
90 Self::Ehr => "EHR",
91 Self::EhrStatus => "EHR_STATUS",
92 Self::EhrAccess => "EHR_ACCESS",
93 Self::Folder => "FOLDER",
94 Self::ItemTag => "ITEM_TAG",
95 Self::RevisionHistory => "REVISION_HISTORY",
96 Self::Other(name) => name,
97 }
98 }
99}
100
101#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
104pub struct RootObject {
105 pub template_id: Option<String>,
107 pub archetype_id: Option<String>,
109 pub version_uid: Option<String>,
111 pub kind: Option<ResourceKind>,
115}
116
117impl fmt::Debug for RootObject {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 f.debug_struct("RootObject")
120 .field("template_id", &self.template_id.is_some())
121 .field("archetype_id", &self.archetype_id.is_some())
122 .field("version_uid", &self.version_uid.is_some())
123 .field("kind", &self.kind)
124 .finish()
125 }
126}
127
128#[derive(Clone, Default, PartialEq, Eq)]
130pub struct Evidence {
131 reached: Option<Basis>,
132 objects: Vec<RootObject>,
133 queried_templates: BTreeSet<String>,
134 queried_archetypes: BTreeSet<String>,
135 every_root_bound: bool,
136 unrooted: bool,
137 no_category: bool,
138 kinds: BTreeSet<ResourceKind>,
139 unreadable: Option<Unreadable>,
140 constructed: BTreeSet<Category>,
141}
142
143#[derive(Debug, Clone, Default, PartialEq, Eq)]
145pub struct Queried {
146 pub templates: BTreeSet<String>,
148 pub archetypes: BTreeSet<String>,
150 pub every_root_bound: bool,
153}
154
155impl Evidence {
156 #[must_use]
159 pub fn reached(basis: Basis, objects: Vec<RootObject>) -> Self {
160 Self {
161 reached: Some(basis),
162 objects,
163 ..Self::default()
164 }
165 }
166
167 #[must_use]
170 pub fn no_category() -> Self {
171 Self {
172 no_category: true,
173 ..Self::default()
174 }
175 }
176
177 #[must_use]
180 pub fn resource(kind: ResourceKind) -> Self {
181 Self {
182 no_category: true,
183 kinds: BTreeSet::from([kind]),
184 ..Self::default()
185 }
186 }
187
188 #[must_use]
191 pub fn unreadable(why: Unreadable) -> Self {
192 Self {
193 unreadable: Some(why),
194 ..Self::default()
195 }
196 }
197
198 #[must_use]
200 pub fn queried(mut self, queried: Queried) -> Self {
201 self.queried_templates = queried.templates;
202 self.queried_archetypes = queried.archetypes;
203 self.every_root_bound = queried.every_root_bound;
204 self
205 }
206
207 #[must_use]
211 pub fn with_unrooted(mut self) -> Self {
212 self.unrooted = true;
213 self
214 }
215
216 #[must_use]
221 pub fn constructed(mut self, category: Category) -> Self {
222 self.constructed.insert(category);
223 self
224 }
225
226 #[must_use]
228 pub fn objects(&self) -> &[RootObject] {
229 &self.objects
230 }
231}
232
233impl fmt::Debug for Evidence {
234 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235 f.debug_struct("Evidence")
236 .field("reached", &self.reached)
237 .field("objects", &self.objects.len())
238 .field("queried_templates", &self.queried_templates.len())
239 .field("queried_archetypes", &self.queried_archetypes.len())
240 .field("every_root_bound", &self.every_root_bound)
241 .field("unrooted", &self.unrooted)
242 .field("no_category", &self.no_category)
243 .field("kinds", &self.kinds)
244 .field("unreadable", &self.unreadable.is_some())
245 .field("constructed", &self.constructed)
246 .finish()
247 }
248}
249
250#[derive(Debug, Clone, PartialEq, Eq)]
252#[non_exhaustive]
253pub enum Unclassified {
254 Unmapped,
256 NamedNothing,
258 Unbound,
261 Unreadable(Unreadable),
263}
264
265impl Unclassified {
266 #[must_use]
269 pub fn code(&self) -> &'static str {
270 match self {
271 Self::Unmapped => "unmapped",
272 Self::NamedNothing => "named-nothing",
273 Self::Unbound => "unbound",
274 Self::Unreadable(why) => why.code(),
275 }
276 }
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
281#[non_exhaustive]
282pub enum Unreadable {
283 Operation,
286 Format,
289 Body,
292 NoObject,
294}
295
296impl Unreadable {
297 #[must_use]
299 pub fn code(self) -> &'static str {
300 match self {
301 Self::Operation => "operation-not-read",
302 Self::Format => "format-not-read",
303 Self::Body => "body-not-read",
304 Self::NoObject => "no-object-returned",
305 }
306 }
307}
308
309#[derive(Clone, PartialEq, Eq)]
319pub struct Classification {
320 categories: BTreeMap<Category, BTreeSet<Basis>>,
321 no_category: bool,
322 kinds: BTreeSet<ResourceKind>,
323 unclassified: Option<Unclassified>,
324 templates: BTreeSet<String>,
325 archetypes: BTreeSet<String>,
326 versions: BTreeSet<String>,
327 unmapped: BTreeSet<String>,
328 counts: EvidenceCounts,
329 digest: Option<String>,
330}
331
332#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
335pub struct EvidenceCounts {
336 pub templates: usize,
338 pub archetypes: usize,
340 pub versions: usize,
342 pub unmapped: usize,
344}
345
346impl fmt::Debug for Classification {
347 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348 f.debug_struct("Classification")
349 .field("categories", &self.categories)
350 .field("no_category", &self.no_category)
351 .field("kinds", &self.kinds)
352 .field("unclassified", &self.unclassified.as_ref().map(|_| "set"))
353 .field("templates", &self.templates.len())
354 .field("archetypes", &self.archetypes.len())
355 .field("versions", &self.versions.len())
356 .field("unmapped", &self.unmapped.len())
357 .finish_non_exhaustive()
358 }
359}
360
361impl Classification {
362 #[must_use]
364 pub fn categories(&self) -> &BTreeMap<Category, BTreeSet<Basis>> {
365 &self.categories
366 }
367
368 #[must_use]
371 pub fn is_no_category(&self) -> bool {
372 self.no_category && self.categories.is_empty() && self.unclassified.is_none()
373 }
374
375 #[must_use]
378 pub fn no_category_kinds(&self) -> &BTreeSet<ResourceKind> {
379 &self.kinds
380 }
381
382 #[must_use]
385 pub fn evidence_counts(&self) -> EvidenceCounts {
386 self.counts
387 }
388
389 #[must_use]
392 pub fn unclassified(&self) -> Option<&Unclassified> {
393 self.unclassified.as_ref()
394 }
395
396 #[must_use]
398 pub fn templates(&self) -> &BTreeSet<String> {
399 &self.templates
400 }
401
402 #[must_use]
404 pub fn archetypes(&self) -> &BTreeSet<String> {
405 &self.archetypes
406 }
407
408 #[must_use]
410 pub fn versions(&self) -> &BTreeSet<String> {
411 &self.versions
412 }
413
414 #[must_use]
416 pub fn unmapped(&self) -> &BTreeSet<String> {
417 &self.unmapped
418 }
419
420 #[must_use]
422 pub fn digest(&self) -> Option<&str> {
423 self.digest.as_deref()
424 }
425
426 fn add(&mut self, mapping: &Mapping, basis: Basis) {
427 match mapping {
428 Mapping::NoCategory => self.no_category = true,
429 Mapping::Categories(categories) => {
430 for category in categories {
431 self.categories
432 .entry(category.clone())
433 .or_default()
434 .insert(basis);
435 }
436 }
437 }
438 }
439}
440
441impl CategoryMap {
442 #[must_use]
450 pub fn classify(&self, evidence: &Evidence) -> Classification {
451 let mut classified = Classification {
452 categories: BTreeMap::new(),
453 no_category: evidence.no_category,
454 kinds: evidence.kinds.clone(),
455 unclassified: None,
456 templates: BTreeSet::new(),
457 archetypes: BTreeSet::new(),
458 versions: BTreeSet::new(),
459 unmapped: BTreeSet::new(),
460 counts: EvidenceCounts::default(),
461 digest: self.digest().map(str::to_owned),
462 };
463 if evidence.no_category {
464 return classified;
465 }
466 if let Some(why) = evidence.unreadable {
467 classified.unclassify(Unclassified::Unreadable(why));
468 }
469 if let Some(basis) = evidence.reached {
470 for object in &evidence.objects {
471 self.object(object, basis, &mut classified);
472 }
473 }
474 for category in &evidence.constructed {
475 classified
476 .categories
477 .entry(category.clone())
478 .or_default()
479 .insert(Basis::Construction);
480 }
481 if evidence.objects.is_empty() || evidence.unrooted {
482 self.queried(evidence, &mut classified);
483 }
484 if classified.categories.is_empty() && !classified.no_category {
487 classified.unclassify(Unclassified::NamedNothing);
488 }
489 classified.bound(self.evidence_limit());
490 classified
491 }
492
493 fn object(&self, object: &RootObject, basis: Basis, classified: &mut Classification) {
497 classified.templates.extend(object.template_id.clone());
498 classified.archetypes.extend(object.archetype_id.clone());
499 classified.versions.extend(object.version_uid.clone());
500 if let Some(kind) = &object.kind {
501 classified.no_category = true;
502 classified.kinds.insert(kind.clone());
503 return;
504 }
505 let template = object
506 .template_id
507 .as_deref()
508 .map(|template| self.template(template));
509 let archetype = object
510 .archetype_id
511 .as_deref()
512 .and_then(|archetype| self.archetype(archetype));
513 let mapping = match (template, archetype) {
514 (Some(Some(mapping)), _) | (None | Some(None), Some(mapping))
515 if !matches!((template, mapping), (Some(None), Mapping::NoCategory)) =>
516 {
517 Some(mapping)
518 }
519 _ => None,
520 };
521 if let Some(mapping) = mapping {
522 classified.add(mapping, basis);
523 return;
524 }
525 classified.unmapped.extend(
526 object
527 .template_id
528 .iter()
529 .chain(&object.archetype_id)
530 .cloned(),
531 );
532 classified.unclassify(Unclassified::Unmapped);
533 }
534
535 fn queried(&self, evidence: &Evidence, classified: &mut Classification) {
540 classified
541 .templates
542 .extend(evidence.queried_templates.iter().cloned());
543 classified
544 .archetypes
545 .extend(evidence.queried_archetypes.iter().cloned());
546 let (ids, table) = if evidence.queried_templates.is_empty() {
547 (&evidence.queried_archetypes, Table::Archetypes)
548 } else {
549 (&evidence.queried_templates, Table::Templates)
550 };
551 if ids.is_empty() {
552 classified.unclassify(Unclassified::NamedNothing);
553 return;
554 }
555 for id in ids {
556 let found = match table {
557 Table::Templates => self.template(id),
558 Table::Archetypes => self.archetype(id),
559 };
560 if let Some(mapping) = found {
561 classified.add(mapping, Basis::Queried);
562 } else {
563 classified.unmapped.insert(id.clone());
564 classified.unclassify(Unclassified::Unmapped);
565 }
566 }
567 if !evidence.every_root_bound {
568 classified.unclassify(Unclassified::Unbound);
569 }
570 }
571}
572
573impl Classification {
574 fn bound(&mut self, limit: Option<usize>) {
577 self.counts = EvidenceCounts {
578 templates: self.templates.len(),
579 archetypes: self.archetypes.len(),
580 versions: self.versions.len(),
581 unmapped: self.unmapped.len(),
582 };
583 if let Some(limit) = limit {
584 for set in [
585 &mut self.templates,
586 &mut self.archetypes,
587 &mut self.versions,
588 &mut self.unmapped,
589 ] {
590 while set.len() > limit {
591 set.pop_last();
592 }
593 }
594 }
595 }
596
597 fn unclassify(&mut self, why: Unclassified) {
599 if self.unclassified.is_none() {
600 self.unclassified = Some(why);
601 }
602 }
603}