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};
4use std::sync::Arc;
5use thiserror::Error;
6
7/// Failure to resolve a property conclusively.
8#[derive(Clone, Debug, Error, PartialEq, Eq)]
9pub enum PropertyResolutionError {
10    /// The requested property reference is malformed.
11    #[error("property request is invalid")]
12    InvalidRequest,
13    /// Returned data names another object or property.
14    #[error("property response does not match its request")]
15    ResponseRequestMismatch,
16    /// A conclusive answer lacks exact, reviewable provenance.
17    #[error("property evidence is not exact and reviewable")]
18    InexactEvidence,
19    /// A conclusive answer contains a non-finite numeric value.
20    #[error("property value is not finite")]
21    InvalidValue,
22    /// The source cannot currently provide a conclusive answer.
23    #[error("property resolution unavailable: {0}")]
24    Unavailable(String),
25}
26
27/// Request for one property on one source-qualified object.
28#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
29pub struct PropertyRequest {
30    object_id: ObjectId,
31    property_set: Option<String>,
32    property: String,
33}
34impl PropertyRequest {
35    /// Creates a request. An omitted set requests an unambiguous property by name.
36    pub fn try_new(
37        object_id: ObjectId,
38        property_set: Option<String>,
39        property: impl Into<String>,
40    ) -> Result<Self, PropertyResolutionError> {
41        let property = property.into();
42        if property.trim().is_empty()
43            || property_set
44                .as_ref()
45                .is_some_and(|value| value.trim().is_empty())
46        {
47            return Err(PropertyResolutionError::InvalidRequest);
48        }
49        Ok(Self {
50            object_id,
51            property_set,
52            property,
53        })
54    }
55    /// Requested object.
56    pub fn object_id(&self) -> &ObjectId {
57        &self.object_id
58    }
59    /// Optional requested property set.
60    pub fn property_set(&self) -> Option<&str> {
61        self.property_set.as_deref()
62    }
63    /// Requested property name.
64    pub fn property(&self) -> &str {
65        &self.property
66    }
67    fn matches(&self, property: &Property) -> bool {
68        property.name == self.property
69            && self
70                .property_set
71                .as_ref()
72                .is_none_or(|set| property.property_set == *set)
73    }
74}
75
76/// Exact proof that a requested property is absent.
77#[derive(Clone, Debug, PartialEq)]
78pub struct CompletePropertyAbsenceEvidence {
79    request: PropertyRequest,
80    evidence: Evidence,
81}
82impl CompletePropertyAbsenceEvidence {
83    /// Creates request-bound exact absence evidence.
84    pub fn try_new(
85        request: PropertyRequest,
86        evidence: Evidence,
87    ) -> Result<Self, PropertyResolutionError> {
88        if !reviewable(&evidence) {
89            return Err(PropertyResolutionError::InexactEvidence);
90        }
91        Ok(Self { request, evidence })
92    }
93    /// Bound request.
94    pub fn request(&self) -> &PropertyRequest {
95        &self.request
96    }
97    /// Exact reviewable provenance.
98    pub fn evidence(&self) -> &Evidence {
99        &self.evidence
100    }
101}
102
103/// Exact property value bound to the request that produced it.
104#[derive(Clone, Debug, PartialEq)]
105pub struct ResolvedProperty {
106    request: PropertyRequest,
107    property: Property,
108}
109impl ResolvedProperty {
110    /// Creates an exact request-bound property value.
111    pub fn try_new(
112        request: PropertyRequest,
113        property: Property,
114    ) -> Result<Self, PropertyResolutionError> {
115        if !request.matches(&property) {
116            return Err(PropertyResolutionError::ResponseRequestMismatch);
117        }
118        if !property.evidence.as_ref().is_some_and(reviewable) {
119            return Err(PropertyResolutionError::InexactEvidence);
120        }
121        if !valid_value(&property.value) {
122            return Err(PropertyResolutionError::InvalidValue);
123        }
124        Ok(Self { request, property })
125    }
126    /// Bound request, including the source-qualified object identity.
127    pub fn request(&self) -> &PropertyRequest {
128        &self.request
129    }
130    /// Exact typed property and its reviewable provenance.
131    pub fn property(&self) -> &Property {
132        &self.property
133    }
134}
135
136/// Conclusive property result from a trusted source adapter.
137#[derive(Clone, Debug, PartialEq)]
138pub enum PropertyResolution {
139    /// The exact request-bound property value and its provenance.
140    Present(ResolvedProperty),
141    /// Exact proof that the requested property is absent.
142    Absent(CompletePropertyAbsenceEvidence),
143}
144
145/// Trusted adapter seam for property resolution.
146pub trait PropertyResolutionService: Send + Sync {
147    /// Resolves one request or reports why it is not conclusive.
148    fn resolve(
149        &self,
150        request: &PropertyRequest,
151    ) -> Result<PropertyResolution, PropertyResolutionError>;
152}
153
154/// Cloneable, type-erased property service registered by the host.
155#[derive(Clone)]
156pub struct PropertyResolutionServiceHandle(Arc<dyn PropertyResolutionService>);
157impl PropertyResolutionServiceHandle {
158    /// Wraps a trusted service.
159    pub fn new(service: Arc<dyn PropertyResolutionService>) -> Self {
160        Self(service)
161    }
162    /// Resolves and validates request binding and exact provenance.
163    pub fn resolve(
164        &self,
165        request: &PropertyRequest,
166    ) -> Result<PropertyResolution, PropertyResolutionError> {
167        let resolution = self.0.resolve(request)?;
168        match &resolution {
169            PropertyResolution::Present(resolved) => {
170                if resolved.request() != request || !request.matches(resolved.property()) {
171                    return Err(PropertyResolutionError::ResponseRequestMismatch);
172                }
173                if !resolved
174                    .property()
175                    .evidence
176                    .as_ref()
177                    .is_some_and(reviewable)
178                {
179                    return Err(PropertyResolutionError::InexactEvidence);
180                }
181                if !valid_value(&resolved.property().value) {
182                    return Err(PropertyResolutionError::InvalidValue);
183                }
184            }
185            PropertyResolution::Absent(evidence) => {
186                if evidence.request() != request {
187                    return Err(PropertyResolutionError::ResponseRequestMismatch);
188                }
189                if !reviewable(evidence.evidence()) {
190                    return Err(PropertyResolutionError::InexactEvidence);
191                }
192            }
193        }
194        Ok(resolution)
195    }
196}
197
198fn valid_value(value: &PropertyValue) -> bool {
199    match value {
200        PropertyValue::Decimal(value) | PropertyValue::Quantity { value, .. } => value.is_finite(),
201        PropertyValue::Null
202        | PropertyValue::Boolean(_)
203        | PropertyValue::Integer(_)
204        | PropertyValue::String(_) => true,
205    }
206}
207
208fn reviewable(evidence: &Evidence) -> bool {
209    evidence.exact && !evidence.locator.trim().is_empty()
210}