use std::fmt::Display;
use ahash::AHashMap;
use kdl::{KdlDocument, KdlNode};
use miette::{Diagnostic, LabeledSpan, NamedSource, Severity, SourceCode, SourceSpan};
use smol_str::SmolStr;
use thiserror::Error;
pub struct RawBlueprint {
name: SmolStr,
merge: MergeMode,
components: Vec<ComponentEntry>,
}
impl RawBlueprint {
pub fn load_from_kdl(
doc: &KdlDocument,
src: NamedSource,
) -> Result<Vec<RawBlueprint>, RawBlueprintDeserError> {
let mut out = Vec::new();
for kid in doc.nodes() {
let comps = match kid.children() {
Some(comps) => comps,
None => {
return Err(RawBlueprintDeserError {
span: *kid.span(),
kind: RawBlueprintParseErrorKind::NoChildren,
src,
})
}
};
let mut merge = None;
for entry in kid.entries() {
let key = if let Some(key) = entry.name() {
key
} else {
return Err(RawBlueprintDeserError {
span: *entry.span(),
kind: RawBlueprintParseErrorKind::TopLevelArgument,
src,
});
};
match key.value() {
"merge" => {
if merge.is_some() {
return Err(RawBlueprintDeserError {
span: *entry.span(),
kind: RawBlueprintParseErrorKind::ClobberInherit,
src,
});
}
let mode = if let Some(s) = entry.value().as_string() {
s
} else {
return Err(RawBlueprintDeserError {
span: *entry.span(),
kind: RawBlueprintParseErrorKind::BadMerge,
src,
});
};
let mode = match mode.to_lowercase().as_str() {
"merge" => MergeMode::Merge,
"clobber" => MergeMode::Clobber,
_ => {
return Err(RawBlueprintDeserError {
span: *entry.span(),
kind: RawBlueprintParseErrorKind::BadMerge,
src,
})
}
};
merge = Some(mode);
}
_ => {
return Err(RawBlueprintDeserError {
span: *entry.span(),
kind: RawBlueprintParseErrorKind::InvalidKey,
src,
})
}
}
if entry.ty().is_some() {
return Err(RawBlueprintDeserError {
span: *entry.span(),
kind: RawBlueprintParseErrorKind::TopLevelAnnotation,
src,
});
}
}
let merge = merge.unwrap_or_default();
let components = {
let mut components = Vec::new();
for node in comps.nodes() {
let entry = match node.ty() {
None => ComponentEntry::Component(node.clone()),
Some(ann) => {
if !node.entries().is_empty() || node.children().is_some() {
return Err(RawBlueprintDeserError {
span: *node.span(),
kind: RawBlueprintParseErrorKind::BadAnnotation,
src,
});
} else {
match ann.value() {
"splice" => ComponentEntry::Splice(node.name().value().into()),
_ => {
return Err(RawBlueprintDeserError {
span: *node.span(),
kind: RawBlueprintParseErrorKind::BadAnnotation,
src,
})
}
}
}
}
};
components.push(entry);
}
components
};
let bp = RawBlueprint {
name: kid.name().value().into(),
merge,
components,
};
out.push(bp)
}
Ok(out)
}
}
enum ComponentEntry {
Component(KdlNode),
Splice(SmolStr),
}
pub struct Blueprint {
pub name: SmolStr,
pub components: Vec<KdlNode>,
}
pub struct BlueprintLibrary {
prints: AHashMap<SmolStr, RawBlueprint>,
}
impl BlueprintLibrary {
pub fn new() -> Self {
Self {
prints: AHashMap::new(),
}
}
pub fn insert_raw(&mut self, blueprint: RawBlueprint) {
match self.prints.get_mut(&blueprint.name) {
None => {
self.prints.insert(blueprint.name.clone(), blueprint);
}
Some(old) => match blueprint.merge {
MergeMode::Clobber => {
*old = blueprint;
}
MergeMode::Merge => {
for comp in blueprint.components.into_iter() {
let clobberee = match &comp {
ComponentEntry::Splice(_) => None,
ComponentEntry::Component(new_node) => {
old.components.iter_mut().find(|old_comp| {
if let ComponentEntry::Component(it) = old_comp {
it.name() == new_node.name()
} else {
false
}
})
}
};
if let Some(clobberee) = clobberee {
*clobberee = comp;
} else {
old.components.push(comp);
}
}
}
},
}
}
pub fn load_str(&mut self, src: &str, filename: &str) -> Result<(), BlueprintParseError> {
let doc = src.parse()?;
let source = NamedSource::new(filename, src.to_owned());
let raws = RawBlueprint::load_from_kdl(&doc, source)?;
for raw in raws {
self.insert_raw(raw);
}
Ok(())
}
pub fn lookup(&self, name: &str) -> Result<Blueprint, BlueprintLookupError> {
fn recurse(
lib: &BlueprintLibrary,
name: &SmolStr,
path: Vec<SmolStr>,
) -> Result<Vec<KdlNode>, BlueprintLookupError> {
let raw = lib.prints.get(name).ok_or_else(|| match path.as_slice() {
[] => BlueprintLookupError::BlueprintNotFound(name.clone()),
[.., last] => BlueprintLookupError::InheriteeNotFound(last.clone(), name.clone()),
})?;
let mut out = Vec::new();
for comp in raw.components.iter() {
match comp {
ComponentEntry::Component(node) => {
out.push(node.clone());
}
ComponentEntry::Splice(parent_name) => {
if let Some(ono) = path
.iter()
.enumerate()
.find_map(|(idx, kid)| (kid == parent_name).then_some(idx))
{
let mut problem = path[ono..].to_vec();
problem.push(name.clone());
problem.push(path[ono].clone());
return Err(BlueprintLookupError::InheritanceLoop(problem));
}
let mut path2 = path.clone();
path2.push(name.clone());
let to_splice = recurse(lib, parent_name, path2)?;
out.extend(to_splice);
}
}
}
Ok(out)
}
let smol_name = name.into();
let components = recurse(self, &smol_name, Vec::new())?;
Ok(Blueprint {
name: name.into(),
components,
})
}
}
#[derive(Debug, Clone, Copy, Default)]
pub enum MergeMode {
#[default]
Merge,
Clobber,
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum BlueprintLookupError {
#[error("the entrypoint blueprint {0} was not found")]
BlueprintNotFound(SmolStr),
#[error("when trying to inherit from another blueprint, the following loop was found: {0:?}")]
InheritanceLoop(Vec<SmolStr>),
#[error(
"the blueprint {0} tried to inherit from the blueprint {1} but the second was not found"
)]
InheriteeNotFound(SmolStr, SmolStr),
}
#[derive(Debug, Error)]
pub enum BlueprintParseError {
#[error("error when parsing kdl: {0}")]
Parse(#[from] kdl::KdlError),
#[error("error when turning kdl into blueprints: {0}")]
Deser(#[from] RawBlueprintDeserError),
}
macro_rules! passthru {
($($func:ident -> $ret:ty);*) => {
$(
fn $func<'a>(&'a self) -> $ret {
match self {
BlueprintParseError::Parse(x) => x.$func(),
BlueprintParseError::Deser(x) => x.$func(),
}
}
)*
};
}
impl Diagnostic for BlueprintParseError {
passthru! {
code -> Option<Box<dyn Display + 'a>>;
severity -> Option<Severity>;
help -> Option<Box<dyn Display + 'a>>;
url -> Option<Box<dyn Display + 'a>>;
source_code -> Option<&dyn SourceCode>;
labels -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>>;
related -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>>;
diagnostic_source -> Option<&dyn Diagnostic>
}
}
#[derive(Debug, Error, Diagnostic)]
#[error("{kind}")]
pub struct RawBlueprintDeserError {
#[label]
pub span: SourceSpan,
#[source_code]
pub src: NamedSource,
pub kind: RawBlueprintParseErrorKind,
}
const TOP_LEVEL_REQS: &str = r#"only `merge="merge"` or `merge="clobber"` are allowed"#;
const ANN_REQS: &str =
r#"only `(splice)a-blueprint` with no further args/props/children is allowed"#;
#[derive(Debug, Error)]
pub enum RawBlueprintParseErrorKind {
#[error("blueprint node had no children")]
NoChildren,
#[error("blueprint node had an argument; {}", TOP_LEVEL_REQS)]
TopLevelArgument,
#[error("blueprint node had an annotation; {}", TOP_LEVEL_REQS)]
TopLevelAnnotation,
#[error("blueprint node had an invalid key; {}", TOP_LEVEL_REQS)]
InvalidKey,
#[error(r#"the `merge` key didn't equal "clobber" or "merge""#)]
BadMerge,
#[error("redefined `inherit`")]
ClobberInherit,
#[error("redefined `merge`")]
ClobberMerge,
#[error("bad annotation; {}", ANN_REQS)]
BadAnnotation,
}