Skip to main content

rustev_backend_rules/
check.rs

1//! The plan check a host runs at setup (spec 005, 3.11.4).
2
3use rustev_contract::Identified;
4use rustev_contract::definition::{ItemSource, SemanticDecl, StepBody, TypeDecl};
5use rustev_contract::plan::{Plan, PlanExecution, PlanStepDetail};
6
7use crate::RulesBackend;
8use crate::program::{CTask, FieldType};
9
10/// One way a plan's use of this backend does not match its program.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct PlanMismatch {
13    pub step: String,
14    pub kind: PlanMismatchKind,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum PlanMismatchKind {
19    /// The plan binds this backend id to a different descriptor.
20    Descriptor,
21    /// The program has no task with the step's task name.
22    NoTask(String),
23    /// The task exists but its operation, question or options differ.
24    TaskDiffers(&'static str),
25    /// A field reads a reference the step does not project.
26    NotProjected { field: String, reference: String },
27    /// A field's declared type does not match the definition's type there.
28    Incompatible { field: String, reference: String },
29}
30
31impl RulesBackend {
32    /// Check every semantic step `plan` binds to this backend, as primary or
33    /// runtime fallback target. Returns every mismatch; an empty plan use is
34    /// not an error.
35    pub fn check_plan(&self, plan: &Plan) -> Result<(), Vec<PlanMismatch>> {
36        let me = &self.descriptor.backend_id;
37        let my_descriptor = self.descriptor.id().ok();
38        let mut bound: Vec<(&str, bool)> = vec![];
39        for s in &plan.steps {
40            if let PlanStepDetail::Semantic(b) = &s.detail
41                && &b.backend_id == me
42            {
43                bound.push((&s.id, Some(&b.descriptor) == my_descriptor.as_ref()));
44            }
45        }
46        if let PlanExecution::Declared(d) = &plan.execution {
47            for f in d.fallbacks.iter().filter(|f| &f.backend_id == me) {
48                let entry = (
49                    f.step.as_str(),
50                    Some(&f.descriptor) == my_descriptor.as_ref(),
51                );
52                // A step is checked once however often it reaches this backend.
53                if !bound.contains(&entry) {
54                    bound.push(entry);
55                }
56            }
57        }
58        let mut out = vec![];
59        for (step, same_descriptor) in bound {
60            let mismatch = |kind| PlanMismatch {
61                step: step.to_string(),
62                kind,
63            };
64            if !same_descriptor {
65                out.push(mismatch(PlanMismatchKind::Descriptor));
66                continue;
67            }
68            let decl = plan.definition.steps.iter().find_map(|s| match &s.body {
69                StepBody::Semantic(d) if s.id == step => Some(d),
70                _ => None,
71            });
72            let Some(decl) = decl else { continue };
73            let Some(task) = self.tasks.iter().find(|t| t.name == decl.task) else {
74                out.push(mismatch(PlanMismatchKind::NoTask(decl.task.clone())));
75                continue;
76            };
77            if task.operation != decl.operation {
78                out.push(mismatch(PlanMismatchKind::TaskDiffers("operation")));
79            }
80            if task.question != decl.question {
81                out.push(mismatch(PlanMismatchKind::TaskDiffers("question")));
82            }
83            if task.options != decl.options {
84                out.push(mismatch(PlanMismatchKind::TaskDiffers("options")));
85            }
86            for kind in fields(task, decl, plan) {
87                out.push(mismatch(kind));
88            }
89        }
90        if out.is_empty() { Ok(()) } else { Err(out) }
91    }
92}
93
94fn fields(task: &CTask, decl: &SemanticDecl, plan: &Plan) -> Vec<PlanMismatchKind> {
95    let mut out = vec![];
96    for f in &task.fields {
97        let reference = match &f.member {
98            Some(m) => format!("{}/{m}", f.key),
99            None => f.key.clone(),
100        };
101        if !decl.project.contains(&f.key) {
102            out.push(PlanMismatchKind::NotProjected {
103                field: f.name.clone(),
104                reference,
105            });
106            continue;
107        }
108        let ty = declared_type(&f.key, decl, plan).and_then(|t| match &f.member {
109            None => Some(t),
110            Some(m) => match t {
111                TypeDecl::Record { fields, .. } => {
112                    fields.iter().find(|x| &x.name == m).map(|x| &x.ty)
113                }
114                _ => None,
115            },
116        });
117        if !ty.is_some_and(|t| compatible(t, f.ty)) {
118            out.push(PlanMismatchKind::Incompatible {
119                field: f.name.clone(),
120                reference,
121            });
122        }
123    }
124    out
125}
126
127/// The definition's type at a projection key: an input's type, or a bound
128/// item's record type through the step's `for_each` list.
129fn declared_type<'p>(key: &str, decl: &SemanticDecl, plan: &'p Plan) -> Option<&'p TypeDecl> {
130    let input = |name: &str| {
131        plan.definition
132            .inputs
133            .iter()
134            .find(|i| i.name == name)
135            .map(|i| &i.ty)
136    };
137    if let Some(name) = key.strip_prefix("input:") {
138        return input(name);
139    }
140    let bind = key.strip_prefix("bind:")?;
141    let f = decl.for_each.iter().find(|f| f.bind == bind)?;
142    let ItemSource::List { list, .. } = &f.items else {
143        return None;
144    };
145    match input(list.strip_prefix("input:")?)? {
146        TypeDecl::List { item, .. } => Some(item),
147        _ => None,
148    }
149}
150
151fn compatible(t: &TypeDecl, f: FieldType) -> bool {
152    matches!(
153        (t, f),
154        (
155            TypeDecl::Text { .. } | TypeDecl::Enum { .. },
156            FieldType::Text
157        ) | (TypeDecl::Bool, FieldType::Bool)
158            | (TypeDecl::Integer | TypeDecl::Timestamp, FieldType::Integer)
159            | (TypeDecl::Decimal, FieldType::Decimal)
160    )
161}