axioval_engine/
properties.rs1use axioval_ir::{Evidence, ObjectId, Property, PropertyValue};
4use std::sync::Arc;
5use thiserror::Error;
6
7#[derive(Clone, Debug, Error, PartialEq, Eq)]
9pub enum PropertyResolutionError {
10 #[error("property request is invalid")]
12 InvalidRequest,
13 #[error("property response does not match its request")]
15 ResponseRequestMismatch,
16 #[error("property evidence is not exact and reviewable")]
18 InexactEvidence,
19 #[error("property value is not finite")]
21 InvalidValue,
22 #[error("property resolution unavailable: {0}")]
24 Unavailable(String),
25}
26
27#[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 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 pub fn object_id(&self) -> &ObjectId {
57 &self.object_id
58 }
59 pub fn property_set(&self) -> Option<&str> {
61 self.property_set.as_deref()
62 }
63 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#[derive(Clone, Debug, PartialEq)]
78pub struct CompletePropertyAbsenceEvidence {
79 request: PropertyRequest,
80 evidence: Evidence,
81}
82impl CompletePropertyAbsenceEvidence {
83 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 pub fn request(&self) -> &PropertyRequest {
95 &self.request
96 }
97 pub fn evidence(&self) -> &Evidence {
99 &self.evidence
100 }
101}
102
103#[derive(Clone, Debug, PartialEq)]
105pub struct ResolvedProperty {
106 request: PropertyRequest,
107 property: Property,
108}
109impl ResolvedProperty {
110 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 pub fn request(&self) -> &PropertyRequest {
128 &self.request
129 }
130 pub fn property(&self) -> &Property {
132 &self.property
133 }
134}
135
136#[derive(Clone, Debug, PartialEq)]
138pub enum PropertyResolution {
139 Present(ResolvedProperty),
141 Absent(CompletePropertyAbsenceEvidence),
143}
144
145pub trait PropertyResolutionService: Send + Sync {
147 fn resolve(
149 &self,
150 request: &PropertyRequest,
151 ) -> Result<PropertyResolution, PropertyResolutionError>;
152}
153
154#[derive(Clone)]
156pub struct PropertyResolutionServiceHandle(Arc<dyn PropertyResolutionService>);
157impl PropertyResolutionServiceHandle {
158 pub fn new(service: Arc<dyn PropertyResolutionService>) -> Self {
160 Self(service)
161 }
162 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}