1use std::collections::{BTreeMap, BTreeSet};
24use std::sync::Arc;
25
26use axioval_ir::contract::Selector;
27use axioval_ir::{Object, ObjectId, Project, SourceId};
28use thiserror::Error;
29
30use crate::{
31 CompiledRule, ConceptBindings, RuleOutcomes, ServiceRegistry, SessionSources,
32 SnapshotBoundService, SourceSnapshot,
33};
34
35#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
37pub struct ResourceRequest {
38 source: SourceId,
39 class: String,
40 include_subtypes: bool,
41}
42
43impl ResourceRequest {
44 pub fn try_new(
51 source: SourceId,
52 class: impl Into<String>,
53 include_subtypes: bool,
54 ) -> Result<Self, ResourceError> {
55 let class = class.into();
56 if class.trim().is_empty() {
57 return Err(ResourceError::InvalidRequest(
58 "a resource class name must not be blank".into(),
59 ));
60 }
61 Ok(Self {
62 source,
63 class,
64 include_subtypes,
65 })
66 }
67
68 #[must_use]
70 pub fn source(&self) -> &SourceId {
71 &self.source
72 }
73
74 #[must_use]
76 pub fn class(&self) -> &str {
77 &self.class
78 }
79
80 #[must_use]
82 pub fn include_subtypes(&self) -> bool {
83 self.include_subtypes
84 }
85}
86
87#[derive(Clone, Debug, Error, Eq, PartialEq)]
89pub enum ResourceError {
90 #[error("resource service does not cover source `{0}`")]
92 UncoveredSource(SourceId),
93 #[error("invalid resource request: {0}")]
95 InvalidRequest(String),
96 #[error("resource objects cannot be listed exactly: {0}")]
98 Unreadable(String),
99 #[error("resource service answered out of contract: {0}")]
102 InvalidAnswer(String),
103}
104
105pub trait ResourceService: Send + Sync {
107 fn source_snapshots(&self) -> &[SourceSnapshot];
109
110 fn resources(&self, request: &ResourceRequest) -> Result<Vec<Object>, ResourceError>;
120}
121
122#[derive(Clone)]
124pub struct ResourceServiceHandle(Arc<dyn ResourceService>);
125
126impl ResourceServiceHandle {
127 #[must_use]
129 pub fn new(service: Arc<dyn ResourceService>) -> Self {
130 Self(service)
131 }
132
133 pub fn resources(&self, request: &ResourceRequest) -> Result<Vec<Object>, ResourceError> {
136 if !self
137 .0
138 .source_snapshots()
139 .iter()
140 .any(|snapshot| *snapshot.source() == request.source)
141 {
142 return Err(ResourceError::UncoveredSource(request.source.clone()));
143 }
144 let objects = self.0.resources(request)?;
145 for object in &objects {
146 if object.id.source != request.source {
147 return Err(ResourceError::InvalidAnswer(format!(
148 "{} is not of source `{}`",
149 object.id, request.source
150 )));
151 }
152 if !object.properties.is_empty()
153 || !object.classifications.is_empty()
154 || !object.relationships.is_empty()
155 {
156 return Err(ResourceError::InvalidAnswer(format!(
157 "{} carries facts of its own",
158 object.id
159 )));
160 }
161 }
162 if let Some(pair) = objects.windows(2).find(|pair| pair[0].id >= pair[1].id) {
163 return Err(ResourceError::InvalidAnswer(format!(
164 "{} is listed out of order or twice",
165 pair[1].id
166 )));
167 }
168 Ok(objects)
169 }
170}
171
172impl SnapshotBoundService for ResourceServiceHandle {
173 fn source_snapshots(&self) -> &[SourceSnapshot] {
174 self.0.source_snapshots()
175 }
176}
177
178#[derive(Clone, Debug, Default)]
185pub struct ResourceObjects {
186 classes: BTreeMap<(SourceId, String, bool), Result<Vec<ObjectId>, String>>,
189 objects: BTreeMap<ObjectId, Object>,
190}
191
192#[derive(Debug, Default)]
194pub struct Reached<'a> {
195 pub objects: Vec<&'a Object>,
197 pub unreadable: Vec<(SourceId, String)>,
200}
201
202impl ResourceObjects {
203 #[must_use]
205 pub fn new() -> Self {
206 Self::default()
207 }
208
209 #[must_use]
212 pub fn with_class(
213 mut self,
214 source: SourceId,
215 class: impl Into<String>,
216 include_subtypes: bool,
217 answer: Result<Vec<Object>, String>,
218 ) -> Self {
219 let answer = answer.map(|objects| {
220 objects
221 .into_iter()
222 .map(|object| {
223 let id = object.id.clone();
224 self.objects.entry(id.clone()).or_insert(object);
225 id
226 })
227 .collect()
228 });
229 self.classes
230 .insert((source, class.into(), include_subtypes), answer);
231 self
232 }
233
234 #[must_use]
236 pub fn is_empty(&self) -> bool {
237 self.classes.is_empty()
238 }
239
240 #[must_use]
242 pub fn object(&self, id: &ObjectId) -> Option<&Object> {
243 self.objects.get(id)
244 }
245
246 #[must_use]
256 pub fn reached<'s>(
257 &'s self,
258 selector: &Selector,
259 outcomes: Option<&RuleOutcomes>,
260 ) -> Reached<'s> {
261 let mut ids = BTreeSet::new();
262 let mut unreadable = BTreeSet::new();
263 self.reach(selector, outcomes, &mut ids, &mut unreadable);
264 Reached {
265 objects: ids.iter().filter_map(|id| self.objects.get(*id)).collect(),
266 unreadable: unreadable.into_iter().collect(),
267 }
268 }
269
270 fn reach<'s>(
271 &'s self,
272 selector: &Selector,
273 outcomes: Option<&RuleOutcomes>,
274 ids: &mut BTreeSet<&'s ObjectId>,
275 unreadable: &mut BTreeSet<(SourceId, String)>,
276 ) {
277 match selector {
278 Selector::EntityType {
279 object_type,
280 include_subtypes,
281 } => {
282 for ((source, class, subtypes), answer) in &self.classes {
283 if class != object_type || subtypes != include_subtypes {
284 continue;
285 }
286 match answer {
287 Ok(listed) => ids.extend(listed),
288 Err(why) => {
289 unreadable.insert((source.clone(), why.clone()));
290 }
291 }
292 }
293 }
294 Selector::AllOf { operands } | Selector::AnyOf { operands } => {
295 for operand in operands {
296 self.reach(operand, outcomes, ids, unreadable);
297 }
298 }
299 Selector::RuleOutcome { rule, .. } => {
300 let Some(record) = outcomes.and_then(|outcomes| outcomes.get(rule)) else {
301 return;
302 };
303 ids.extend(
304 record
305 .named()
306 .filter_map(|id| self.objects.get_key_value(id).map(|(id, _)| id)),
307 );
308 unreadable.extend(record.unread_resources().cloned());
309 }
310 Selector::All
311 | Selector::Not { .. }
312 | Selector::Related { .. }
313 | Selector::Property { .. }
314 | Selector::PropertyPattern { .. }
315 | Selector::Classification { .. }
316 | Selector::Discipline { .. }
317 | Selector::Source { .. } => {}
318 }
319 }
320}
321
322fn named_classes<'s>(selector: &'s Selector, out: &mut BTreeSet<(&'s str, bool)>) {
325 match selector {
326 Selector::EntityType {
327 object_type,
328 include_subtypes,
329 } => {
330 out.insert((object_type, *include_subtypes));
331 }
332 Selector::AllOf { operands } | Selector::AnyOf { operands } => {
333 for operand in operands {
334 named_classes(operand, out);
335 }
336 }
337 _ => {}
338 }
339}
340
341pub(crate) fn install(services: &mut ServiceRegistry, project: &Project, rules: &[CompiledRule]) {
349 let mut population = ResourceObjects::new();
350 let handle = services.get::<ResourceServiceHandle>().cloned();
351 if let Some(handle) = handle {
352 let mut named = BTreeSet::new();
353 for rule in rules {
354 named_classes(&rule.selector, &mut named);
355 }
356 let sources: Vec<SourceId> = services
357 .get::<SessionSources>()
358 .map(|sources| sources.iter().cloned().collect())
359 .unwrap_or_default();
360 let bindings = services.get::<ConceptBindings>();
361 let mut asked: BTreeMap<(SourceId, String, bool), Result<Vec<Object>, String>> =
362 BTreeMap::new();
363 for source in &sources {
364 for (written, subtypes) in &named {
365 let native = match bindings {
366 Some(bindings) => match bindings.object_type(written, source) {
367 Ok(native) => native.to_owned(),
368 Err(_) => continue,
369 },
370 None => (*written).to_owned(),
371 };
372 let key = (source.clone(), native.to_ascii_uppercase(), *subtypes);
373 let answer = asked
374 .entry(key)
375 .or_insert_with(|| {
376 ResourceRequest::try_new(source.clone(), native, *subtypes)
377 .and_then(|request| handle.resources(&request))
378 .and_then(|objects| {
379 match objects
380 .iter()
381 .find(|object| project.object(&object.id).is_some())
382 {
383 Some(object) => Err(ResourceError::InvalidAnswer(format!(
384 "{} is an object, not a resource object",
385 object.id
386 ))),
387 None => Ok(objects),
388 }
389 })
390 .map_err(|error| error.to_string())
391 })
392 .clone();
393 population = population.with_class(source.clone(), *written, *subtypes, answer);
394 }
395 }
396 }
397 services.replace(population);
398}