axioval_engine/
properties.rs1use axioval_ir::{Evidence, ObjectId, Property, PropertyValue};
4use std::sync::Arc;
5use thiserror::Error;
6
7use crate::session::{SnapshotBoundService, SourceSnapshot};
8
9#[derive(Clone, Debug, Error, PartialEq, Eq)]
11pub enum PropertyResolutionError {
12 #[error("property request is invalid")]
14 InvalidRequest,
15 #[error("property response does not match its request")]
17 ResponseRequestMismatch,
18 #[error("property evidence is not exact and reviewable")]
20 InexactEvidence,
21 #[error("property value is not finite")]
23 InvalidValue,
24 #[error("property source coverage is incomplete: {0}")]
26 Incomplete(String),
27 #[error("property evidence conflicts: {0}")]
29 Conflicting(String),
30 #[error("property resolution unavailable: {0}")]
32 Unavailable(String),
33}
34
35#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
41pub struct PropertyRequest {
42 object_id: ObjectId,
43 property_set: Option<String>,
44 property: String,
45}
46impl PropertyRequest {
47 pub fn try_new(
49 object_id: ObjectId,
50 property_set: Option<String>,
51 property: impl Into<String>,
52 ) -> Result<Self, PropertyResolutionError> {
53 let property = property.into();
54 if property.trim().is_empty()
55 || property_set
56 .as_ref()
57 .is_some_and(|value| value.trim().is_empty())
58 {
59 return Err(PropertyResolutionError::InvalidRequest);
60 }
61 Ok(Self {
62 object_id,
63 property_set,
64 property,
65 })
66 }
67 pub fn object_id(&self) -> &ObjectId {
69 &self.object_id
70 }
71 pub fn property_set(&self) -> Option<&str> {
73 self.property_set.as_deref()
74 }
75 pub fn property(&self) -> &str {
77 &self.property
78 }
79 fn matches(&self, property: &Property) -> bool {
80 property.name == self.property
81 && self
82 .property_set
83 .as_ref()
84 .is_none_or(|set| property.property_set == *set)
85 }
86}
87
88#[derive(Clone, Debug, PartialEq)]
90pub struct CompletePropertyAbsenceEvidence {
91 request: PropertyRequest,
92 evidence: Evidence,
93}
94impl CompletePropertyAbsenceEvidence {
95 pub fn try_new(
97 request: PropertyRequest,
98 evidence: Evidence,
99 ) -> Result<Self, PropertyResolutionError> {
100 if !reviewable(&evidence) || evidence.source != request.object_id().source {
101 return Err(PropertyResolutionError::InexactEvidence);
102 }
103 Ok(Self { request, evidence })
104 }
105 pub fn request(&self) -> &PropertyRequest {
107 &self.request
108 }
109 pub fn evidence(&self) -> &Evidence {
111 &self.evidence
112 }
113}
114
115#[derive(Clone, Debug, PartialEq)]
117pub struct ResolvedProperty {
118 request: PropertyRequest,
119 property: Property,
120}
121impl ResolvedProperty {
122 pub fn try_new(
124 request: PropertyRequest,
125 property: Property,
126 ) -> Result<Self, PropertyResolutionError> {
127 if !request.matches(&property) {
128 return Err(PropertyResolutionError::ResponseRequestMismatch);
129 }
130 if !property.evidence.as_ref().is_some_and(|evidence| {
131 reviewable(evidence) && evidence.source == request.object_id().source
132 }) {
133 return Err(PropertyResolutionError::InexactEvidence);
134 }
135 if !valid_value(&property.value) {
136 return Err(PropertyResolutionError::InvalidValue);
137 }
138 Ok(Self { request, property })
139 }
140 pub fn request(&self) -> &PropertyRequest {
142 &self.request
143 }
144 pub fn property(&self) -> &Property {
146 &self.property
147 }
148}
149
150#[derive(Clone, Debug, PartialEq)]
152pub enum PropertyResolution {
153 Present(ResolvedProperty),
155 Absent(CompletePropertyAbsenceEvidence),
157}
158
159pub trait PropertyResolutionService: Send + Sync {
161 fn source_snapshots(&self) -> &[SourceSnapshot] {
166 &[]
167 }
168 fn resolve(
170 &self,
171 request: &PropertyRequest,
172 ) -> Result<PropertyResolution, PropertyResolutionError>;
173}
174
175#[derive(Clone)]
177pub struct PropertyResolutionServiceHandle {
178 service: Arc<dyn PropertyResolutionService>,
179}
180impl PropertyResolutionServiceHandle {
181 pub fn new(service: Arc<dyn PropertyResolutionService>) -> Self {
183 Self { service }
184 }
185 pub fn resolve(
187 &self,
188 request: &PropertyRequest,
189 ) -> Result<PropertyResolution, PropertyResolutionError> {
190 let resolution = self.service.resolve(request)?;
191 match &resolution {
192 PropertyResolution::Present(resolved) => {
193 if resolved.request() != request || !request.matches(resolved.property()) {
194 return Err(PropertyResolutionError::ResponseRequestMismatch);
195 }
196 if !resolved
197 .property()
198 .evidence
199 .as_ref()
200 .is_some_and(|evidence| {
201 reviewable(evidence) && evidence.source == request.object_id().source
202 })
203 {
204 return Err(PropertyResolutionError::InexactEvidence);
205 }
206 if !valid_value(&resolved.property().value) {
207 return Err(PropertyResolutionError::InvalidValue);
208 }
209 }
210 PropertyResolution::Absent(evidence) => {
211 if evidence.request() != request {
212 return Err(PropertyResolutionError::ResponseRequestMismatch);
213 }
214 if !reviewable(evidence.evidence())
215 || evidence.evidence().source != request.object_id().source
216 {
217 return Err(PropertyResolutionError::InexactEvidence);
218 }
219 }
220 }
221 Ok(resolution)
222 }
223}
224
225impl SnapshotBoundService for PropertyResolutionServiceHandle {
226 fn source_snapshots(&self) -> &[SourceSnapshot] {
227 self.service.source_snapshots()
228 }
229}
230
231fn valid_value(value: &PropertyValue) -> bool {
232 match value {
233 PropertyValue::Decimal(value) | PropertyValue::Quantity { value, .. } => value.is_finite(),
234 PropertyValue::Null
235 | PropertyValue::Boolean(_)
236 | PropertyValue::Integer(_)
237 | PropertyValue::String(_) => true,
238 }
239}
240
241fn reviewable(evidence: &Evidence) -> bool {
242 evidence.exact && !evidence.locator.trim().is_empty()
243}