use espf::context::ParsedNode;
use espf::diag::EspfError;
use espf::value::{Object, Spanned, Value};
use espf::writer::{Document, Element, WriteError, write_document};
use espforge_model::{Console, Meta, Runtime};
use espforge_options::{Answers, OptValue};
use std::collections::HashMap;
use std::ops::Range;
fn s(v: &str) -> Value {
Value::Str(Spanned {
value: v.to_string(),
span: 0..0,
})
}
fn element(tag: &str, attrs: Vec<(&str, Value)>) -> Element {
let mut o = Object::new(0..0);
for (k, v) in attrs {
o.fields.insert(k.to_string(), v);
}
Element {
tag: tag.to_string(),
attrs: o,
}
}
fn opt_value(v: &OptValue) -> Value {
match v {
OptValue::Bool(b) => s(if *b { "true" } else { "false" }),
OptValue::Str(x) => s(x),
OptValue::List(items) => Value::Array(
items
.iter()
.map(|i| Spanned {
value: i.clone(),
span: 0..0,
})
.collect(),
),
}
}
fn espforge_elements(meta: &Meta, answers: &Answers) -> Vec<Element> {
let mut project = vec![("name", s(&meta.name))];
if let Some(d) = &meta.description {
project.push(("description", s(d)));
}
let mut options = vec![(
"defaults",
s(if answers.defaults { "true" } else { "false" }),
)];
for (name, a) in &answers.values {
options.push((name.as_str(), opt_value(&a.value)));
}
vec![
element("project", project),
element("chip", vec![("type", s(&meta.chip))]),
element(
"runtime",
vec![(
"type",
s(match meta.runtime {
Runtime::Blocking => "blocking",
Runtime::Embassy => "embassy",
}),
)],
),
element(
"console",
vec![(
"type",
s(match meta.console {
Console::Uart => "uart",
Console::JtagSerial => "jtag-serial",
}),
)],
),
element("options", options),
]
}
pub fn effective_document(
sections: &HashMap<String, Vec<ParsedNode>>,
meta: &Meta,
answers: &Answers,
) -> Result<Document, Vec<EspfError>> {
let mut doc = Document::from_sections(sections)?;
doc.sections
.insert("espforge".to_string(), espforge_elements(meta, answers));
Ok(doc)
}
pub fn blank_document(meta: &Meta, answers: &Answers) -> Document {
let mut doc = Document::default();
doc.sections
.insert("espforge".to_string(), espforge_elements(meta, answers));
for name in ["peripherals", "components", "devices", "services"] {
doc.sections.insert(name.to_string(), Vec::new());
}
doc
}
pub fn render(doc: &Document) -> Result<String, WriteError> {
write_document(doc)
}
pub fn chip_value(src: &str) -> Option<(Range<usize>, String)> {
let parsed = espf::parser::parse_espf(src);
let node = parsed
.sections
.get("espforge")?
.iter()
.find(|n| n.tag_name == "chip")?;
let attr = node.get_attr_spanned("type")?;
Some((
attr.val_offset..attr.val_offset + attr.val.len(),
attr.val.to_string(),
))
}
pub fn set_chip(src: &str, chip: &str) -> Option<String> {
let (span, _) = chip_value(src)?;
Some(format!(
"{}{}{}",
&src[..span.start],
chip,
&src[span.end..]
))
}
#[cfg(test)]
mod tests {
use super::*;
use espf::parser::parse_espf;
const SRC: &str = "# espforge\n <project name=\"hello\" />\n <chip type=\"esp32c3\" />\n <runtime type=\"blocking\" />\n# peripherals\n# components\n# devices\n# services\n <ntp id=\"n\" servers+=\"a\" servers+=\"b\" />\n";
fn effective(meta: &Meta, answers: &Answers) -> (String, Document) {
let p = parse_espf(SRC);
let doc = effective_document(&p.sections, meta, answers).unwrap();
(render(&doc).unwrap(), doc)
}
fn meta() -> Meta {
Meta {
name: "hello".into(),
description: None,
chip: "esp32s3".into(),
runtime: Runtime::Embassy,
console: Console::Uart,
}
}
#[test]
fn overrides_are_reflected_and_other_sections_kept() {
let mut answers = Answers::empty(0..0);
answers
.apply_flag(espforge_options::embedded_schema(), "alloc", "false")
.unwrap();
let (text, _) = effective(&meta(), &answers);
assert!(text.contains("<chip type=\"esp32s3\" />"), "{text}");
assert!(text.contains("<runtime type=\"embassy\" />"));
assert!(
text.contains("<options alloc=\"false\" defaults=\"true\" />"),
"{text}"
);
assert!(text.contains("<ntp id=\"n\" servers+=\"a\" servers+=\"b\" />"));
}
#[test]
fn output_round_trips() {
let (text, doc) = effective(&meta(), &Answers::empty(0..0));
let again = parse_espf(&text);
assert!(
again.junk.is_empty() && again.structure_errors.is_empty(),
"{text}"
);
assert_eq!(Document::from_sections(&again.sections).unwrap(), doc);
assert!(crate::frontend::load(&text).is_ok(), "{text}");
}
#[test]
fn blank_document_is_a_loadable_empty_project() {
let mut answers = Answers::empty(0..0);
answers
.apply_flag(espforge_options::embedded_schema(), "wokwi", "false")
.unwrap();
let text = render(&blank_document(&meta(), &answers)).unwrap();
let fe = crate::frontend::load(&text).unwrap_or_else(|_| panic!("{text}"));
assert_eq!(fe.meta.chip, "esp32s3");
assert!(fe.answers.defaults);
assert!(fe.answers.values.contains_key("wokwi"));
for name in ["peripherals", "components", "devices", "services"] {
assert!(fe.parsed.sections[name].is_empty());
}
}
const HELLO: &str = "# espforge\n <project name=\"hello\" />\n <chip type=\"esp32c3\" />\n <runtime type=\"blocking\" />\n# peripherals\n# components\n# devices\n# services\n";
fn hello() -> &'static str {
HELLO
}
#[test]
fn chip_value_finds_the_chip_type() {
let (span, chip) = chip_value(hello()).unwrap();
assert_eq!(chip, "esp32c3");
assert_eq!(&hello()[span], "esp32c3");
}
#[test]
fn set_chip_rewrites_only_the_chip() {
let src = hello();
let out = set_chip(src, "esp32s3").unwrap();
assert!(out.contains("<chip type=\"esp32s3\" />"));
assert_eq!(
out.replace("esp32s3", "esp32c3"),
src,
"nothing else may change"
);
assert!(crate::frontend::load(&out).is_ok());
}
#[test]
fn set_chip_is_none_without_a_chip() {
assert!(set_chip("# espforge\n", "esp32c3").is_none());
}
#[test]
fn every_embedded_example_has_a_chip_to_rewrite() {
for category in espforge_examples::categories() {
for ex in &category.examples {
assert!(
chip_value(&ex.content).is_some(),
"{}/{}",
category.name,
ex.name
);
}
}
}
}