use crate::diag::{Diagnostic, Severity};
use crate::ir::Value;
pub fn value_to_yaml_string(v: &Value) -> Result<String, String> {
serde_yaml::to_string(&super::value_to_json(v))
.map_err(|e| format!("YAML serialization failed: {e}"))
}
pub fn value_to_toml_string(v: &Value) -> Result<(String, Vec<Diagnostic>), String> {
check_toml_homogeneous(v, "")?;
let mut diags = Vec::new();
let doc = match build_toml_value(v, "", &mut diags) {
Some(toml::Value::Table(map)) => toml::Value::Table(map),
Some(other) => {
let mut map = toml::map::Map::new();
map.insert("value".to_string(), other);
toml::Value::Table(map)
}
None => toml::Value::Table(toml::map::Map::new()),
};
toml::to_string(&doc).map(|s| (s, diags)).map_err(|e| {
format!("TOML serialization failed: {e}")
})
}
fn check_toml_homogeneous(v: &Value, path: &str) -> Result<(), String> {
let label = if path.is_empty() { "(root)" } else { path };
match v {
Value::Array(items) => {
let mut kinds = std::collections::HashSet::new();
for (i, item) in items.iter().enumerate() {
let item_path = format!("{path}[{i}]");
check_toml_homogeneous(item, &item_path)?;
if !matches!(item, Value::Null) {
kinds.insert(toml_kind(item));
}
}
if kinds.len() > 1 {
return Err(format!(
"mixed-type array at '{label}' cannot be represented in TOML"
));
}
Ok(())
}
Value::Table(map) => {
for (key, val) in map {
let child = if path.is_empty() {
key.clone()
} else {
format!("{path}.{key}")
};
check_toml_homogeneous(val, &child)?;
}
Ok(())
}
_ => Ok(()),
}
}
fn toml_kind(v: &Value) -> u8 {
match v {
Value::Null => 0,
Value::Bool(_) => 1,
Value::Int(_) => 2,
Value::Float(_) => 3,
Value::String(_) | Value::DurationMs(_) | Value::Quantity(_, _) | Value::Secret(_) => 4,
Value::Array(_) => 5,
Value::Table(_) => 6,
}
}
fn build_toml_value(v: &Value, path: &str, diags: &mut Vec<Diagnostic>) -> Option<toml::Value> {
let at = |key: &str| {
if path.is_empty() {
key.to_string()
} else {
format!("{path}.{key}")
}
};
match v {
Value::Null => {
diags.push(null_omitted(if path.is_empty() { "(root)" } else { path }));
None
}
Value::Bool(b) => Some(toml::Value::Boolean(*b)),
Value::Int(i) => Some(toml::Value::Integer(*i)),
Value::Float(f) => Some(toml::Value::Float(*f)),
Value::String(s) => Some(toml::Value::String(s.clone())),
Value::DurationMs(ms) => Some(toml::Value::String(format!("{ms}ms"))),
Value::Quantity(n, unit) => Some(toml::Value::String(format!("{n}{}", unit.suffix()))),
Value::Secret(_) => Some(toml::Value::String("***SECRET***".to_string())),
Value::Array(items) => {
let mut out = Vec::with_capacity(items.len());
for (i, item) in items.iter().enumerate() {
let item_path = format!("{path}[{i}]");
if let Some(toml_item) = build_toml_value(item, &item_path, diags) {
out.push(toml_item);
}
}
Some(toml::Value::Array(out))
}
Value::Table(map) => {
let mut out = toml::map::Map::with_capacity(map.len());
for (key, val) in map {
if let Some(toml_val) = build_toml_value(val, &at(key), diags) {
out.insert(key.clone(), toml_val);
}
}
Some(toml::Value::Table(out))
}
}
}
fn null_omitted(path: &str) -> Diagnostic {
Diagnostic {
severity: Severity::Warning,
code: "W006".to_string(),
message: format!("null has no TOML representation; omitted '{path}'"),
path: Some(path.to_string()),
span: None,
repair: None,
confidence: 1.0,
file: None,
}
}