1use 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#[derive(Clone, Debug, Error, PartialEq, Eq)]
12pub enum PropertyResolutionError {
13 #[error("property request is invalid")]
15 InvalidRequest,
16 #[error("property response does not match its request")]
18 ResponseRequestMismatch,
19 #[error("property evidence is not exact and reviewable")]
21 InexactEvidence,
22 #[error("property value is invalid for its type")]
26 InvalidValue,
27 #[error("property source coverage is incomplete: {0}")]
29 Incomplete(String),
30 #[error("property evidence conflicts: {0}")]
32 Conflicting(String),
33 #[error("property resolution unavailable: {0}")]
35 Unavailable(String),
36 #[error("{0}")]
41 NotRecorded(String),
42 #[error("{0}")]
46 MissingService(String),
47 #[error("{}", .0.reason())]
52 UnreadableValue(Box<UnreadableValue>),
53}
54
55#[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 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 pub fn request(&self) -> &PropertyRequest {
102 &self.request
103 }
104 pub fn data_type(&self) -> &str {
107 &self.data_type
108 }
109 pub fn evidence(&self) -> &Evidence {
111 &self.evidence
112 }
113 pub fn reason(&self) -> &str {
115 &self.reason
116 }
117}
118
119#[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 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 pub fn object_id(&self) -> &ObjectId {
153 &self.object_id
154 }
155 pub fn property_set(&self) -> Option<&str> {
157 self.property_set.as_deref()
158 }
159 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#[derive(Clone, Debug, PartialEq)]
174pub struct CompletePropertyAbsenceEvidence {
175 request: PropertyRequest,
176 evidence: Evidence,
177}
178impl CompletePropertyAbsenceEvidence {
179 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 pub fn request(&self) -> &PropertyRequest {
191 &self.request
192 }
193 pub fn evidence(&self) -> &Evidence {
195 &self.evidence
196 }
197}
198
199#[derive(Clone, Debug, PartialEq)]
201pub struct ResolvedProperty {
202 request: PropertyRequest,
203 property: Property,
204}
205impl ResolvedProperty {
206 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 pub fn request(&self) -> &PropertyRequest {
224 &self.request
225 }
226 pub fn property(&self) -> &Property {
228 &self.property
229 }
230}
231
232#[derive(Clone, Debug, PartialEq)]
234pub enum PropertyResolution {
235 Present(ResolvedProperty),
237 Absent(CompletePropertyAbsenceEvidence),
239}
240
241#[derive(Clone, Debug)]
247pub struct NamePattern {
248 pattern: String,
249 regex: Regex,
250}
251impl NamePattern {
252 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 pub fn as_str(&self) -> &str {
266 &self.pattern
267 }
268 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#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
292pub enum NameMatch {
293 Any,
295 Exact(String),
297 Pattern(NamePattern),
299}
300impl NameMatch {
301 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#[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 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 pub fn object_id(&self) -> &ObjectId {
361 &self.object_id
362 }
363 pub fn property_set(&self) -> &NameMatch {
365 &self.property_set
366 }
367 pub fn property(&self) -> &NameMatch {
369 &self.property
370 }
371 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#[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 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 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 pub fn request(&self) -> &PropertyEnumerationRequest {
484 &self.request
485 }
486 pub fn properties(&self) -> &[Property] {
488 &self.properties
489 }
490 pub fn evidence(&self) -> &Evidence {
492 &self.evidence
493 }
494 pub fn empty_sets(&self) -> &[String] {
498 &self.empty_sets
499 }
500}
501
502pub trait PropertyResolutionService: Send + Sync {
504 fn source_snapshots(&self) -> &[SourceSnapshot] {
509 &[]
510 }
511 fn resolve(
513 &self,
514 request: &PropertyRequest,
515 ) -> Result<PropertyResolution, PropertyResolutionError>;
516 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#[derive(Clone)]
535pub struct PropertyResolutionServiceHandle {
536 service: Arc<dyn PropertyResolutionService>,
537}
538impl PropertyResolutionServiceHandle {
539 pub fn new(service: Arc<dyn PropertyResolutionService>) -> Self {
541 Self { service }
542 }
543 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 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
613fn 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 PropertyValue::List(elements) => elements.iter().all(valid_scalar),
634 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
658fn admissible_evidence(request: &PropertyRequest, property: &Property) -> bool {
662 let interval = matches!(property.value, PropertyValue::Measured { .. });
663 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}