active_core/convert/
export.rs1use crate::diag::{Diagnostic, Severity};
7use crate::ir::Value;
8
9pub fn value_to_yaml_string(v: &Value) -> Result<String, String> {
12 serde_yaml::to_string(&super::value_to_json(v))
13 .map_err(|e| format!("YAML serialization failed: {e}"))
14}
15
16pub fn value_to_toml_string(v: &Value) -> Result<(String, Vec<Diagnostic>), String> {
22 check_toml_homogeneous(v, "")?;
23 let mut diags = Vec::new();
24 let doc = match build_toml_value(v, "", &mut diags) {
25 Some(toml::Value::Table(map)) => toml::Value::Table(map),
27 Some(other) => {
30 let mut map = toml::map::Map::new();
31 map.insert("value".to_string(), other);
32 toml::Value::Table(map)
33 }
34 None => toml::Value::Table(toml::map::Map::new()),
35 };
36 toml::to_string(&doc).map(|s| (s, diags)).map_err(|e| {
37 format!("TOML serialization failed: {e}")
39 })
40}
41
42fn check_toml_homogeneous(v: &Value, path: &str) -> Result<(), String> {
46 let label = if path.is_empty() { "(root)" } else { path };
47 match v {
48 Value::Array(items) => {
49 let mut kinds = std::collections::HashSet::new();
50 for (i, item) in items.iter().enumerate() {
51 let item_path = format!("{path}[{i}]");
52 check_toml_homogeneous(item, &item_path)?;
53 if !matches!(item, Value::Null) {
54 kinds.insert(toml_kind(item));
55 }
56 }
57 if kinds.len() > 1 {
58 return Err(format!(
59 "mixed-type array at '{label}' cannot be represented in TOML"
60 ));
61 }
62 Ok(())
63 }
64 Value::Table(map) => {
65 for (key, val) in map {
66 let child = if path.is_empty() {
67 key.clone()
68 } else {
69 format!("{path}.{key}")
70 };
71 check_toml_homogeneous(val, &child)?;
72 }
73 Ok(())
74 }
75 _ => Ok(()),
76 }
77}
78
79fn toml_kind(v: &Value) -> u8 {
80 match v {
81 Value::Null => 0,
82 Value::Bool(_) => 1,
83 Value::Int(_) => 2,
84 Value::Float(_) => 3,
85 Value::String(_) | Value::DurationMs(_) | Value::Quantity(_, _) | Value::Secret(_) => 4,
86 Value::Array(_) => 5,
87 Value::Table(_) => 6,
88 }
89}
90
91fn build_toml_value(v: &Value, path: &str, diags: &mut Vec<Diagnostic>) -> Option<toml::Value> {
94 let at = |key: &str| {
95 if path.is_empty() {
96 key.to_string()
97 } else {
98 format!("{path}.{key}")
99 }
100 };
101 match v {
102 Value::Null => {
104 diags.push(null_omitted(if path.is_empty() { "(root)" } else { path }));
105 None
106 }
107 Value::Bool(b) => Some(toml::Value::Boolean(*b)),
108 Value::Int(i) => Some(toml::Value::Integer(*i)),
109 Value::Float(f) => Some(toml::Value::Float(*f)),
110 Value::String(s) => Some(toml::Value::String(s.clone())),
111 Value::DurationMs(ms) => Some(toml::Value::String(format!("{ms}ms"))),
115 Value::Quantity(n, unit) => Some(toml::Value::String(unit.format_amount(*n))),
116 Value::Secret(_) => Some(toml::Value::String("***SECRET***".to_string())),
117 Value::Array(items) => {
118 let mut out = Vec::with_capacity(items.len());
119 for (i, item) in items.iter().enumerate() {
120 let item_path = format!("{path}[{i}]");
121 if let Some(toml_item) = build_toml_value(item, &item_path, diags) {
122 out.push(toml_item);
123 }
124 }
125 Some(toml::Value::Array(out))
126 }
127 Value::Table(map) => {
128 let mut out = toml::map::Map::with_capacity(map.len());
129 for (key, val) in map {
130 if let Some(toml_val) = build_toml_value(val, &at(key), diags) {
131 out.insert(key.clone(), toml_val);
132 }
133 }
134 Some(toml::Value::Table(out))
135 }
136 }
137}
138
139fn null_omitted(path: &str) -> Diagnostic {
140 Diagnostic {
141 severity: Severity::Warning,
142 code: "W006".to_string(),
143 message: format!("null has no TOML representation; omitted '{path}'"),
144 path: Some(path.to_string()),
145 span: None,
146 repair: None,
147 confidence: 1.0,
148 file: None,
149 }
150}