Skip to main content

axioval_engine/
properties.rs

1//! Exact source-neutral property-resolution host-service contracts.
2
3use axioval_ir::{Evidence, ObjectId, Property, PropertyValue, is_reserved_set};
4use regex::Regex;
5use std::sync::Arc;
6use thiserror::Error;
7
8use crate::session::{SnapshotBoundService, SourceSnapshot};
9
10/// Failure to resolve a property conclusively.
11#[derive(Clone, Debug, Error, PartialEq, Eq)]
12pub enum PropertyResolutionError {
13    /// The requested property reference is malformed.
14    #[error("property request is invalid")]
15    InvalidRequest,
16    /// Returned data names another object or property.
17    #[error("property response does not match its request")]
18    ResponseRequestMismatch,
19    /// A conclusive answer lacks exact, reviewable provenance.
20    #[error("property evidence is not exact and reviewable")]
21    InexactEvidence,
22    /// A conclusive answer contains a value that is not a valid value of its
23    /// kind: a non-finite number, or text that is not the date its source
24    /// type declares.
25    #[error("property value is invalid for its type")]
26    InvalidValue,
27    /// The source could answer only part of the request scope.
28    #[error("property source coverage is incomplete: {0}")]
29    Incomplete(String),
30    /// Mutually incompatible exact facts were returned.
31    #[error("property evidence conflicts: {0}")]
32    Conflicting(String),
33    /// The source cannot currently provide a conclusive answer.
34    #[error("property resolution unavailable: {0}")]
35    Unavailable(String),
36    /// The source records this kind of property for no object at all (a
37    /// model with no presentation layers), so an object's missing value is
38    /// no evidence of absence. The message describes the source, never the
39    /// object, so every object of the source answers identically.
40    #[error("{0}")]
41    NotRecorded(String),
42    /// The run registered no service that could answer the request, for any
43    /// object (a measured value without geometry). The message describes
44    /// the run, never the object.
45    #[error("{0}")]
46    MissingService(String),
47    /// The property is present and states a value of a type the source
48    /// declares exactly, but the value cannot be read exactly (a measure
49    /// whose unit does not resolve). What depends on the declared type and
50    /// on presence alone is decided; the value is not.
51    #[error("{}", .0.reason())]
52    UnreadableValue(Box<UnreadableValue>),
53}
54
55/// A present property whose declared type is known exactly and whose
56/// stated value cannot be read exactly, bound to the request it answers.
57///
58/// The source states a value (never `$`): the property exists and holds
59/// one. Only the value itself is unknown, so a capability may decide
60/// presence, non-emptiness and the declared type, and must leave every
61/// comparison of the value not evaluated.
62#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct UnreadableValue {
64    request: PropertyRequest,
65    data_type: String,
66    evidence: Evidence,
67    reason: String,
68}
69impl UnreadableValue {
70    /// Creates a request-bound answer: the property holds a value of
71    /// `data_type`, in the source's own vocabulary, that cannot be read
72    /// exactly for `reason`.
73    ///
74    /// # Errors
75    ///
76    /// [`PropertyResolutionError::InvalidRequest`] for a blank type or
77    /// reason, and [`PropertyResolutionError::InexactEvidence`] for evidence
78    /// that is not exact and reviewable or not from the requested object's
79    /// source.
80    pub fn try_new(
81        request: PropertyRequest,
82        data_type: impl Into<String>,
83        evidence: Evidence,
84        reason: impl Into<String>,
85    ) -> Result<Self, PropertyResolutionError> {
86        let (data_type, reason) = (data_type.into(), reason.into());
87        if data_type.trim().is_empty() || reason.trim().is_empty() {
88            return Err(PropertyResolutionError::InvalidRequest);
89        }
90        if !reviewable(&evidence) || evidence.source != request.object_id().source {
91            return Err(PropertyResolutionError::InexactEvidence);
92        }
93        Ok(Self {
94            request,
95            data_type,
96            evidence,
97            reason,
98        })
99    }
100    /// Bound request.
101    pub fn request(&self) -> &PropertyRequest {
102        &self.request
103    }
104    /// The value's type as the source declares it (an IFC property:
105    /// `IFCMASSMEASURE`).
106    pub fn data_type(&self) -> &str {
107        &self.data_type
108    }
109    /// Exact reviewable provenance of the property.
110    pub fn evidence(&self) -> &Evidence {
111        &self.evidence
112    }
113    /// Why the value cannot be read exactly.
114    pub fn reason(&self) -> &str {
115        &self.reason
116    }
117}
118
119/// Request for one direct property on one source-qualified object.
120///
121/// This contract covers occurrence/type inheritance owned by the source adapter,
122/// but never traverses semantic relationships to other objects. Related-object
123/// selection requires a separately complete relationship service.
124#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
125pub struct PropertyRequest {
126    object_id: ObjectId,
127    property_set: Option<String>,
128    property: String,
129}
130impl PropertyRequest {
131    /// Creates a request. An omitted set requests an unambiguous property by name.
132    pub fn try_new(
133        object_id: ObjectId,
134        property_set: Option<String>,
135        property: impl Into<String>,
136    ) -> Result<Self, PropertyResolutionError> {
137        let property = property.into();
138        if property.trim().is_empty()
139            || property_set
140                .as_ref()
141                .is_some_and(|value| value.trim().is_empty())
142        {
143            return Err(PropertyResolutionError::InvalidRequest);
144        }
145        Ok(Self {
146            object_id,
147            property_set,
148            property,
149        })
150    }
151    /// Requested object.
152    pub fn object_id(&self) -> &ObjectId {
153        &self.object_id
154    }
155    /// Optional requested property set.
156    pub fn property_set(&self) -> Option<&str> {
157        self.property_set.as_deref()
158    }
159    /// Requested property name.
160    pub fn property(&self) -> &str {
161        &self.property
162    }
163    fn matches(&self, property: &Property) -> bool {
164        property.name == self.property
165            && self
166                .property_set
167                .as_ref()
168                .is_none_or(|set| property.property_set == *set)
169    }
170}
171
172/// Exact proof that a requested property is absent.
173#[derive(Clone, Debug, PartialEq)]
174pub struct CompletePropertyAbsenceEvidence {
175    request: PropertyRequest,
176    evidence: Evidence,
177}
178impl CompletePropertyAbsenceEvidence {
179    /// Creates request-bound exact absence evidence.
180    pub fn try_new(
181        request: PropertyRequest,
182        evidence: Evidence,
183    ) -> Result<Self, PropertyResolutionError> {
184        if !reviewable(&evidence) || evidence.source != request.object_id().source {
185            return Err(PropertyResolutionError::InexactEvidence);
186        }
187        Ok(Self { request, evidence })
188    }
189    /// Bound request.
190    pub fn request(&self) -> &PropertyRequest {
191        &self.request
192    }
193    /// Exact reviewable provenance.
194    pub fn evidence(&self) -> &Evidence {
195        &self.evidence
196    }
197}
198
199/// Exact property value bound to the request that produced it.
200#[derive(Clone, Debug, PartialEq)]
201pub struct ResolvedProperty {
202    request: PropertyRequest,
203    property: Property,
204}
205impl ResolvedProperty {
206    /// Creates an exact request-bound property value.
207    pub fn try_new(
208        request: PropertyRequest,
209        property: Property,
210    ) -> Result<Self, PropertyResolutionError> {
211        if !request.matches(&property) {
212            return Err(PropertyResolutionError::ResponseRequestMismatch);
213        }
214        if !admissible_evidence(&request, &property) {
215            return Err(PropertyResolutionError::InexactEvidence);
216        }
217        if !valid_property(&property) {
218            return Err(PropertyResolutionError::InvalidValue);
219        }
220        Ok(Self { request, property })
221    }
222    /// Bound request, including the source-qualified object identity.
223    pub fn request(&self) -> &PropertyRequest {
224        &self.request
225    }
226    /// Exact typed property and its reviewable provenance.
227    pub fn property(&self) -> &Property {
228        &self.property
229    }
230}
231
232/// Conclusive property result from a trusted source adapter.
233#[derive(Clone, Debug, PartialEq)]
234pub enum PropertyResolution {
235    /// The exact request-bound property value and its provenance.
236    Present(ResolvedProperty),
237    /// Exact proof that the requested property is absent.
238    Absent(CompletePropertyAbsenceEvidence),
239}
240
241/// A regular expression over whole property-set or property names.
242///
243/// The syntax is the `regex` crate's; callers holding an XML Schema pattern
244/// translate it first (`axioval_rules::translate_xsd_pattern`). The pattern
245/// always matches a whole name. Two patterns are equal when their text is.
246#[derive(Clone, Debug)]
247pub struct NamePattern {
248    pattern: String,
249    regex: Regex,
250}
251impl NamePattern {
252    /// Compiles `pattern` to match whole names.
253    ///
254    /// # Errors
255    ///
256    /// [`PropertyResolutionError::InvalidRequest`] when the pattern does not
257    /// compile.
258    pub fn new(pattern: impl Into<String>) -> Result<Self, PropertyResolutionError> {
259        let pattern = pattern.into();
260        let regex = Regex::new(&format!(r"\A(?:{pattern})\z"))
261            .map_err(|_| PropertyResolutionError::InvalidRequest)?;
262        Ok(Self { pattern, regex })
263    }
264    /// The pattern as written.
265    pub fn as_str(&self) -> &str {
266        &self.pattern
267    }
268    /// Whether the whole of `name` matches.
269    pub fn is_match(&self, name: &str) -> bool {
270        self.regex.is_match(name)
271    }
272}
273impl PartialEq for NamePattern {
274    fn eq(&self, other: &Self) -> bool {
275        self.pattern == other.pattern
276    }
277}
278impl Eq for NamePattern {}
279impl PartialOrd for NamePattern {
280    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
281        Some(self.cmp(other))
282    }
283}
284impl Ord for NamePattern {
285    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
286        self.pattern.cmp(&other.pattern)
287    }
288}
289
290/// Which property-set or property names an enumeration selects.
291#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
292pub enum NameMatch {
293    /// Every name.
294    Any,
295    /// One exact name, compared as the source spells it.
296    Exact(String),
297    /// Every name the pattern matches as a whole.
298    Pattern(NamePattern),
299}
300impl NameMatch {
301    /// Whether `name` is selected.
302    pub fn matches(&self, name: &str) -> bool {
303        match self {
304            Self::Any => true,
305            Self::Exact(exact) => exact == name,
306            Self::Pattern(pattern) => pattern.is_match(name),
307        }
308    }
309}
310impl std::fmt::Display for NameMatch {
311    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312        match self {
313            Self::Any => f.write_str("*"),
314            Self::Exact(name) => f.write_str(name),
315            Self::Pattern(pattern) => write!(f, "/{}/", pattern.as_str()),
316        }
317    }
318}
319
320/// Request for every property of one object whose set and name match.
321///
322/// Enumeration covers the source's own property sets, as
323/// [`PropertyRequest`] does with occurrence/type inheritance, and never the
324/// reserved sets ([`axioval_ir::is_reserved_set`]), which name engine
325/// vocabulary and are resolved by name only: an exact reserved set is an
326/// invalid request, and a pattern never selects one.
327#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
328pub struct PropertyEnumerationRequest {
329    object_id: ObjectId,
330    property_set: NameMatch,
331    property: NameMatch,
332}
333impl PropertyEnumerationRequest {
334    /// Creates a request.
335    ///
336    /// # Errors
337    ///
338    /// [`PropertyResolutionError::InvalidRequest`] for a blank exact name or
339    /// an exact reserved set.
340    pub fn try_new(
341        object_id: ObjectId,
342        property_set: NameMatch,
343        property: NameMatch,
344    ) -> Result<Self, PropertyResolutionError> {
345        let blank =
346            |name: &NameMatch| matches!(name, NameMatch::Exact(name) if name.trim().is_empty());
347        if blank(&property_set)
348            || blank(&property)
349            || matches!(&property_set, NameMatch::Exact(set) if is_reserved_set(set))
350        {
351            return Err(PropertyResolutionError::InvalidRequest);
352        }
353        Ok(Self {
354            object_id,
355            property_set,
356            property,
357        })
358    }
359    /// Requested object.
360    pub fn object_id(&self) -> &ObjectId {
361        &self.object_id
362    }
363    /// Which property sets are searched.
364    pub fn property_set(&self) -> &NameMatch {
365        &self.property_set
366    }
367    /// Which properties of those sets are selected.
368    pub fn property(&self) -> &NameMatch {
369        &self.property
370    }
371    /// Whether `property` is one the request selects.
372    pub fn selects(&self, property: &Property) -> bool {
373        !is_reserved_set(&property.property_set)
374            && self.property_set.matches(&property.property_set)
375            && self.property.matches(&property.name)
376    }
377}
378
379/// Every property an enumeration request selects, exactly, with the evidence
380/// that nothing else is selected.
381///
382/// Properties are sorted by set and name; no set and name occurs twice. An
383/// empty enumeration is an exact proof that the object has no selected
384/// property, as [`CompletePropertyAbsenceEvidence`] is for one name.
385///
386/// A set the object carries without any member holds no property, so it
387/// leaves no trace among the properties. A source that can tell such a set
388/// apart from no set at all names it in [`Self::empty_sets`], so a rule
389/// requiring a property in every selected set fails on it, as IDS requires.
390#[derive(Clone, Debug, PartialEq)]
391pub struct PropertyEnumeration {
392    request: PropertyEnumerationRequest,
393    properties: Vec<Property>,
394    evidence: Evidence,
395    empty_sets: Vec<String>,
396}
397impl PropertyEnumeration {
398    /// Creates a request-bound enumeration.
399    ///
400    /// # Errors
401    ///
402    /// [`PropertyResolutionError::InexactEvidence`] when the completeness
403    /// evidence or a property's evidence is not exact, reviewable and from
404    /// the object's source; [`PropertyResolutionError::ResponseRequestMismatch`]
405    /// for a property the request does not select;
406    /// [`PropertyResolutionError::InvalidValue`] for an invalid value; and
407    /// [`PropertyResolutionError::Conflicting`] when a set and name occur
408    /// twice.
409    pub fn try_new(
410        request: PropertyEnumerationRequest,
411        mut properties: Vec<Property>,
412        evidence: Evidence,
413    ) -> Result<Self, PropertyResolutionError> {
414        let source = &request.object_id().source;
415        if !reviewable(&evidence) || evidence.source != *source {
416            return Err(PropertyResolutionError::InexactEvidence);
417        }
418        for property in &properties {
419            if !request.selects(property) {
420                return Err(PropertyResolutionError::ResponseRequestMismatch);
421            }
422            if !property
423                .evidence
424                .as_ref()
425                .is_some_and(|evidence| reviewable(evidence) && evidence.source == *source)
426            {
427                return Err(PropertyResolutionError::InexactEvidence);
428            }
429            if !valid_property(property) {
430                return Err(PropertyResolutionError::InvalidValue);
431            }
432        }
433        properties.sort_by(|a, b| (&a.property_set, &a.name).cmp(&(&b.property_set, &b.name)));
434        if let Some(pair) = properties.windows(2).find(|pair| {
435            pair[0].property_set == pair[1].property_set && pair[0].name == pair[1].name
436        }) {
437            return Err(PropertyResolutionError::Conflicting(format!(
438                "{}.{} is enumerated twice",
439                pair[0].property_set, pair[0].name
440            )));
441        }
442        Ok(Self {
443            request,
444            properties,
445            evidence,
446            empty_sets: Vec::new(),
447        })
448    }
449    /// The same enumeration, also naming the selected sets the object carries
450    /// without any member, sorted and each once.
451    ///
452    /// # Errors
453    ///
454    /// [`PropertyResolutionError::ResponseRequestMismatch`] for a set the
455    /// request does not select, or of a reserved name; and
456    /// [`PropertyResolutionError::Conflicting`] for a set an enumerated
457    /// property is in, which is not empty.
458    pub fn with_empty_sets(
459        mut self,
460        sets: impl IntoIterator<Item = String>,
461    ) -> Result<Self, PropertyResolutionError> {
462        let mut sets: Vec<String> = sets.into_iter().collect();
463        for set in &sets {
464            if is_reserved_set(set) || !self.request.property_set().matches(set) {
465                return Err(PropertyResolutionError::ResponseRequestMismatch);
466            }
467            if self
468                .properties
469                .iter()
470                .any(|property| property.property_set == *set)
471            {
472                return Err(PropertyResolutionError::Conflicting(format!(
473                    "set {set} is reported empty and holds a property"
474                )));
475            }
476        }
477        sets.sort();
478        sets.dedup();
479        self.empty_sets = sets;
480        Ok(self)
481    }
482    /// Bound request.
483    pub fn request(&self) -> &PropertyEnumerationRequest {
484        &self.request
485    }
486    /// The selected properties, sorted by set and name.
487    pub fn properties(&self) -> &[Property] {
488        &self.properties
489    }
490    /// Exact reviewable evidence that the enumeration is complete.
491    pub fn evidence(&self) -> &Evidence {
492        &self.evidence
493    }
494    /// The selected sets the object carries without any member, sorted. The
495    /// same evidence proves there are no others; a source that cannot tell
496    /// an empty set from none reports none.
497    pub fn empty_sets(&self) -> &[String] {
498        &self.empty_sets
499    }
500}
501
502/// Trusted adapter seam for property resolution.
503pub trait PropertyResolutionService: Send + Sync {
504    /// Exact source snapshots used to construct this resolver.
505    ///
506    /// The default is intentionally unbound for services used only through a
507    /// raw [`crate::ServiceRegistry`]; an [`crate::EvidenceSession`] rejects it.
508    fn source_snapshots(&self) -> &[SourceSnapshot] {
509        &[]
510    }
511    /// Resolves one request or reports why it is not conclusive.
512    fn resolve(
513        &self,
514        request: &PropertyRequest,
515    ) -> Result<PropertyResolution, PropertyResolutionError>;
516    /// Enumerates every property of one object the request selects, or
517    /// reports why the enumeration is not conclusive.
518    ///
519    /// The default refuses: a source that cannot list an object's
520    /// properties never answers with an empty enumeration, which would be a
521    /// proof of absence.
522    fn enumerate(
523        &self,
524        request: &PropertyEnumerationRequest,
525    ) -> Result<PropertyEnumeration, PropertyResolutionError> {
526        let _ = request;
527        Err(PropertyResolutionError::Unavailable(
528            "this property source cannot enumerate an object's properties".into(),
529        ))
530    }
531}
532
533/// Cloneable, type-erased property service registered by the host.
534#[derive(Clone)]
535pub struct PropertyResolutionServiceHandle {
536    service: Arc<dyn PropertyResolutionService>,
537}
538impl PropertyResolutionServiceHandle {
539    /// Wraps a trusted service for use outside an evidence session.
540    pub fn new(service: Arc<dyn PropertyResolutionService>) -> Self {
541        Self { service }
542    }
543    /// Resolves and validates request binding and exact provenance.
544    ///
545    /// A [`PropertyResolutionError::UnreadableValue`] is an answer too: it
546    /// is bound to this very request and its evidence checked like a
547    /// present value's.
548    pub fn resolve(
549        &self,
550        request: &PropertyRequest,
551    ) -> Result<PropertyResolution, PropertyResolutionError> {
552        let resolution = match self.service.resolve(request) {
553            Err(PropertyResolutionError::UnreadableValue(unreadable)) => {
554                if unreadable.request() != request {
555                    return Err(PropertyResolutionError::ResponseRequestMismatch);
556                }
557                if !reviewable(unreadable.evidence())
558                    || unreadable.evidence().source != request.object_id().source
559                {
560                    return Err(PropertyResolutionError::InexactEvidence);
561                }
562                return Err(PropertyResolutionError::UnreadableValue(unreadable));
563            }
564            other => other?,
565        };
566        match &resolution {
567            PropertyResolution::Present(resolved) => {
568                if resolved.request() != request || !request.matches(resolved.property()) {
569                    return Err(PropertyResolutionError::ResponseRequestMismatch);
570                }
571                if !admissible_evidence(request, resolved.property()) {
572                    return Err(PropertyResolutionError::InexactEvidence);
573                }
574                if !valid_property(resolved.property()) {
575                    return Err(PropertyResolutionError::InvalidValue);
576                }
577            }
578            PropertyResolution::Absent(evidence) => {
579                if evidence.request() != request {
580                    return Err(PropertyResolutionError::ResponseRequestMismatch);
581                }
582                if !reviewable(evidence.evidence())
583                    || evidence.evidence().source != request.object_id().source
584                {
585                    return Err(PropertyResolutionError::InexactEvidence);
586                }
587            }
588        }
589        Ok(resolution)
590    }
591    /// Enumerates and validates request binding and exact provenance.
592    ///
593    /// The enumeration's own constructor already checked every property
594    /// against the request; this binds the answer to this very request.
595    pub fn enumerate(
596        &self,
597        request: &PropertyEnumerationRequest,
598    ) -> Result<PropertyEnumeration, PropertyResolutionError> {
599        let enumeration = self.service.enumerate(request)?;
600        if enumeration.request() != request {
601            return Err(PropertyResolutionError::ResponseRequestMismatch);
602        }
603        Ok(enumeration)
604    }
605}
606
607impl SnapshotBoundService for PropertyResolutionServiceHandle {
608    fn source_snapshots(&self) -> &[SourceSnapshot] {
609        self.service.source_snapshots()
610    }
611}
612
613/// Whether a property's value is well formed: `valid_value`, and a
614/// complex property declares no type, since it holds no value of one.
615fn valid_property(property: &Property) -> bool {
616    valid_value(&property.value)
617        && !(matches!(property.value, PropertyValue::Complex)
618            && (property.data_type().is_some() || property.column_types().is_some()))
619}
620
621fn valid_value(value: &PropertyValue) -> bool {
622    match value {
623        PropertyValue::Decimal(value) | PropertyValue::Quantity { value, .. } => value.is_finite(),
624        PropertyValue::Null
625        | PropertyValue::Boolean(_)
626        | PropertyValue::Integer(_)
627        | PropertyValue::String(_)
628        | PropertyValue::Date(_)
629        | PropertyValue::DateTime(_)
630        | PropertyValue::Reference(_)
631        | PropertyValue::Complex => true,
632        // A list holds scalar values only: no null, no nested composite.
633        PropertyValue::List(elements) => elements.iter().all(valid_scalar),
634        // A range states at least one scalar, and one kind of value.
635        PropertyValue::Bounded { .. } => value.stated_values().is_some_and(|stated| {
636            !stated.is_empty()
637                && stated.iter().all(|part| valid_scalar(part))
638                && stated
639                    .windows(2)
640                    .all(|pair| std::mem::discriminant(pair[0]) == std::mem::discriminant(pair[1]))
641        }),
642        PropertyValue::Measured { lower, upper, .. } => {
643            lower.is_finite() && upper.is_finite() && lower <= upper
644        }
645        PropertyValue::Table(rows) => {
646            !rows.is_empty()
647                && rows
648                    .iter()
649                    .all(|row| valid_scalar(&row.defining) && valid_scalar(&row.defined))
650        }
651    }
652}
653
654fn valid_scalar(value: &PropertyValue) -> bool {
655    value.is_scalar() && valid_value(value)
656}
657
658/// Whether `property` carries evidence from the requested object's source
659/// that a reviewer can follow: exact, or for a measured interval (which is
660/// never exact) at least located.
661fn admissible_evidence(request: &PropertyRequest, property: &Property) -> bool {
662    let interval = matches!(property.value, PropertyValue::Measured { .. });
663    // A reference names an instance of the requested object's own source.
664    if let PropertyValue::Reference(target) = &property.value
665        && target.source != request.object_id().source
666    {
667        return false;
668    }
669    property.evidence.as_ref().is_some_and(|evidence| {
670        (reviewable(evidence) || interval && !evidence.locator.trim().is_empty())
671            && evidence.source == request.object_id().source
672    })
673}
674
675fn reviewable(evidence: &Evidence) -> bool {
676    evidence.exact && !evidence.locator.trim().is_empty()
677}