use kdl::{KdlDocument, KdlNode};
use std::fs;
use std::path::Path;
const SCHEMA: &str = "../espforge-espgenerate-template/src/espf.schema.kdl";
fn main() {
println!("cargo:rerun-if-changed={SCHEMA}");
let src = fs::read_to_string(SCHEMA).unwrap_or_else(|e| panic!("cannot read {SCHEMA}: {e}"));
let doc = KdlDocument::parse_v2(&src)
.unwrap_or_else(|e| panic!("{SCHEMA} is not valid KDL v2:\n{e}"));
let mut required: Vec<String> = Vec::new();
let mut options: Vec<String> = Vec::new();
let mut sections: Vec<SectionDraft> = Vec::new();
for node in doc.nodes() {
match node.name().value() {
"required" => required.extend(positional_strings(node)),
"options" => {
for child in node.children().map(|c| c.nodes()).unwrap_or(&[]) {
if child.name().value() == "option" {
options.push(option_literal(child));
}
}
}
"section" => sections.push(section_draft(node)),
"element" => {
let section = prop_string(node, "section").unwrap_or_default();
let literal = element_literal(node);
match sections.iter_mut().find(|s| s.name == section) {
Some(section) => section.leafs.push(literal),
None => {}
}
}
_ => {}
}
}
write_if_changed(
Path::new("src/generated_options.rs"),
&options_source(&required, &options),
);
write_if_changed(
Path::new("src/generated_catalog.rs"),
&catalog_source(§ions),
);
}
fn options_source(required: &[String], options: &[String]) -> String {
let required = if required.is_empty() {
"Vec::new()".to_string()
} else {
string_vec(required)
};
format!(
concat!(
"// AUTO-GENERATED BY BUILD.RS FROM espf.schema.kdl - DO NOT EDIT MANUALLY\n",
"\n",
"//! The option half of `espf.schema.kdl` as Rust.\n",
"//!\n",
"//! `schema::embedded_schema` returns this table; a test asserts it equals\n",
"//! what `schema::load` parses from the embedded text, so the loader stays\n",
"//! the authority on what the schema means.\n",
"\n",
"use crate::schema::{{Compat, Kind, OptValue, OptionDef, Requires, Schema}};\n",
"\n",
"#[rustfmt::skip]\n",
"pub(crate) fn schema() -> Schema {{\n",
" Schema {{\n",
" required: {required},\n",
" options: vec![\n",
"{options}",
" ],\n",
" }}\n",
"}}\n",
),
required = required,
options = options.join(""),
)
}
fn option_literal(node: &kdl::KdlNode) -> String {
let name = positional_strings(node).remove(0);
let mut display_name = None;
let mut selection_group = None;
let mut kind_name = "bool".to_string();
let mut nightly = false;
let mut default = None;
let mut requires: Vec<String> = Vec::new();
let mut tools: Vec<String> = Vec::new();
let mut chips: Option<Vec<String>> = None;
let mut choices: Vec<String> = Vec::new();
let mut help = None;
for entry in node.entries().iter().filter(|e| e.name().is_some()) {
match entry.name().map(|k| k.value()).unwrap_or("") {
"display_name" => display_name = entry.value().as_string().map(str::to_string),
"selection_group" => selection_group = entry.value().as_string().map(str::to_string),
"kind" => {
if let Some(kind) = entry.value().as_string() {
kind_name = kind.to_string();
}
}
"requires_nightly" => nightly = entry.value().as_bool().unwrap_or(false),
"default" => {
default = match entry.value().as_bool() {
Some(value) => Some(format!("Some(OptValue::Bool({value}))")),
None => entry
.value()
.as_string()
.map(|value| format!("Some(OptValue::Str({value:?}.to_string()))")),
}
}
_ => {}
}
}
for child in node.children().map(|c| c.nodes()).unwrap_or(&[]) {
match child.name().value() {
"help" => help = positional_strings(child).into_iter().next(),
"requires" => requires.extend(positional_strings(child)),
"requires_tools" => tools.extend(positional_strings(child)),
"choices" => choices.extend(positional_strings(child)),
"compatible" => {
chips = Some(
child
.children()
.map(|c| c.nodes())
.unwrap_or(&[])
.iter()
.filter(|c| c.name().value() == "chip")
.flat_map(positional_strings)
.collect(),
)
}
_ => {}
}
}
let kind = match kind_name.as_str() {
"string" => "Kind::String".to_string(),
"list" => "Kind::List".to_string(),
"choice" => format!("Kind::Choice({})", string_vec(&choices)),
_ => "Kind::Bool".to_string(),
};
if kind_name == "bool" && default.is_none() {
default = Some("Some(OptValue::Bool(false))".to_string());
}
let span = node_span(node);
format!(
" OptionDef {{\n\
\x20 name: {name:?}.to_string(),\n\
\x20 display_name: {display_name},\n\
\x20 help: {help},\n\
\x20 kind: {kind},\n\
\x20 default: {default},\n\
\x20 requires: Requires {{\n\
\x20 options: {requires},\n\
\x20 tools: {tools},\n\
\x20 nightly: {nightly},\n\
\x20 }},\n\
\x20 compat: Compat {{ chips: {chips} }},\n\
\x20 selection_group: {selection_group},\n\
\x20 span: {span},\n\
\x20 }},\n",
display_name = opt_string(display_name),
help = opt_string(help),
default = default.unwrap_or_else(|| "None".to_string()),
requires = string_vec(&requires),
tools = string_vec(&tools),
chips = match chips {
Some(chips) => format!("Some({})", string_vec(&chips)),
None => "None".to_string(),
},
selection_group = opt_string(selection_group),
)
}
struct SectionDraft {
name: String,
summary: String,
description: String,
leafs: Vec<String>,
}
fn section_draft(node: &KdlNode) -> SectionDraft {
let mut summary = String::new();
let mut description = String::new();
for child in node.children().map(|c| c.nodes()).unwrap_or(&[]) {
let value = positional_strings(child).into_iter().next().unwrap_or_default();
match child.name().value() {
"summary" if summary.is_empty() => summary = value,
"description" if description.is_empty() => description = value,
_ => {}
}
}
SectionDraft {
name: positional_strings(node).into_iter().next().unwrap_or_default(),
summary,
description,
leafs: Vec::new(),
}
}
fn catalog_source(sections: &[SectionDraft]) -> String {
let mut out = String::from(concat!(
"// AUTO-GENERATED BY BUILD.RS FROM espf.schema.kdl - DO NOT EDIT MANUALLY\n",
"\n",
"//! The element half of `espf.schema.kdl` as Rust: the sections, elements,\n",
"//! attributes and examples behind `espforge espf`.\n",
"//!\n",
"//! `catalog::embedded` returns this table; a test asserts it equals what\n",
"//! `catalog::load` parses from the embedded text, so the loader stays the\n",
"//! authority on what the schema means.\n",
"\n",
"use crate::catalog::{AttrDef, AttrKind, Catalog, ElementDef, Example, SectionDef, Status};\n",
"use crate::schema::Compat;\n",
"\n",
"#[rustfmt::skip]\n",
"pub(crate) fn catalog() -> Catalog {\n",
" Catalog {\n",
" sections: vec![\n",
));
for section in sections {
out.push_str(&format!(
" SectionDef {{\n\
\x20 name: {name:?}.to_string(),\n\
\x20 summary: {summary:?}.to_string(),\n\
\x20 description: {description:?}.to_string(),\n\
\x20 leafs: vec![\n\
{leafs}\
\x20 ],\n\
\x20 }},\n",
name = section.name,
summary = section.summary,
description = section.description,
leafs = section.leafs.join(""),
));
}
out.push_str(concat!(
" ],\n",
" overview: espforge_espgenerate_template::OVERVIEW_TXT,\n",
" }\n",
"}\n",
));
out
}
fn element_literal(node: &KdlNode) -> String {
let mut summary = String::new();
let mut description: Option<String> = None;
let mut attrs: Vec<String> = Vec::new();
let mut examples: Vec<String> = Vec::new();
let mut see_also: Vec<String> = Vec::new();
let mut chips: Option<Vec<String>> = None;
for child in node.children().map(|c| c.nodes()).unwrap_or(&[]) {
match child.name().value() {
"summary" if summary.is_empty() => {
summary = positional_strings(child).into_iter().next().unwrap_or_default()
}
"description" if description.is_none() => {
description = positional_strings(child).into_iter().next()
}
"attr" => attrs.push(attr_literal(child)),
"example" => {
let strings = positional_strings(child);
if strings.len() == 2 {
examples.push(format!(
" Example {{\n\
\x20 title: {:?}.to_string(),\n\
\x20 fragment: {:?}.to_string(),\n\
\x20 }},\n",
strings[0], strings[1]
));
}
}
"see_also" => see_also.extend(positional_strings(child)),
"compatible" => {
chips = Some(
child
.children()
.map(|c| c.nodes())
.unwrap_or(&[])
.iter()
.filter(|c| c.name().value() == "chip")
.flat_map(positional_strings)
.collect(),
)
}
_ => {}
}
}
let status = match prop_string(node, "status").as_deref() {
Some("planned") => "Status::Planned",
_ => "Status::Implemented",
};
format!(
" ElementDef {{\n\
\x20 tag: {tag:?}.to_string(),\n\
\x20 section: {section:?}.to_string(),\n\
\x20 summary: {summary:?}.to_string(),\n\
\x20 description: {description},\n\
\x20 status: {status},\n\
\x20 attrs: vec![\n\
{attrs}\
\x20 ],\n\
\x20 examples: vec![\n\
{examples}\
\x20 ],\n\
\x20 see_also: {see_also},\n\
\x20 compat: Compat {{ chips: {chips} }},\n\
\x20 span: {span},\n\
\x20 }},\n",
tag = positional_strings(node).into_iter().next().unwrap_or_default(),
section = prop_string(node, "section").unwrap_or_default(),
description = match description {
Some(text) => format!("Some({text:?}.to_string())"),
None => "None".to_string(),
},
attrs = attrs.join(""),
examples = examples.join(""),
see_also = string_vec(&see_also),
chips = match chips {
Some(chips) => format!("Some({})", string_vec(&chips)),
None => "None".to_string(),
},
span = node_span(node),
)
}
fn attr_literal(node: &KdlNode) -> String {
let name = positional_strings(node).into_iter().next().unwrap_or_default();
let mut required = false;
for entry in node
.entries()
.iter()
.filter(|e| e.name().is_some_and(|n| n.value() == "required"))
{
if let Some(value) = entry.value().as_bool() {
required = value;
}
}
let choices: Vec<String> = node
.children()
.map(|c| c.nodes())
.unwrap_or(&[])
.iter()
.filter(|c| c.name().value() == "choices")
.flat_map(positional_strings)
.collect();
let help = node
.children()
.map(|c| c.nodes())
.unwrap_or(&[])
.iter()
.find(|c| c.name().value() == "help")
.and_then(|c| positional_strings(c).into_iter().next());
let kind_name = prop_string(node, "kind").unwrap_or_default();
let to: Vec<String> = props(node, "to");
let of = prop_string(node, "of");
let kind = kind_literal(&name, &kind_name, &choices, &to, of.as_deref());
format!(
" AttrDef {{\n\
\x20 name: {name:?}.to_string(),\n\
\x20 kind: {kind},\n\
\x20 required: {required},\n\
\x20 default: {default},\n\
\x20 help: {help},\n\
\x20 span: {span},\n\
\x20 }},\n",
default = match prop_string(node, "default") {
Some(text) => format!("Some({text:?}.to_string())"),
None => "None".to_string(),
},
help = match help {
Some(text) => format!("Some({text:?}.to_string())"),
None => "None".to_string(),
},
span = node_span(node),
)
}
fn kind_literal(
_name: &str,
kind_name: &str,
choices: &[String],
to: &[String],
of: Option<&str>,
) -> String {
match kind_name {
"ident" => "AttrKind::Ident".to_string(),
"str" => "AttrKind::Str".to_string(),
"pin" => "AttrKind::Pin".to_string(),
"freq" => "AttrKind::Freq".to_string(),
"hex" => "AttrKind::Hex".to_string(),
"u32" => "AttrKind::U32".to_string(),
"bool" => "AttrKind::Bool".to_string(),
"choice" => format!("AttrKind::Choice({})", string_vec(choices)),
"ref" => format!("AttrKind::Ref {{ to: {} }}", string_vec(to)),
"pin_or_ref" => "AttrKind::PinOrRef".to_string(),
"list" => {
let inner = match of {
Some("ident") => "AttrKind::Ident",
Some("str") => "AttrKind::Str",
Some("pin") => "AttrKind::Pin",
Some("freq") => "AttrKind::Freq",
Some("hex") => "AttrKind::Hex",
Some("u32") => "AttrKind::U32",
Some("bool") => "AttrKind::Bool",
_ => "AttrKind::Str",
};
format!("AttrKind::List(Box::new({inner}))")
}
_ => "AttrKind::Str".to_string(),
}
}
fn node_span(node: &KdlNode) -> String {
let span = node.span();
format!("{}..{}", span.offset(), span.offset() + span.len())
}
fn prop_string(node: &KdlNode, key: &str) -> Option<String> {
props(node, key).into_iter().next()
}
fn props(node: &KdlNode, key: &str) -> Vec<String> {
node.entries()
.iter()
.filter(|entry| entry.name().is_some_and(|n| n.value() == key))
.filter_map(|entry| entry.value().as_string().map(str::to_string))
.collect()
}
fn opt_string(value: Option<String>) -> String {
match value {
Some(text) => format!("Some({text:?}.to_string())"),
None => "None".to_string(),
}
}
fn string_vec(values: &[String]) -> String {
if values.is_empty() {
return "Vec::new()".to_string();
}
format!(
"vec![{}]",
values
.iter()
.map(|value| format!("{value:?}.to_string()"))
.collect::<Vec<_>>()
.join(", ")
)
}
fn positional_strings(node: &kdl::KdlNode) -> Vec<String> {
node.entries()
.iter()
.filter(|entry| entry.name().is_none())
.filter_map(|entry| entry.value().as_string().map(str::to_string))
.collect()
}
fn write_if_changed(path: &Path, contents: &str) {
let current = fs::read_to_string(path).unwrap_or_default();
if current != contents {
fs::write(path, contents)
.unwrap_or_else(|e| panic!("cannot write {}: {e}", path.display()));
}
}