ontocore_reasoner/
cache.rs1use crate::adapter::ReasonerId;
2use crate::input::ReasonerInput;
3use crate::result::{ClassificationResult, ReasonerSnapshot};
4use std::collections::HashMap;
5
6const MAX_CACHE_ENTRIES: usize = 8;
8
9#[derive(Debug, Clone)]
10pub struct ReasonerCache {
11 pub content_hash: String,
12 pub profile: ReasonerId,
13 pub snapshot: ReasonerSnapshot,
14 pub classification: ClassificationResult,
15 pub input: ReasonerInput,
16}
17
18#[derive(Debug, Default)]
19pub struct ReasonerCacheStore {
20 entries: HashMap<(String, String), ReasonerCache>,
21 order: Vec<(String, String)>,
23}
24
25impl ReasonerCacheStore {
26 pub fn new() -> Self {
27 Self::default()
28 }
29
30 pub fn get(&self, content_hash: &str, profile: ReasonerId) -> Option<&ReasonerCache> {
31 self.entries.get(&(content_hash.to_string(), profile.as_str().to_string()))
32 }
33
34 pub fn get_any_for_profile(&self, profile: ReasonerId) -> Option<&ReasonerCache> {
35 self.entries.values().find(|e| e.profile == profile)
36 }
37
38 pub fn insert(&mut self, cache: ReasonerCache) {
39 let profile_key = cache.profile.as_str().to_string();
40 let key = (cache.content_hash.clone(), profile_key.clone());
41
42 let stale: Vec<_> =
44 self.entries.keys().filter(|(_, p)| p == &profile_key).cloned().collect();
45 for stale_key in stale {
46 self.entries.remove(&stale_key);
47 self.order.retain(|k| k != &stale_key);
48 }
49
50 if self.entries.insert(key.clone(), cache).is_none() {
51 self.order.push(key);
52 } else if let Some(pos) = self.order.iter().position(|k| k == &key) {
53 let k = self.order.remove(pos);
54 self.order.push(k);
55 }
56
57 while self.entries.len() > MAX_CACHE_ENTRIES {
58 if let Some(oldest) = self.order.first().cloned() {
59 self.entries.remove(&oldest);
60 self.order.remove(0);
61 } else {
62 break;
63 }
64 }
65 }
66
67 pub fn invalidate(&mut self) {
68 self.entries.clear();
69 self.order.clear();
70 }
71
72 pub fn latest_snapshot(&self) -> Option<&ReasonerSnapshot> {
73 self.entries.values().max_by_key(|e| e.snapshot.classified_at).map(|e| &e.snapshot)
74 }
75
76 pub fn len(&self) -> usize {
77 self.entries.len()
78 }
79
80 pub fn is_empty(&self) -> bool {
81 self.entries.is_empty()
82 }
83
84 pub fn store_classification(
85 &mut self,
86 input: ReasonerInput,
87 profile: ReasonerId,
88 classification: ClassificationResult,
89 ) -> ReasonerSnapshot {
90 let snapshot = ReasonerSnapshot::from(classification.clone());
91 let cache = ReasonerCache {
92 content_hash: input.content_hash.clone(),
93 profile,
94 snapshot: snapshot.clone(),
95 classification,
96 input,
97 };
98 self.insert(cache);
99 snapshot
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use crate::input::ReasonerInput;
107 use crate::result::{ClassificationResult, InferredHierarchy};
108 use ontocore_catalog::ClassHierarchy;
109 use ontologos_core::Ontology;
110 use std::collections::HashMap;
111 use std::path::PathBuf;
112
113 fn dummy_input(hash: &str) -> ReasonerInput {
114 ReasonerInput {
115 workspace: PathBuf::from("/tmp"),
116 content_hash: hash.to_string(),
117 ontology: Ontology::new(),
118 asserted_hierarchy: ClassHierarchy::default(),
119 document_overrides: HashMap::new(),
120 }
121 }
122
123 fn dummy_classification(profile: &str) -> ClassificationResult {
124 ClassificationResult {
125 profile_used: profile.to_string(),
126 consistent: true,
127 unsatisfiable: Vec::new(),
128 inferred: InferredHierarchy {
129 edges: Vec::new(),
130 unsatisfiable: Vec::new(),
131 combined: ClassHierarchy::default(),
132 },
133 new_inferences: Vec::new(),
134 warnings: Vec::new(),
135 duration_ms: 0,
136 subsumption_count: 0,
137 inferred_axiom_count: 0,
138 }
139 }
140
141 #[test]
142 fn one_entry_per_profile() {
143 let mut store = ReasonerCacheStore::new();
144 store.store_classification(dummy_input("a"), ReasonerId::El, dummy_classification("el"));
145 store.store_classification(dummy_input("b"), ReasonerId::El, dummy_classification("el"));
146 assert_eq!(store.len(), 1);
147 assert!(store.get("b", ReasonerId::El).is_some());
148 assert!(store.get("a", ReasonerId::El).is_none());
149 }
150}