use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use axioval_ir::contract::Selector;
use axioval_ir::{Object, ObjectId, Project, SourceId};
use thiserror::Error;
use crate::{
CompiledRule, ConceptBindings, RuleOutcomes, ServiceRegistry, SessionSources,
SnapshotBoundService, SourceSnapshot,
};
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct ResourceRequest {
source: SourceId,
class: String,
include_subtypes: bool,
}
impl ResourceRequest {
pub fn try_new(
source: SourceId,
class: impl Into<String>,
include_subtypes: bool,
) -> Result<Self, ResourceError> {
let class = class.into();
if class.trim().is_empty() {
return Err(ResourceError::InvalidRequest(
"a resource class name must not be blank".into(),
));
}
Ok(Self {
source,
class,
include_subtypes,
})
}
#[must_use]
pub fn source(&self) -> &SourceId {
&self.source
}
#[must_use]
pub fn class(&self) -> &str {
&self.class
}
#[must_use]
pub fn include_subtypes(&self) -> bool {
self.include_subtypes
}
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum ResourceError {
#[error("resource service does not cover source `{0}`")]
UncoveredSource(SourceId),
#[error("invalid resource request: {0}")]
InvalidRequest(String),
#[error("resource objects cannot be listed exactly: {0}")]
Unreadable(String),
#[error("resource service answered out of contract: {0}")]
InvalidAnswer(String),
}
pub trait ResourceService: Send + Sync {
fn source_snapshots(&self) -> &[SourceSnapshot];
fn resources(&self, request: &ResourceRequest) -> Result<Vec<Object>, ResourceError>;
}
#[derive(Clone)]
pub struct ResourceServiceHandle(Arc<dyn ResourceService>);
impl ResourceServiceHandle {
#[must_use]
pub fn new(service: Arc<dyn ResourceService>) -> Self {
Self(service)
}
pub fn resources(&self, request: &ResourceRequest) -> Result<Vec<Object>, ResourceError> {
if !self
.0
.source_snapshots()
.iter()
.any(|snapshot| *snapshot.source() == request.source)
{
return Err(ResourceError::UncoveredSource(request.source.clone()));
}
let objects = self.0.resources(request)?;
for object in &objects {
if object.id.source != request.source {
return Err(ResourceError::InvalidAnswer(format!(
"{} is not of source `{}`",
object.id, request.source
)));
}
if !object.properties.is_empty()
|| !object.classifications.is_empty()
|| !object.relationships.is_empty()
{
return Err(ResourceError::InvalidAnswer(format!(
"{} carries facts of its own",
object.id
)));
}
}
if let Some(pair) = objects.windows(2).find(|pair| pair[0].id >= pair[1].id) {
return Err(ResourceError::InvalidAnswer(format!(
"{} is listed out of order or twice",
pair[1].id
)));
}
Ok(objects)
}
}
impl SnapshotBoundService for ResourceServiceHandle {
fn source_snapshots(&self) -> &[SourceSnapshot] {
self.0.source_snapshots()
}
}
#[derive(Clone, Debug, Default)]
pub struct ResourceObjects {
classes: BTreeMap<(SourceId, String, bool), Result<Vec<ObjectId>, String>>,
objects: BTreeMap<ObjectId, Object>,
}
#[derive(Debug, Default)]
pub struct Reached<'a> {
pub objects: Vec<&'a Object>,
pub unreadable: Vec<(SourceId, String)>,
}
impl ResourceObjects {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_class(
mut self,
source: SourceId,
class: impl Into<String>,
include_subtypes: bool,
answer: Result<Vec<Object>, String>,
) -> Self {
let answer = answer.map(|objects| {
objects
.into_iter()
.map(|object| {
let id = object.id.clone();
self.objects.entry(id.clone()).or_insert(object);
id
})
.collect()
});
self.classes
.insert((source, class.into(), include_subtypes), answer);
self
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.classes.is_empty()
}
#[must_use]
pub fn object(&self, id: &ObjectId) -> Option<&Object> {
self.objects.get(id)
}
#[must_use]
pub fn reached<'s>(
&'s self,
selector: &Selector,
outcomes: Option<&RuleOutcomes>,
) -> Reached<'s> {
let mut ids = BTreeSet::new();
let mut unreadable = BTreeSet::new();
self.reach(selector, outcomes, &mut ids, &mut unreadable);
Reached {
objects: ids.iter().filter_map(|id| self.objects.get(*id)).collect(),
unreadable: unreadable.into_iter().collect(),
}
}
fn reach<'s>(
&'s self,
selector: &Selector,
outcomes: Option<&RuleOutcomes>,
ids: &mut BTreeSet<&'s ObjectId>,
unreadable: &mut BTreeSet<(SourceId, String)>,
) {
match selector {
Selector::EntityType {
object_type,
include_subtypes,
} => {
for ((source, class, subtypes), answer) in &self.classes {
if class != object_type || subtypes != include_subtypes {
continue;
}
match answer {
Ok(listed) => ids.extend(listed),
Err(why) => {
unreadable.insert((source.clone(), why.clone()));
}
}
}
}
Selector::AllOf { operands } | Selector::AnyOf { operands } => {
for operand in operands {
self.reach(operand, outcomes, ids, unreadable);
}
}
Selector::RuleOutcome { rule, .. } => {
let Some(record) = outcomes.and_then(|outcomes| outcomes.get(rule)) else {
return;
};
ids.extend(
record
.named()
.filter_map(|id| self.objects.get_key_value(id).map(|(id, _)| id)),
);
unreadable.extend(record.unread_resources().cloned());
}
Selector::All
| Selector::Not { .. }
| Selector::Related { .. }
| Selector::Property { .. }
| Selector::PropertyPattern { .. }
| Selector::Classification { .. }
| Selector::Discipline { .. }
| Selector::Source { .. } => {}
}
}
}
fn named_classes<'s>(selector: &'s Selector, out: &mut BTreeSet<(&'s str, bool)>) {
match selector {
Selector::EntityType {
object_type,
include_subtypes,
} => {
out.insert((object_type, *include_subtypes));
}
Selector::AllOf { operands } | Selector::AnyOf { operands } => {
for operand in operands {
named_classes(operand, out);
}
}
_ => {}
}
}
pub(crate) fn install(services: &mut ServiceRegistry, project: &Project, rules: &[CompiledRule]) {
let mut population = ResourceObjects::new();
let handle = services.get::<ResourceServiceHandle>().cloned();
if let Some(handle) = handle {
let mut named = BTreeSet::new();
for rule in rules {
named_classes(&rule.selector, &mut named);
}
let sources: Vec<SourceId> = services
.get::<SessionSources>()
.map(|sources| sources.iter().cloned().collect())
.unwrap_or_default();
let bindings = services.get::<ConceptBindings>();
let mut asked: BTreeMap<(SourceId, String, bool), Result<Vec<Object>, String>> =
BTreeMap::new();
for source in &sources {
for (written, subtypes) in &named {
let native = match bindings {
Some(bindings) => match bindings.object_type(written, source) {
Ok(native) => native.to_owned(),
Err(_) => continue,
},
None => (*written).to_owned(),
};
let key = (source.clone(), native.to_ascii_uppercase(), *subtypes);
let answer = asked
.entry(key)
.or_insert_with(|| {
ResourceRequest::try_new(source.clone(), native, *subtypes)
.and_then(|request| handle.resources(&request))
.and_then(|objects| {
match objects
.iter()
.find(|object| project.object(&object.id).is_some())
{
Some(object) => Err(ResourceError::InvalidAnswer(format!(
"{} is an object, not a resource object",
object.id
))),
None => Ok(objects),
}
})
.map_err(|error| error.to_string())
})
.clone();
population = population.with_class(source.clone(), *written, *subtypes, answer);
}
}
}
services.replace(population);
}