use rustev_contract::Identified;
use rustev_contract::definition::{ItemSource, SemanticDecl, StepBody, TypeDecl};
use rustev_contract::plan::{Plan, PlanExecution, PlanStepDetail};
use crate::RulesBackend;
use crate::program::{CTask, FieldType};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlanMismatch {
pub step: String,
pub kind: PlanMismatchKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PlanMismatchKind {
Descriptor,
NoTask(String),
TaskDiffers(&'static str),
NotProjected { field: String, reference: String },
Incompatible { field: String, reference: String },
}
impl RulesBackend {
pub fn check_plan(&self, plan: &Plan) -> Result<(), Vec<PlanMismatch>> {
let me = &self.descriptor.backend_id;
let my_descriptor = self.descriptor.id().ok();
let mut bound: Vec<(&str, bool)> = vec![];
for s in &plan.steps {
let binding = match &s.detail {
PlanStepDetail::Semantic(b) => Some((&b.backend_id, &b.descriptor)),
PlanStepDetail::Rank(b) => Some((&b.backend_id, &b.descriptor)),
_ => None,
};
if let Some((backend, descriptor)) = binding
&& backend == me
{
bound.push((&s.id, Some(descriptor) == my_descriptor.as_ref()));
}
}
if let PlanExecution::Declared(d) = &plan.execution {
for f in d.fallbacks.iter().filter(|f| &f.backend_id == me) {
let entry = (
f.step.as_str(),
Some(&f.descriptor) == my_descriptor.as_ref(),
);
if !bound.contains(&entry) {
bound.push(entry);
}
}
}
let mut out = vec![];
for (step, same_descriptor) in bound {
let mismatch = |kind| PlanMismatch {
step: step.to_string(),
kind,
};
if !same_descriptor {
out.push(mismatch(PlanMismatchKind::Descriptor));
continue;
}
let decl = plan.definition.steps.iter().find_map(|s| match &s.body {
StepBody::Semantic(d) if s.id == step => Some(d.clone()),
StepBody::Rank(r) if s.id == step => {
use rustev_contract::definition::*;
if r.mode != rustev_contract::ranking::RankMode::Pointwise {
out.push(mismatch(PlanMismatchKind::TaskDiffers("mode")));
}
let mut project = r.project.clone();
project.push(format!("bind:{}", r.bind));
Some(SemanticDecl {
operation: Operation::Rank,
task: r.task.clone(),
question: r.question.clone(),
options: vec!["score".into()],
candidates: Candidates::From(r.candidates.clone()),
requires: RequiredKind::Scores,
project,
for_each: vec![ForEach {
over: r.candidates.clone(),
bind: r.bind.clone(),
items: r.items.clone(),
}],
backend: r.backend.clone(),
calibration: CalibrationRef::None,
fallback: Fallback::None,
})
}
_ => None,
});
let Some(decl) = decl else { continue };
let Some(task) = self.tasks.iter().find(|t| t.name == decl.task) else {
out.push(mismatch(PlanMismatchKind::NoTask(decl.task.clone())));
continue;
};
if task.operation != decl.operation {
out.push(mismatch(PlanMismatchKind::TaskDiffers("operation")));
}
if task.question != decl.question {
out.push(mismatch(PlanMismatchKind::TaskDiffers("question")));
}
if task.options != decl.options {
out.push(mismatch(PlanMismatchKind::TaskDiffers("options")));
}
for kind in fields(task, &decl, plan) {
out.push(mismatch(kind));
}
}
if out.is_empty() { Ok(()) } else { Err(out) }
}
}
fn fields(task: &CTask, decl: &SemanticDecl, plan: &Plan) -> Vec<PlanMismatchKind> {
let mut out = vec![];
for f in &task.fields {
let reference = match &f.member {
Some(m) => format!("{}/{m}", f.key),
None => f.key.clone(),
};
if !decl.project.contains(&f.key) {
out.push(PlanMismatchKind::NotProjected {
field: f.name.clone(),
reference,
});
continue;
}
let ty = declared_type(&f.key, decl, plan).and_then(|t| match &f.member {
None => Some(t),
Some(m) => match t {
TypeDecl::Record { fields, .. } => {
fields.iter().find(|x| &x.name == m).map(|x| &x.ty)
}
_ => None,
},
});
if !ty.is_some_and(|t| compatible(t, f.ty)) {
out.push(PlanMismatchKind::Incompatible {
field: f.name.clone(),
reference,
});
}
}
out
}
fn declared_type<'p>(key: &str, decl: &SemanticDecl, plan: &'p Plan) -> Option<&'p TypeDecl> {
let input = |name: &str| {
plan.definition
.inputs
.iter()
.find(|i| i.name == name)
.map(|i| &i.ty)
};
if let Some(name) = key.strip_prefix("input:") {
return input(name);
}
let bind = key.strip_prefix("bind:")?;
let f = decl.for_each.iter().find(|f| f.bind == bind)?;
let ItemSource::List { list, .. } = &f.items else {
return None;
};
match input(list.strip_prefix("input:")?)? {
TypeDecl::List { item, .. } => Some(item),
_ => None,
}
}
fn compatible(t: &TypeDecl, f: FieldType) -> bool {
matches!(
(t, f),
(
TypeDecl::Text { .. } | TypeDecl::Enum { .. },
FieldType::Text
) | (TypeDecl::Bool, FieldType::Bool)
| (TypeDecl::Integer | TypeDecl::Timestamp, FieldType::Integer)
| (TypeDecl::Decimal, FieldType::Decimal)
)
}