use axioval_ir::{Evidence, ObjectId, Property, PropertyValue, is_reserved_set};
use regex::Regex;
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 invalid for its type")]
InvalidValue,
#[error("property source coverage is incomplete: {0}")]
Incomplete(String),
#[error("property evidence conflicts: {0}")]
Conflicting(String),
#[error("property resolution unavailable: {0}")]
Unavailable(String),
#[error("{0}")]
NotRecorded(String),
#[error("{0}")]
MissingService(String),
#[error("{}", .0.reason())]
UnreadableValue(Box<UnreadableValue>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UnreadableValue {
request: PropertyRequest,
data_type: String,
evidence: Evidence,
reason: String,
}
impl UnreadableValue {
pub fn try_new(
request: PropertyRequest,
data_type: impl Into<String>,
evidence: Evidence,
reason: impl Into<String>,
) -> Result<Self, PropertyResolutionError> {
let (data_type, reason) = (data_type.into(), reason.into());
if data_type.trim().is_empty() || reason.trim().is_empty() {
return Err(PropertyResolutionError::InvalidRequest);
}
if !reviewable(&evidence) || evidence.source != request.object_id().source {
return Err(PropertyResolutionError::InexactEvidence);
}
Ok(Self {
request,
data_type,
evidence,
reason,
})
}
pub fn request(&self) -> &PropertyRequest {
&self.request
}
pub fn data_type(&self) -> &str {
&self.data_type
}
pub fn evidence(&self) -> &Evidence {
&self.evidence
}
pub fn reason(&self) -> &str {
&self.reason
}
}
#[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 !admissible_evidence(&request, &property) {
return Err(PropertyResolutionError::InexactEvidence);
}
if !valid_property(&property) {
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),
}
#[derive(Clone, Debug)]
pub struct NamePattern {
pattern: String,
regex: Regex,
}
impl NamePattern {
pub fn new(pattern: impl Into<String>) -> Result<Self, PropertyResolutionError> {
let pattern = pattern.into();
let regex = Regex::new(&format!(r"\A(?:{pattern})\z"))
.map_err(|_| PropertyResolutionError::InvalidRequest)?;
Ok(Self { pattern, regex })
}
pub fn as_str(&self) -> &str {
&self.pattern
}
pub fn is_match(&self, name: &str) -> bool {
self.regex.is_match(name)
}
}
impl PartialEq for NamePattern {
fn eq(&self, other: &Self) -> bool {
self.pattern == other.pattern
}
}
impl Eq for NamePattern {}
impl PartialOrd for NamePattern {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for NamePattern {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.pattern.cmp(&other.pattern)
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum NameMatch {
Any,
Exact(String),
Pattern(NamePattern),
}
impl NameMatch {
pub fn matches(&self, name: &str) -> bool {
match self {
Self::Any => true,
Self::Exact(exact) => exact == name,
Self::Pattern(pattern) => pattern.is_match(name),
}
}
}
impl std::fmt::Display for NameMatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Any => f.write_str("*"),
Self::Exact(name) => f.write_str(name),
Self::Pattern(pattern) => write!(f, "/{}/", pattern.as_str()),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct PropertyEnumerationRequest {
object_id: ObjectId,
property_set: NameMatch,
property: NameMatch,
}
impl PropertyEnumerationRequest {
pub fn try_new(
object_id: ObjectId,
property_set: NameMatch,
property: NameMatch,
) -> Result<Self, PropertyResolutionError> {
let blank =
|name: &NameMatch| matches!(name, NameMatch::Exact(name) if name.trim().is_empty());
if blank(&property_set)
|| blank(&property)
|| matches!(&property_set, NameMatch::Exact(set) if is_reserved_set(set))
{
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) -> &NameMatch {
&self.property_set
}
pub fn property(&self) -> &NameMatch {
&self.property
}
pub fn selects(&self, property: &Property) -> bool {
!is_reserved_set(&property.property_set)
&& self.property_set.matches(&property.property_set)
&& self.property.matches(&property.name)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct PropertyEnumeration {
request: PropertyEnumerationRequest,
properties: Vec<Property>,
evidence: Evidence,
empty_sets: Vec<String>,
}
impl PropertyEnumeration {
pub fn try_new(
request: PropertyEnumerationRequest,
mut properties: Vec<Property>,
evidence: Evidence,
) -> Result<Self, PropertyResolutionError> {
let source = &request.object_id().source;
if !reviewable(&evidence) || evidence.source != *source {
return Err(PropertyResolutionError::InexactEvidence);
}
for property in &properties {
if !request.selects(property) {
return Err(PropertyResolutionError::ResponseRequestMismatch);
}
if !property
.evidence
.as_ref()
.is_some_and(|evidence| reviewable(evidence) && evidence.source == *source)
{
return Err(PropertyResolutionError::InexactEvidence);
}
if !valid_property(property) {
return Err(PropertyResolutionError::InvalidValue);
}
}
properties.sort_by(|a, b| (&a.property_set, &a.name).cmp(&(&b.property_set, &b.name)));
if let Some(pair) = properties.windows(2).find(|pair| {
pair[0].property_set == pair[1].property_set && pair[0].name == pair[1].name
}) {
return Err(PropertyResolutionError::Conflicting(format!(
"{}.{} is enumerated twice",
pair[0].property_set, pair[0].name
)));
}
Ok(Self {
request,
properties,
evidence,
empty_sets: Vec::new(),
})
}
pub fn with_empty_sets(
mut self,
sets: impl IntoIterator<Item = String>,
) -> Result<Self, PropertyResolutionError> {
let mut sets: Vec<String> = sets.into_iter().collect();
for set in &sets {
if is_reserved_set(set) || !self.request.property_set().matches(set) {
return Err(PropertyResolutionError::ResponseRequestMismatch);
}
if self
.properties
.iter()
.any(|property| property.property_set == *set)
{
return Err(PropertyResolutionError::Conflicting(format!(
"set {set} is reported empty and holds a property"
)));
}
}
sets.sort();
sets.dedup();
self.empty_sets = sets;
Ok(self)
}
pub fn request(&self) -> &PropertyEnumerationRequest {
&self.request
}
pub fn properties(&self) -> &[Property] {
&self.properties
}
pub fn evidence(&self) -> &Evidence {
&self.evidence
}
pub fn empty_sets(&self) -> &[String] {
&self.empty_sets
}
}
pub trait PropertyResolutionService: Send + Sync {
fn source_snapshots(&self) -> &[SourceSnapshot] {
&[]
}
fn resolve(
&self,
request: &PropertyRequest,
) -> Result<PropertyResolution, PropertyResolutionError>;
fn enumerate(
&self,
request: &PropertyEnumerationRequest,
) -> Result<PropertyEnumeration, PropertyResolutionError> {
let _ = request;
Err(PropertyResolutionError::Unavailable(
"this property source cannot enumerate an object's properties".into(),
))
}
}
#[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 = match self.service.resolve(request) {
Err(PropertyResolutionError::UnreadableValue(unreadable)) => {
if unreadable.request() != request {
return Err(PropertyResolutionError::ResponseRequestMismatch);
}
if !reviewable(unreadable.evidence())
|| unreadable.evidence().source != request.object_id().source
{
return Err(PropertyResolutionError::InexactEvidence);
}
return Err(PropertyResolutionError::UnreadableValue(unreadable));
}
other => other?,
};
match &resolution {
PropertyResolution::Present(resolved) => {
if resolved.request() != request || !request.matches(resolved.property()) {
return Err(PropertyResolutionError::ResponseRequestMismatch);
}
if !admissible_evidence(request, resolved.property()) {
return Err(PropertyResolutionError::InexactEvidence);
}
if !valid_property(resolved.property()) {
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)
}
pub fn enumerate(
&self,
request: &PropertyEnumerationRequest,
) -> Result<PropertyEnumeration, PropertyResolutionError> {
let enumeration = self.service.enumerate(request)?;
if enumeration.request() != request {
return Err(PropertyResolutionError::ResponseRequestMismatch);
}
Ok(enumeration)
}
}
impl SnapshotBoundService for PropertyResolutionServiceHandle {
fn source_snapshots(&self) -> &[SourceSnapshot] {
self.service.source_snapshots()
}
}
fn valid_property(property: &Property) -> bool {
valid_value(&property.value)
&& !(matches!(property.value, PropertyValue::Complex)
&& (property.data_type().is_some() || property.column_types().is_some()))
}
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(_)
| PropertyValue::Date(_)
| PropertyValue::DateTime(_)
| PropertyValue::Reference(_)
| PropertyValue::Complex => true,
PropertyValue::List(elements) => elements.iter().all(valid_scalar),
PropertyValue::Bounded { .. } => value.stated_values().is_some_and(|stated| {
!stated.is_empty()
&& stated.iter().all(|part| valid_scalar(part))
&& stated
.windows(2)
.all(|pair| std::mem::discriminant(pair[0]) == std::mem::discriminant(pair[1]))
}),
PropertyValue::Measured { lower, upper, .. } => {
lower.is_finite() && upper.is_finite() && lower <= upper
}
PropertyValue::Table(rows) => {
!rows.is_empty()
&& rows
.iter()
.all(|row| valid_scalar(&row.defining) && valid_scalar(&row.defined))
}
}
}
fn valid_scalar(value: &PropertyValue) -> bool {
value.is_scalar() && valid_value(value)
}
fn admissible_evidence(request: &PropertyRequest, property: &Property) -> bool {
let interval = matches!(property.value, PropertyValue::Measured { .. });
if let PropertyValue::Reference(target) = &property.value
&& target.source != request.object_id().source
{
return false;
}
property.evidence.as_ref().is_some_and(|evidence| {
(reviewable(evidence) || interval && !evidence.locator.trim().is_empty())
&& evidence.source == request.object_id().source
})
}
fn reviewable(evidence: &Evidence) -> bool {
evidence.exact && !evidence.locator.trim().is_empty()
}