use std::path::PathBuf;
use saphyr::{LoadableYamlNode, MarkedYaml, Scalar, YamlData};
use shaperail_core::ResourceDefinition;
use crate::span::SpanMap;
#[derive(Debug, thiserror::Error)]
pub enum SaphyrParseError {
#[error("yaml scan error: {0}")]
Scan(#[from] saphyr::ScanError),
#[error("deserialization error: {0}")]
Deserialize(#[from] serde_yaml::Error),
#[error("empty or non-mapping YAML document")]
EmptyDocument,
}
pub fn parse_with_spans(yaml: &str) -> Result<(ResourceDefinition, SpanMap), SaphyrParseError> {
parse_with_spans_in_file(yaml, PathBuf::new())
}
pub fn parse_with_spans_in_file(
yaml: &str,
file: PathBuf,
) -> Result<(ResourceDefinition, SpanMap), SaphyrParseError> {
let docs = MarkedYaml::load_from_str(yaml)?;
let doc = docs
.into_iter()
.next()
.ok_or(SaphyrParseError::EmptyDocument)?;
let mut span_map = SpanMap::new(file);
let serde_value = walk_node(&doc, "", &mut span_map);
let rd: ResourceDefinition = serde_yaml::from_value(serde_value)?;
Ok((rd, span_map))
}
fn scalar_to_serde(scalar: &Scalar<'_>) -> serde_yaml::Value {
match scalar {
Scalar::Null => serde_yaml::Value::Null,
Scalar::Boolean(b) => serde_yaml::Value::Bool(*b),
Scalar::Integer(i) => serde_yaml::Value::Number(serde_yaml::Number::from(*i)),
Scalar::FloatingPoint(f) => {
serde_yaml::Value::Number(serde_yaml::Number::from(f64::from(*f)))
}
Scalar::String(s) => serde_yaml::Value::String(s.as_ref().to_owned()),
}
}
fn walk_node(node: &MarkedYaml<'_>, path: &str, map: &mut SpanMap) -> serde_yaml::Value {
let start = node.span.start;
let end = node.span.end;
let line = start.line() as u32;
let col = start.col() as u32 + 1;
let end_line = end.line() as u32;
let end_col = if end.line() == 0 && end.col() == 0 {
col
} else {
end.col() as u32 + 1
};
map.insert(path, line, col, end_line, end_col);
match &node.data {
YamlData::Value(scalar) => scalar_to_serde(scalar),
YamlData::Sequence(seq) => {
let items = seq
.iter()
.enumerate()
.map(|(i, child)| {
let child_path = if path.is_empty() {
format!("[{i}]")
} else {
format!("{path}[{i}]")
};
walk_node(child, &child_path, map)
})
.collect();
serde_yaml::Value::Sequence(items)
}
YamlData::Mapping(mapping) => {
let mut out = serde_yaml::Mapping::new();
for (k, v) in mapping {
let key_str = match &k.data {
YamlData::Value(Scalar::String(s)) => s.as_ref().to_owned(),
YamlData::Value(scalar) => format!("{scalar:?}"),
_ => continue,
};
let key_path = if path.is_empty() {
key_str.clone()
} else {
format!("{path}.{key_str}")
};
let serde_val = walk_node(v, &key_path, map);
out.insert(serde_yaml::Value::String(key_str), serde_val);
}
serde_yaml::Value::Mapping(out)
}
YamlData::Tagged(_, inner) => walk_node(inner, path, map),
YamlData::Alias(_) | YamlData::BadValue => serde_yaml::Value::Null,
YamlData::Representation(s, _, _) => serde_yaml::Value::String(s.as_ref().to_owned()),
}
}