use std::collections::HashSet;
use crate::contract::{SectionOpts, Spec};
use crate::tree::SectionNode;
pub(crate) struct Slot<'a> {
pub(crate) spec_idx: usize,
pub(crate) names: &'a [String],
pub(crate) optional: bool,
pub(crate) repeatable: bool,
pub(crate) min: Option<usize>,
pub(crate) max: Option<usize>,
pub(crate) opts: Option<&'a SectionOpts>,
}
impl<'a> Slot<'a> {
pub(crate) fn admits(&self, name: &str) -> bool {
self.names.iter().any(|n| n == name)
}
pub(crate) fn label(&self) -> String {
self.names.join("’ / ‘")
}
pub(crate) fn hint<'h>(&self, level: Option<&'h str>) -> Option<&'h str>
where
'a: 'h,
{
self.opts.and_then(|o| o.description.as_deref()).or(level)
}
}
fn make_slot<'a>(
spec_idx: usize,
names: &'a [String],
optional: bool,
opts: Option<&'a SectionOpts>,
) -> Slot<'a> {
Slot {
spec_idx,
names,
optional: optional || opts.is_some_and(|o| o.optional),
repeatable: opts.is_some_and(|o| o.repeatable),
min: opts.and_then(|o| o.min),
max: opts.and_then(|o| o.max),
opts,
}
}
pub(crate) fn slots_of(specs: &[Spec]) -> Vec<Slot<'_>> {
let mut slots = Vec::new();
for (spec_idx, spec) in specs.iter().enumerate() {
let (inner, optional) = spec.unwrap_optional();
match inner {
Spec::Section(s) => {
slots.push(make_slot(spec_idx, &s.names, optional, s.opts.as_ref()))
}
Spec::OneOf(o) => slots.push(make_slot(spec_idx, &o.names, optional, o.opts.as_ref())),
_ => {}
}
}
slots
}
pub(crate) fn slot_for(slots: &[Slot<'_>], name: &str) -> Option<usize> {
slots.iter().position(|s| s.admits(name))
}
pub(crate) struct SlotBinding<'a> {
pub(crate) node: &'a SectionNode,
pub(crate) slot_idx: Option<usize>,
pub(crate) fills: bool,
}
pub(crate) fn bind_slots<'a>(nodes: &'a [SectionNode], slots: &[Slot<'_>]) -> Vec<SlotBinding<'a>> {
let mut filled = HashSet::new();
nodes
.iter()
.map(|node| {
let slot_idx = slot_for(slots, &node.name);
let fills = slot_idx.is_some_and(|i| slots[i].repeatable || !filled.contains(&i));
if let Some(i) = slot_idx {
filled.insert(i);
}
SlotBinding {
node,
slot_idx,
fills,
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::contract::{one_of, optional, section, section_with};
use crate::finding::SourcePos;
fn node(name: &str) -> SectionNode {
SectionNode::new(name, 2, SourcePos::line(1))
}
#[test]
fn a_section_fills_the_first_slot_that_names_it_once_unless_repeatable() {
let specs = vec![
optional(section("A")),
one_of(["B", "Bee"]),
section_with(
"C",
SectionOpts {
repeatable: true,
..Default::default()
},
),
section("A"),
];
let slots = slots_of(&specs);
assert_eq!(slots.len(), 4);
assert!(slots[0].optional);
let nodes: Vec<SectionNode> = ["A", "B", "Bee", "C", "C", "X", "A"]
.into_iter()
.map(node)
.collect();
let got: Vec<(Option<usize>, bool)> = bind_slots(&nodes, &slots)
.iter()
.map(|b| (b.slot_idx, b.fills))
.collect();
assert_eq!(
got,
vec![
(Some(0), true),
(Some(1), true),
(Some(1), false), (Some(2), true),
(Some(2), true), (None, false),
(Some(0), false), ]
);
}
}