use espf::context::{ParsedNode, TagAttribute};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Span {
pub start: usize,
pub end: usize,
}
impl Span {
pub fn at_node(node: &ParsedNode<'_>) -> Self {
Self {
start: node.offset,
end: node.offset,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Spanned<T> {
pub value: T,
pub span: Span,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Ref<'a> {
pub raw: &'a str,
pub name: &'a str,
pub span: Span,
}
impl<'a> Ref<'a> {
fn at(raw: &'a str, span: Span) -> Self {
Self {
raw,
name: raw.trim_start_matches('$'),
span,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PinOrRef<'a> {
Pin(Spanned<u8>),
Ref(Ref<'a>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Issue<'a> {
Missing { attr: &'static str },
Invalid {
attr: &'static str,
value: &'a str,
},
}
fn find<'a>(node: &'a ParsedNode<'a>, attr: &str) -> Option<&'a TagAttribute<'a>> {
node.attributes.iter().find(|a| a.key == attr)
}
fn span_of(attr: &TagAttribute<'_>) -> Span {
Span {
start: attr.val_offset,
end: attr.val_offset + attr.val.len(),
}
}
fn missing<'a>(attr: &'static str) -> Issue<'a> {
Issue::Missing { attr }
}
fn invalid<'a>(attr: &'static str, value: &'a str) -> Issue<'a> {
Issue::Invalid { attr, value }
}
fn shown<'a>(attr: &'a TagAttribute<'a>) -> Spanned<&'a str> {
Spanned {
value: attr.val,
span: span_of(attr),
}
}
pub fn ident<'a>(
node: &'a ParsedNode<'a>,
name: &'static str,
) -> Result<Spanned<&'a str>, Issue<'a>> {
let attr = find(node, name).ok_or(missing(name))?;
if attr.val.is_empty() {
return Err(missing(name));
}
Ok(shown(attr))
}
pub fn text<'a>(
node: &'a ParsedNode<'a>,
name: &'static str,
) -> Result<Spanned<&'a str>, Issue<'a>> {
let attr = find(node, name).ok_or(missing(name))?;
Ok(shown(attr))
}
pub fn text_or<'a>(
node: &'a ParsedNode<'a>,
name: &'static str,
default: &'a str,
) -> Result<Spanned<&'a str>, Issue<'a>> {
match find(node, name) {
Some(attr) => Ok(shown(attr)),
None => Ok(Spanned {
value: default,
span: Span::at_node(node),
}),
}
}
pub fn opt_text<'a>(
node: &'a ParsedNode<'a>,
name: &'static str,
) -> Result<Option<Spanned<&'a str>>, Issue<'a>> {
Ok(find(node, name).map(shown))
}
pub fn frequency<'a>(
node: &'a ParsedNode<'a>,
name: &'static str,
) -> Result<Spanned<&'a str>, Issue<'a>> {
text(node, name)
}
pub fn frequency_or<'a>(
node: &'a ParsedNode<'a>,
name: &'static str,
default: &'a str,
) -> Result<Spanned<&'a str>, Issue<'a>> {
text_or(node, name, default)
}
pub fn opt_frequency<'a>(
node: &'a ParsedNode<'a>,
name: &'static str,
) -> Result<Option<Spanned<&'a str>>, Issue<'a>> {
opt_text(node, name)
}
pub fn flag<'a>(node: &'a ParsedNode<'a>, name: &'static str) -> Result<Spanned<bool>, Issue<'a>> {
let attr = find(node, name).ok_or(missing(name))?;
flag_value(attr, name)
}
pub fn flag_or<'a>(
node: &'a ParsedNode<'a>,
name: &'static str,
default: bool,
) -> Result<Spanned<bool>, Issue<'a>> {
match find(node, name) {
Some(attr) => flag_value(attr, name),
None => Ok(Spanned {
value: default,
span: Span::at_node(node),
}),
}
}
fn flag_value<'a>(
attr: &'a TagAttribute<'a>,
name: &'static str,
) -> Result<Spanned<bool>, Issue<'a>> {
let value = match attr.val {
"true" => true,
"false" => false,
other => return Err(invalid(name, other)),
};
Ok(Spanned {
value,
span: span_of(attr),
})
}
pub fn number<'a>(node: &'a ParsedNode<'a>, name: &'static str) -> Result<Spanned<u32>, Issue<'a>> {
let attr = find(node, name).ok_or(missing(name))?;
number_value(attr, name)
}
pub fn number_in<'a>(
node: &'a ParsedNode<'a>,
name: &'static str,
low: u32,
high: u32,
) -> Result<Spanned<u32>, Issue<'a>> {
let attr = find(node, name).ok_or(missing(name))?;
let value = attr
.val
.parse::<u32>()
.map_err(|_| invalid(name, attr.val))?;
if !(low..=high).contains(&value) {
return Err(invalid(name, attr.val));
}
Ok(Spanned {
value,
span: span_of(attr),
})
}
pub fn number_or<'a>(
node: &'a ParsedNode<'a>,
name: &'static str,
default: u32,
) -> Result<Spanned<u32>, Issue<'a>> {
match find(node, name) {
Some(attr) => number_value(attr, name),
None => Ok(Spanned {
value: default,
span: Span::at_node(node),
}),
}
}
pub fn number_in_or<'a>(
node: &'a ParsedNode<'a>,
name: &'static str,
low: u32,
high: u32,
default: u32,
) -> Result<Spanned<u32>, Issue<'a>> {
let Some(attr) = find(node, name) else {
return Ok(Spanned {
value: default,
span: Span::at_node(node),
});
};
let value = attr
.val
.parse::<u32>()
.map_err(|_| invalid(name, attr.val))?;
if !(low..=high).contains(&value) {
return Err(invalid(name, attr.val));
}
Ok(Spanned {
value,
span: span_of(attr),
})
}
fn number_value<'a>(
attr: &'a TagAttribute<'a>,
name: &'static str,
) -> Result<Spanned<u32>, Issue<'a>> {
let value = attr
.val
.parse::<u32>()
.map_err(|_| invalid(name, attr.val))?;
Ok(Spanned {
value,
span: span_of(attr),
})
}
pub fn pin<'a>(node: &'a ParsedNode<'a>, name: &'static str) -> Result<Spanned<u8>, Issue<'a>> {
let attr = find(node, name).ok_or(missing(name))?;
Ok(Spanned {
value: attr
.val
.parse::<u8>()
.map_err(|_| invalid(name, attr.val))?,
span: span_of(attr),
})
}
pub fn opt_pin<'a>(
node: &'a ParsedNode<'a>,
name: &'static str,
) -> Result<Option<Spanned<u8>>, Issue<'a>> {
match find(node, name) {
Some(attr) => Ok(Some(Spanned {
value: attr
.val
.parse::<u8>()
.map_err(|_| invalid(name, attr.val))?,
span: span_of(attr),
})),
None => Ok(None),
}
}
pub fn hex<'a>(node: &'a ParsedNode<'a>, name: &'static str) -> Result<Spanned<u8>, Issue<'a>> {
let attr = find(node, name).ok_or(missing(name))?;
hex_value(attr, name)
}
pub fn opt_hex<'a>(
node: &'a ParsedNode<'a>,
name: &'static str,
) -> Result<Option<Spanned<u8>>, Issue<'a>> {
match find(node, name) {
Some(attr) => Ok(Some(hex_value(attr, name)?)),
None => Ok(None),
}
}
fn hex_value<'a>(attr: &'a TagAttribute<'a>, name: &'static str) -> Result<Spanned<u8>, Issue<'a>> {
let parsed = match attr
.val
.strip_prefix("0x")
.or_else(|| attr.val.strip_prefix("0X"))
{
Some(hex) => u8::from_str_radix(hex, 16),
None => attr.val.parse::<u8>(),
};
Ok(Spanned {
value: parsed.map_err(|_| invalid(name, attr.val))?,
span: span_of(attr),
})
}
pub fn pin_or_ref<'a>(
node: &'a ParsedNode<'a>,
name: &'static str,
) -> Result<PinOrRef<'a>, Issue<'a>> {
let attr = find(node, name).ok_or(missing(name))?;
let span = span_of(attr);
Ok(match attr.val.parse::<u8>() {
Ok(pin) => PinOrRef::Pin(Spanned { value: pin, span }),
Err(_) => PinOrRef::Ref(Ref::at(attr.val, span)),
})
}
pub fn reference<'a>(node: &'a ParsedNode<'a>, name: &'static str) -> Result<Ref<'a>, Issue<'a>> {
let attr = find(node, name).ok_or(missing(name))?;
Ok(Ref::at(attr.val, span_of(attr)))
}
pub fn opt_reference<'a>(
node: &'a ParsedNode<'a>,
name: &'static str,
) -> Result<Option<Ref<'a>>, Issue<'a>> {
Ok(find(node, name).map(|attr| Ref::at(attr.val, span_of(attr))))
}
pub fn parsed<'a, T: Copy>(
node: &'a ParsedNode<'a>,
name: &'static str,
parse: fn(&str) -> Option<T>,
) -> Result<Spanned<T>, Issue<'a>> {
let attr = find(node, name).ok_or(missing(name))?;
parsed_value(attr, name, parse)
}
pub fn parsed_or<'a, T: Copy>(
node: &'a ParsedNode<'a>,
name: &'static str,
parse: fn(&str) -> Option<T>,
default: T,
) -> Result<Spanned<T>, Issue<'a>> {
match find(node, name) {
Some(attr) => parsed_value(attr, name, parse),
None => Ok(Spanned {
value: default,
span: Span::at_node(node),
}),
}
}
pub fn opt_parsed<'a, T: Copy>(
node: &'a ParsedNode<'a>,
name: &'static str,
parse: fn(&str) -> Option<T>,
) -> Result<Option<Spanned<T>>, Issue<'a>> {
match find(node, name) {
Some(attr) => Ok(Some(parsed_value(attr, name, parse)?)),
None => Ok(None),
}
}
fn parsed_value<'a, T: Copy>(
attr: &'a TagAttribute<'a>,
name: &'static str,
parse: fn(&str) -> Option<T>,
) -> Result<Spanned<T>, Issue<'a>> {
Ok(Spanned {
value: parse(attr.val).ok_or_else(|| invalid(name, attr.val))?,
span: span_of(attr),
})
}
pub fn list<'a>(node: &'a ParsedNode<'a>, name: &str) -> Vec<Spanned<&'a str>> {
node.attributes
.iter()
.filter(|a| a.key == name)
.map(shown)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn node<'a>(section: &str, src: &'a str) -> ParsedNode<'a> {
espf::parser::parse_espf(src).sections[section][0].clone()
}
#[test]
fn a_reference_keeps_both_spellings_and_its_span() {
let n = node(
"components",
"# components\n <led id=\"l\" gpio=\"$gpio18\" />\n",
);
let r = reference(&n, "gpio").unwrap();
assert_eq!(r.raw, "$gpio18");
assert_eq!(r.name, "gpio18");
let written_at = &"# components\n <led id=\"l\" gpio="[..];
assert_eq!(r.span.start, written_at.len() + 1);
assert_eq!(r.span.end - r.span.start, "$gpio18".len());
}
#[test]
fn missing_identifiers_are_missing_even_when_empty() {
let n = node(
"components",
"# components\n <led id=\"\" gpio=\"$gpio18\" />\n",
);
assert_eq!(ident(&n, "id"), Err(Issue::Missing { attr: "id" }));
}
#[test]
fn a_bad_choice_reports_the_value_as_written() {
let n = node(
"components",
"# components\n <led id=\"l\" gpio=\"$g\" active_low=\"yes\" />\n",
);
assert_eq!(
flag(&n, "active_low"),
Err(Issue::Invalid {
attr: "active_low",
value: "yes"
})
);
}
#[test]
fn hex_accepts_both_spellings_and_a_range_is_enforced() {
let hi = node(
"components",
"# components\n <i2c_component id=\"c\" i2c=\"$i2c0\" address=\"0x48\" />\n",
);
assert_eq!(hex(&hi, "address").unwrap().value, 0x48);
let range = node(
"peripherals",
"# peripherals\n <spi id=\"s\" sclk=\"1\" mosi=\"2\" mode=\"3\" />\n",
);
assert_eq!(number_in_or(&range, "mode", 0, 3, 0).unwrap().value, 3);
assert_eq!(
number_in_or(&range, "mode", 0, 2, 0),
Err(Issue::Invalid {
attr: "mode",
value: "3"
})
);
}
#[test]
fn defaults_are_attributed_to_the_element() {
let n = node("espforge", "# espforge\n <console />\n");
let default = text_or(&n, "type", "uart").unwrap();
assert_eq!(default.value, "uart");
assert_eq!(default.span, Span::at_node(&n));
}
#[test]
fn a_list_collects_every_occurrence() {
let n = node(
"services",
"# services\n <ntp id=\"ntp0\" servers+=\"pool.ntp.org\" servers+=\"time.google.com\" />\n",
);
let servers: Vec<&str> = list(&n, "servers").into_iter().map(|s| s.value).collect();
assert_eq!(servers, ["pool.ntp.org", "time.google.com"]);
}
}