use espf::context::Refs;
use espf::diag::{EspfError, node_span};
use espforge_bindings::drivers::generated_registry::route_driver;
use espforge_diagnostics::{Diagnostic, Diagnostics, codes};
use espforge_model::Meta;
use espforge_options::{Answers, ResolvedOptions};
use crate::answers::Overrides;
use crate::frontend::{self, Frontend};
#[derive(Debug)]
pub enum Invalid {
Diagnostics(Diagnostics),
Unrouted { diagnostic: Diagnostic },
Message(String),
}
impl From<Vec<EspfError>> for Invalid {
fn from(errors: Vec<EspfError>) -> Self {
Invalid::Diagnostics(errors.iter().map(EspfError::to_diagnostic).collect())
}
}
impl From<Vec<Diagnostic>> for Invalid {
fn from(diagnostics: Vec<Diagnostic>) -> Self {
Invalid::Diagnostics(diagnostics)
}
}
impl From<String> for Invalid {
fn from(m: String) -> Self {
Invalid::Message(m)
}
}
impl Invalid {
pub fn diagnostics(&self) -> Diagnostics {
match self {
Invalid::Diagnostics(diagnostics) => diagnostics.clone(),
Invalid::Unrouted { diagnostic } => vec![diagnostic.clone()],
Invalid::Message(m) => vec![Diagnostic::error(
codes::for_cli_message(m),
0..0,
m.clone(),
"",
)],
}
}
pub fn message(&self) -> Option<&str> {
match self {
Invalid::Message(m) => Some(m),
Invalid::Unrouted { diagnostic } => Some(&diagnostic.message),
Invalid::Diagnostics(_) => None,
}
}
}
pub struct Checked<'a> {
pub fe: Frontend<'a>,
pub meta: Meta,
pub answers: Answers,
pub resolved: ResolvedOptions,
pub notes: Vec<String>,
}
pub fn validate<'a>(src: &'a str, overrides: &Overrides) -> Result<Checked<'a>, Invalid> {
let fe = frontend::load(src)?;
let mut meta = fe.meta.clone();
let mut answers = fe.answers.clone();
let mut notes = Vec::new();
overrides.apply_core(&mut meta, &mut notes)?;
overrides.apply_options(
espforge_options::embedded_schema(),
&mut answers,
&mut notes,
)?;
let resolved = espforge_options::resolve(&meta, &answers, &fe.derived)?;
Refs::resolve(&fe.parsed.sections)?;
Ok(Checked {
fe,
meta,
answers,
resolved,
notes,
})
}
pub fn board(c: &Checked) -> Result<espforge_model::Board, Invalid> {
route_devices(&c.fe.parsed.sections)?;
espforge_hardware::board::build(&c.fe.parsed.sections, &c.meta).map_err(Invalid::from)
}
fn route_devices(
sections: &std::collections::HashMap<String, Vec<espf::context::ParsedNode>>,
) -> Result<(), Invalid> {
for device in sections.get("devices").map(Vec::as_slice).unwrap_or(&[]) {
if let Err(message) = route_driver(device.tag_name, device) {
return Err(Invalid::Unrouted {
diagnostic: Diagnostic::error(
codes::DRIVER_UNKNOWN,
node_span(device),
message,
format!("no driver is compiled for <{}>", device.tag_name),
),
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
const WITH_BUS: &str = "# espforge\n <project name=\"t\" />\n <chip type=\"esp32c3\" />\n <runtime type=\"blocking\" />\n# peripherals\n <i2c id=\"i2c0\" sda=\"4\" scl=\"5\" />\n# components\n <i2c_component id=\"c\" i2c=\"$i2c0\" address=\"0x48\" />\n# devices\n# services\n";
fn hello() -> &'static str {
&espforge_examples::find("01.Basics/helloworld")
.unwrap()
.content
}
#[test]
fn valid_spec_passes_and_lowers() {
let c = validate(hello(), &Overrides::default())
.ok()
.expect("hello validates");
assert!(c.notes.is_empty());
let board = board(&c).unwrap();
assert!(board.components.is_empty());
assert!(board.services.is_empty());
}
#[test]
fn flags_override_and_leave_a_note() {
let overrides = Overrides {
chip: Some("esp32s3".into()),
..Default::default()
};
let c = validate(hello(), &overrides).ok().expect("validates");
assert_eq!(c.meta.chip, "esp32s3");
assert_eq!(c.fe.meta.chip, "esp32c3", "the file's own value is kept");
assert_eq!(c.notes.len(), 1);
}
#[test]
fn bad_flags_are_messages_not_spans() {
let overrides = Overrides {
chip: Some("esp99".into()),
..Default::default()
};
assert!(matches!(
validate(hello(), &overrides).err(),
Some(Invalid::Message(_))
));
}
#[test]
fn graph_errors_are_reported_with_spans() {
let src = WITH_BUS.replace("$i2c0", "$nope");
match validate(&src, &Overrides::default()).err() {
Some(Invalid::Diagnostics(diagnostics)) => {
assert_eq!(diagnostics[0].message, "unknown peripheral");
assert_eq!(diagnostics[0].code, codes::GRAPH_UNKNOWN_PERIPHERAL);
assert_eq!(diagnostics[0].primary, 182..187);
}
_ => panic!("expected a located error"),
}
}
#[test]
fn unsupported_hardware_is_a_failure_not_a_warning() {
let src = "# espforge\n <project name=\"t\" />\n <chip type=\"esp32c3\" />\n <runtime type=\"blocking\" />\n# peripherals\n <pwm id=\"pwm0\" />\n# components\n# devices\n# services\n";
let c = validate(src, &Overrides::default())
.ok()
.expect("graph is fine");
let err = board(&c).unwrap_err();
match err {
Invalid::Diagnostics(diagnostics) => {
assert!(
diagnostics
.iter()
.any(|e| e.labels.iter().any(|label| label.text.contains("<pwm>"))),
"{diagnostics:?}"
);
assert_eq!(diagnostics[0].code, codes::PERIPHERAL_UNSUPPORTED);
}
Invalid::Message(m) => panic!("expected spans, got message: {m}"),
Invalid::Unrouted { diagnostic } => {
panic!("expected lowering spans, got driver routing: {diagnostic:?}")
}
}
}
#[test]
fn a_device_without_a_driver_is_reported_before_lowering() {
let src = "# espforge\n <project name=\"t\" />\n <chip type=\"esp32c3\" />\n <runtime type=\"blocking\" />\n# peripherals\n <gpio id=\"g\" pin=\"18\" />\n# components\n <led id=\"red_led\" gpio=\"$g\" />\n# devices\n <made_up_part id=\"d\" gpio_component=\"$red_led\" />\n# services\n";
let c = validate(src, &Overrides::default())
.ok()
.expect("graph is fine");
match board(&c).unwrap_err() {
Invalid::Unrouted { diagnostic } => {
assert_eq!(diagnostic.code, codes::DRIVER_UNKNOWN);
assert!(
diagnostic.message.contains("made_up_part"),
"{diagnostic:?}"
);
assert_eq!(
diagnostic.labels[0].text,
"no driver is compiled for <made_up_part>"
);
}
other => panic!("expected an unrouted device, got {other:?}"),
}
}
}