use axioval_ir::{Evidence, ObjectId, Property, PropertyValue};
use std::sync::Arc;
use thiserror::Error;
use crate::session::{SnapshotBoundService, SourceSnapshot};
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum PropertyResolutionError {
#[error("property request is invalid")]
InvalidRequest,
#[error("property response does not match its request")]
ResponseRequestMismatch,
#[error("property evidence is not exact and reviewable")]
InexactEvidence,
#[error("property value is not finite")]
InvalidValue,
#[error("property source coverage is incomplete: {0}")]
Incomplete(String),
#[error("property evidence conflicts: {0}")]
Conflicting(String),
#[error("property resolution unavailable: {0}")]
Unavailable(String),
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct PropertyRequest {
object_id: ObjectId,
property_set: Option<String>,
property: String,
}
impl PropertyRequest {
pub fn try_new(
object_id: ObjectId,
property_set: Option<String>,
property: impl Into<String>,
) -> Result<Self, PropertyResolutionError> {
let property = property.into();
if property.trim().is_empty()
|| property_set
.as_ref()
.is_some_and(|value| value.trim().is_empty())
{
return Err(PropertyResolutionError::InvalidRequest);
}
Ok(Self {
object_id,
property_set,
property,
})
}
pub fn object_id(&self) -> &ObjectId {
&self.object_id
}
pub fn property_set(&self) -> Option<&str> {
self.property_set.as_deref()
}
pub fn property(&self) -> &str {
&self.property
}
fn matches(&self, property: &Property) -> bool {
property.name == self.property
&& self
.property_set
.as_ref()
.is_none_or(|set| property.property_set == *set)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CompletePropertyAbsenceEvidence {
request: PropertyRequest,
evidence: Evidence,
}
impl CompletePropertyAbsenceEvidence {
pub fn try_new(
request: PropertyRequest,
evidence: Evidence,
) -> Result<Self, PropertyResolutionError> {
if !reviewable(&evidence) || evidence.source != request.object_id().source {
return Err(PropertyResolutionError::InexactEvidence);
}
Ok(Self { request, evidence })
}
pub fn request(&self) -> &PropertyRequest {
&self.request
}
pub fn evidence(&self) -> &Evidence {
&self.evidence
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ResolvedProperty {
request: PropertyRequest,
property: Property,
}
impl ResolvedProperty {
pub fn try_new(
request: PropertyRequest,
property: Property,
) -> Result<Self, PropertyResolutionError> {
if !request.matches(&property) {
return Err(PropertyResolutionError::ResponseRequestMismatch);
}
if !property.evidence.as_ref().is_some_and(|evidence| {
reviewable(evidence) && evidence.source == request.object_id().source
}) {
return Err(PropertyResolutionError::InexactEvidence);
}
if !valid_value(&property.value) {
return Err(PropertyResolutionError::InvalidValue);
}
Ok(Self { request, property })
}
pub fn request(&self) -> &PropertyRequest {
&self.request
}
pub fn property(&self) -> &Property {
&self.property
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum PropertyResolution {
Present(ResolvedProperty),
Absent(CompletePropertyAbsenceEvidence),
}
pub trait PropertyResolutionService: Send + Sync {
fn source_snapshots(&self) -> &[SourceSnapshot] {
&[]
}
fn resolve(
&self,
request: &PropertyRequest,
) -> Result<PropertyResolution, PropertyResolutionError>;
}
#[derive(Clone)]
pub struct PropertyResolutionServiceHandle {
service: Arc<dyn PropertyResolutionService>,
}
impl PropertyResolutionServiceHandle {
pub fn new(service: Arc<dyn PropertyResolutionService>) -> Self {
Self { service }
}
pub fn resolve(
&self,
request: &PropertyRequest,
) -> Result<PropertyResolution, PropertyResolutionError> {
let resolution = self.service.resolve(request)?;
match &resolution {
PropertyResolution::Present(resolved) => {
if resolved.request() != request || !request.matches(resolved.property()) {
return Err(PropertyResolutionError::ResponseRequestMismatch);
}
if !resolved
.property()
.evidence
.as_ref()
.is_some_and(|evidence| {
reviewable(evidence) && evidence.source == request.object_id().source
})
{
return Err(PropertyResolutionError::InexactEvidence);
}
if !valid_value(&resolved.property().value) {
return Err(PropertyResolutionError::InvalidValue);
}
}
PropertyResolution::Absent(evidence) => {
if evidence.request() != request {
return Err(PropertyResolutionError::ResponseRequestMismatch);
}
if !reviewable(evidence.evidence())
|| evidence.evidence().source != request.object_id().source
{
return Err(PropertyResolutionError::InexactEvidence);
}
}
}
Ok(resolution)
}
}
impl SnapshotBoundService for PropertyResolutionServiceHandle {
fn source_snapshots(&self) -> &[SourceSnapshot] {
self.service.source_snapshots()
}
}
fn valid_value(value: &PropertyValue) -> bool {
match value {
PropertyValue::Decimal(value) | PropertyValue::Quantity { value, .. } => value.is_finite(),
PropertyValue::Null
| PropertyValue::Boolean(_)
| PropertyValue::Integer(_)
| PropertyValue::String(_) => true,
}
}
fn reviewable(evidence: &Evidence) -> bool {
evidence.exact && !evidence.locator.trim().is_empty()
}