Skip to main content

axioval_engine/
resources.rs

1//! Resource objects: source instances outside the object population.
2//!
3//! A project's objects are what a source adapter calls its checked objects
4//! (in IFC every occurrence, context and type object). A source holds much
5//! more: materials, classifications, relationships, task times, surface
6//! styles. A rule may check those as well, but only when it names their
7//! class: they are never part of the object population, so a rule selecting
8//! every object, an object count, BCF and geometry never see them.
9//!
10//! The population is separate on both ends. A [`ResourceService`] lists the
11//! resource objects of one class of one source on request, so nothing is
12//! read for a class no rule names. The runtime asks it, before any rule
13//! runs, for every class an entity-type selector of a rule's applicability
14//! names, and installs the answers as [`ResourceObjects`]. A class the
15//! source declares for its objects, or one with such a subclass when
16//! subtypes are included, has no resource objects: a class selects either
17//! objects or resource objects, never both, so no existing rule changes.
18//!
19//! A resource object states its facts through the same services as an
20//! object (property, attribute and classification resolution), keyed by its
21//! source-qualified identity. It carries no facts of its own.
22
23use 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/// The resource objects of one class of one source.
36#[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    /// Asks for the resource objects of `class` in `source`, and of its
45    /// subclasses when `include_subtypes` is set.
46    ///
47    /// # Errors
48    ///
49    /// [`ResourceError::InvalidRequest`] for a blank class name.
50    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    /// The source asked about.
69    #[must_use]
70    pub fn source(&self) -> &SourceId {
71        &self.source
72    }
73
74    /// The class, in the source's own vocabulary.
75    #[must_use]
76    pub fn class(&self) -> &str {
77        &self.class
78    }
79
80    /// Whether instances of subclasses are asked for too.
81    #[must_use]
82    pub fn include_subtypes(&self) -> bool {
83        self.include_subtypes
84    }
85}
86
87/// Failure to list resource objects conclusively.
88#[derive(Clone, Debug, Error, Eq, PartialEq)]
89pub enum ResourceError {
90    /// The service does not cover this source.
91    #[error("resource service does not cover source `{0}`")]
92    UncoveredSource(SourceId),
93    /// The request itself is malformed.
94    #[error("invalid resource request: {0}")]
95    InvalidRequest(String),
96    /// The source cannot be read completely.
97    #[error("resource objects cannot be listed exactly: {0}")]
98    Unreadable(String),
99    /// The service answered with something other than the request's
100    /// resource objects.
101    #[error("resource service answered out of contract: {0}")]
102    InvalidAnswer(String),
103}
104
105/// Trusted adapter seam listing a source's resource objects by class.
106pub trait ResourceService: Send + Sync {
107    /// Exact source snapshots this service answers for.
108    fn source_snapshots(&self) -> &[SourceSnapshot];
109
110    /// Every resource object of the request's class, complete or refused,
111    /// sorted by identity.
112    ///
113    /// A class the source does not declare has none. So has a class whose
114    /// instances are the source's objects, and, with subtypes, a class one
115    /// of whose subclasses is: such a class selects objects only. Every
116    /// answered object is of the request's source and carries no properties,
117    /// classifications or relationships of its own; its facts are resolved
118    /// through the source's services.
119    fn resources(&self, request: &ResourceRequest) -> Result<Vec<Object>, ResourceError>;
120}
121
122/// Cloneable, type-erased resource service registered by an adapter.
123#[derive(Clone)]
124pub struct ResourceServiceHandle(Arc<dyn ResourceService>);
125
126impl ResourceServiceHandle {
127    /// Wraps a trusted resource service.
128    #[must_use]
129    pub fn new(service: Arc<dyn ResourceService>) -> Self {
130        Self(service)
131    }
132
133    /// Answers one request, refusing sources the service does not cover and
134    /// answers that break the contract.
135    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/// The resource objects one run's rules reach: the answers to every class
179/// their applicability selectors name, per source.
180///
181/// The runtime installs it before any rule runs, replacing any host copy;
182/// capabilities read it through [`ResourceObjects::reached`]. Capability
183/// tests build one with [`ResourceObjects::with_class`].
184#[derive(Clone, Debug, Default)]
185pub struct ResourceObjects {
186    /// Per source, the class as a selector writes it and whether subtypes
187    /// are included: the listed identities, or why they could not be read.
188    classes: BTreeMap<(SourceId, String, bool), Result<Vec<ObjectId>, String>>,
189    objects: BTreeMap<ObjectId, Object>,
190}
191
192/// The resource objects a selector reaches.
193#[derive(Debug, Default)]
194pub struct Reached<'a> {
195    /// Every resource object reached, sorted by identity, once each.
196    pub objects: Vec<&'a Object>,
197    /// Sources whose resource objects of a reached class could not be
198    /// listed, with why, sorted: a selection there is incomplete.
199    pub unreadable: Vec<(SourceId, String)>,
200}
201
202impl ResourceObjects {
203    /// No resource objects.
204    #[must_use]
205    pub fn new() -> Self {
206        Self::default()
207    }
208
209    /// Records the answer for `class` (as a selector writes it) in `source`:
210    /// its resource objects, or why they could not be listed.
211    #[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    /// Whether no class was answered at all.
235    #[must_use]
236    pub fn is_empty(&self) -> bool {
237        self.classes.is_empty()
238    }
239
240    /// The resource object `id`, if a class listed it.
241    #[must_use]
242    pub fn object(&self, id: &ObjectId) -> Option<&Object> {
243        self.objects.get(id)
244    }
245
246    /// The resource objects `selector` reaches, and the sources where it
247    /// reaches a class that could not be listed.
248    ///
249    /// An `entityType` selector reaches its class's resource objects; `allOf`
250    /// and `anyOf` reach what any operand reaches; a `ruleOutcome` selector
251    /// reaches the resource objects the named rule's outcomes name. Nothing
252    /// else reaches a resource object: not `all`, not a negation, not a
253    /// related or property selector alone. A reached object is selected only
254    /// when the whole selector then matches it.
255    #[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
322/// Every entity-type selector through which `selector` reaches resource
323/// objects: its class as written and whether subtypes are included.
324fn 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
341/// Installs the run's [`ResourceObjects`]: every class `rules`'
342/// applicability selectors name, asked of the registered resource service
343/// for every source, each native class once. Without a service, or when no
344/// rule names a class, the population is empty. A class a source's
345/// vocabulary does not bind reaches nothing there; its objects already
346/// report the unbound concept. An answer naming a project object is refused:
347/// the two populations never share an identity.
348pub(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}