1use crate::diag::{Diagnostic, Severity};
8use serde_json::Value as JsonValue;
9use std::collections::HashSet;
10
11pub fn json_text_to_active(text: &str) -> Result<(String, Vec<Diagnostic>), String> {
14 let value: JsonValue =
15 serde_json::from_str(text).map_err(|e| hard_error("invalid JSON", &e.to_string()))?;
16 Ok(json_value_to_active(&value))
17}
18
19pub fn yaml_text_to_active(text: &str) -> Result<(String, Vec<Diagnostic>), String> {
21 let yaml: serde_yaml::Value =
22 serde_yaml::from_str(text).map_err(|e| hard_error("invalid YAML", &e.to_string()))?;
23 let mut emitter = Emitter::new();
24 let value = emitter.yaml_to_json(&yaml, "");
25 let (source, mut diags) = emitter.finish(&value);
26 diags.sort_by(|a, b| a.code.cmp(&b.code));
27 Ok((source, diags))
28}
29
30pub fn toml_text_to_active(text: &str) -> Result<(String, Vec<Diagnostic>), String> {
32 let toml: toml::Value =
33 toml::from_str(text).map_err(|e| hard_error("invalid TOML", &e.to_string()))?;
34 let mut emitter = Emitter::new();
35 let value = emitter.toml_to_json(&toml, "");
36 let (source, mut diags) = emitter.finish(&value);
37 diags.sort_by(|a, b| a.code.cmp(&b.code));
38 Ok((source, diags))
39}
40
41fn hard_error(prefix: &str, detail: &str) -> String {
45 let first = detail.lines().next().unwrap_or("").trim();
46 format!("{prefix}: {first} (source excerpt withheld)")
47}
48
49pub fn json_value_to_active(v: &JsonValue) -> (String, Vec<Diagnostic>) {
54 Emitter::new().finish(v)
55}
56
57struct Emitter {
59 diags: Vec<Diagnostic>,
60}
61
62impl Emitter {
63 fn new() -> Self {
64 Self { diags: Vec::new() }
65 }
66
67 fn finish(mut self, v: &JsonValue) -> (String, Vec<Diagnostic>) {
68 let mut out = String::new();
69 match v {
70 JsonValue::Object(map) => {
71 self.emit_block(map, &mut out, 0, "");
72 }
73 other => {
74 self.note(
75 "W009",
76 format!("top-level {} wrapped as 'value'", Self::kind_name(other)),
77 );
78 let inline = self.inline(other);
79 out.push_str(&format!("value = {inline}\n"));
80 }
81 }
82 (out, self.diags)
83 }
84
85 fn kind_name(v: &JsonValue) -> &'static str {
86 match v {
87 JsonValue::Null => "null",
88 JsonValue::Bool(_) => "boolean",
89 JsonValue::Number(_) => "number",
90 JsonValue::String(_) => "string",
91 JsonValue::Array(_) => "array",
92 JsonValue::Object(_) => "object",
93 }
94 }
95
96 fn emit_block(
100 &mut self,
101 map: &serde_json::Map<String, JsonValue>,
102 out: &mut String,
103 indent: usize,
104 path: &str,
105 ) {
106 let pad = " ".repeat(indent);
107 let mut seen = HashSet::new();
108 for (key, val) in map {
109 let (clean, renamed) = self.block_key(key, val);
110 if renamed {
111 self.note("W007", format!("imported key '{key}' renamed to '{clean}'"));
112 }
113 if !seen.insert(clean.clone()) {
114 self.note(
115 "W007",
116 format!("duplicate key '{clean}' after sanitization; last wins"),
117 );
118 }
119 let child = if path.is_empty() {
120 clean.clone()
121 } else {
122 format!("{path}.{clean}")
123 };
124 match val {
125 JsonValue::Object(nested) => {
126 out.push_str(&format!("{pad}{clean} {{\n"));
127 self.emit_block(nested, out, indent + 1, &child);
128 out.push_str(&format!("{pad}}}\n"));
129 }
130 _ => {
131 let inline = self.inline(val);
132 out.push_str(&format!("{pad}{clean} = {inline}\n"));
133 }
134 }
135 }
136 }
137
138 fn block_key(&self, key: &str, val: &JsonValue) -> (String, bool) {
142 let clean = sanitize_key(key);
143 if let Some(renamed) = contextual_rename(&clean, val) {
144 return (renamed, true);
145 }
146 let changed = clean != key;
147 (clean, changed)
148 }
149
150 fn inline(&mut self, val: &JsonValue) -> String {
156 match val {
157 JsonValue::Null => "null".to_string(),
158 JsonValue::Bool(b) => b.to_string(),
159 JsonValue::Number(n) => {
160 if let Some(i) = n.as_i64() {
161 i.to_string()
162 } else if let Some(u) = n.as_u64() {
163 self.note(
164 "W010",
165 format!("integer {u} exceeds i64 range; emitted as float"),
166 );
167 format!("{}.0", u as f64)
173 } else {
174 n.as_f64()
182 .map(|f| {
183 if !f.is_finite() {
184 "null".to_string()
185 } else {
186 let s = format!("{f}");
187 if s.contains('.') {
188 s
189 } else {
190 format!("{s}.0")
191 }
192 }
193 })
194 .unwrap_or("null".to_string())
195 }
196 }
197 JsonValue::String(s) => Self::quote(s),
198 JsonValue::Array(items) => {
199 let parts: Vec<String> = items.iter().map(|item| self.inline(item)).collect();
200 format!("[{}]", parts.join(", "))
201 }
202 JsonValue::Object(map) => {
203 let mut parts = Vec::with_capacity(map.len());
204 let mut seen = HashSet::new();
205 for (key, item) in map {
206 let clean = sanitize_key(key);
207 let clean = contextual_rename(&clean, item).unwrap_or(clean);
208 if clean != *key {
209 self.note("W007", format!("imported key '{key}' renamed to '{clean}'"));
210 }
211 if !seen.insert(clean.clone()) {
212 self.note(
213 "W007",
214 format!("duplicate key '{clean}' after sanitization; last wins"),
215 );
216 }
217 let inline = self.inline(item);
218 parts.push(format!("{clean} = {inline}"));
219 }
220 format!("{{ {} }}", parts.join(", "))
221 }
222 }
223 }
224
225 fn quote(s: &str) -> String {
228 let mut out = String::with_capacity(s.len() + 2);
229 out.push('"');
230 for c in s.chars() {
231 match c {
232 '\n' => out.push_str("\\n"),
233 '\t' => out.push_str("\\t"),
234 '\r' => out.push_str("\\r"),
235 '\\' => out.push_str("\\\\"),
236 '"' => out.push_str("\\\""),
237 '\0' => out.push_str("\\0"),
238 '$' => out.push_str("\\$"),
239 _ => out.push(c),
240 }
241 }
242 out.push('"');
243 out
244 }
245
246 fn note(&mut self, code: &str, message: String) {
247 self.diags.push(Self::note_static(code, message));
248 }
249
250 fn note_static(code: &str, message: String) -> Diagnostic {
251 Diagnostic {
252 severity: Severity::Warning,
253 code: code.into(),
254 message,
255 path: None,
256 span: None,
257 repair: None,
258 confidence: 1.0,
259 file: None,
260 }
261 }
262
263 fn yaml_to_json(&mut self, v: &serde_yaml::Value, path: &str) -> JsonValue {
266 match v {
267 serde_yaml::Value::Null => JsonValue::Null,
268 serde_yaml::Value::Bool(b) => JsonValue::Bool(*b),
269 serde_yaml::Value::Number(n) => {
270 if let Some(i) = n.as_i64() {
271 JsonValue::from(i)
272 } else if let Some(u) = n.as_u64() {
273 JsonValue::from(u)
274 } else {
275 match n.as_f64() {
276 Some(f) if f.is_finite() => JsonValue::from(f),
277 _ => {
280 self.note(
281 "W010",
282 format!(
283 "non-finite YAML number at '{}' converted to null",
284 display_path(path)
285 ),
286 );
287 JsonValue::Null
288 }
289 }
290 }
291 }
292 serde_yaml::Value::String(s) => JsonValue::String(s.clone()),
293 serde_yaml::Value::Sequence(items) => JsonValue::Array(
294 items
295 .iter()
296 .enumerate()
297 .map(|(i, item)| self.yaml_to_json(item, &format!("{path}[{i}]")))
298 .collect(),
299 ),
300 serde_yaml::Value::Mapping(map) => {
301 let mut out = serde_json::Map::new();
302 for (key, val) in map {
303 let key_str = match key {
304 serde_yaml::Value::String(s) => s.clone(),
305 serde_yaml::Value::Null => {
306 self.note("W007", "YAML null key stringified to 'null'".to_string());
307 "null".to_string()
308 }
309 other => {
310 let rendered = match other {
311 serde_yaml::Value::Bool(b) => b.to_string(),
312 serde_yaml::Value::Number(n) => n.to_string(),
313 _ => "key".to_string(),
314 };
315 self.note(
316 "W007",
317 format!("YAML non-string key stringified to '{rendered}'"),
318 );
319 rendered
320 }
321 };
322 let child = if path.is_empty() {
323 key_str.clone()
324 } else {
325 format!("{path}.{key_str}")
326 };
327 out.insert(key_str, self.yaml_to_json(val, &child));
328 }
329 JsonValue::Object(out)
330 }
331 serde_yaml::Value::Tagged(tagged) => self.yaml_to_json(&tagged.value, path),
332 }
333 }
334
335 fn toml_to_json(&mut self, v: &toml::Value, path: &str) -> JsonValue {
338 match v {
339 toml::Value::String(s) => JsonValue::String(s.clone()),
340 toml::Value::Integer(i) => JsonValue::from(*i),
341 toml::Value::Float(f) => {
342 if f.is_finite() {
343 JsonValue::from(*f)
344 } else {
345 self.note(
346 "W010",
347 format!(
348 "non-finite TOML number at '{}' converted to null",
349 display_path(path)
350 ),
351 );
352 JsonValue::Null
353 }
354 }
355 toml::Value::Boolean(b) => JsonValue::Bool(*b),
356 toml::Value::Datetime(dt) => {
357 self.note(
358 "W008",
359 format!(
360 "TOML datetime at '{}' imported as string",
361 display_path(path)
362 ),
363 );
364 JsonValue::String(dt.to_string())
365 }
366 toml::Value::Array(items) => JsonValue::Array(
367 items
368 .iter()
369 .enumerate()
370 .map(|(i, item)| self.toml_to_json(item, &format!("{path}[{i}]")))
371 .collect(),
372 ),
373 toml::Value::Table(map) => {
374 let mut out = serde_json::Map::new();
375 for (key, val) in map {
376 let child = if path.is_empty() {
377 key.clone()
378 } else {
379 format!("{path}.{key}")
380 };
381 out.insert(key.clone(), self.toml_to_json(val, &child));
382 }
383 JsonValue::Object(out)
384 }
385 }
386 }
387}
388
389fn sanitize_key(key: &str) -> String {
397 const RESERVED: &[&str] = &["let", "env", "secret", "true", "false", "null"];
398 let mut clean = String::with_capacity(key.len());
399 for c in key.chars() {
400 if c.is_ascii_alphanumeric() || c == '_' {
401 clean.push(c);
402 } else {
403 clean.push('_');
404 }
405 }
406 if clean.is_empty() {
407 return "key".to_string();
408 }
409 if clean.starts_with(|c: char| c.is_ascii_digit()) {
410 clean.insert(0, '_');
411 }
412 if RESERVED.contains(&clean.as_str()) {
413 clean.push('_');
414 }
415 clean
416}
417
418fn contextual_rename(clean: &str, val: &JsonValue) -> Option<String> {
426 match (clean, val) {
427 ("schema", JsonValue::Object(_)) => Some("schema_".to_string()),
428 ("depends_on", JsonValue::Array(_)) => Some("depends_on_".to_string()),
429 _ => None,
430 }
431}
432
433fn display_path(path: &str) -> &str {
434 if path.is_empty() {
435 "(root)"
436 } else {
437 path
438 }
439}