use anyhow::{Context, bail};
use regex::Regex;
use serde_json::Value;
use std::sync::OnceLock;
static TRAILING_COMMA_RE: OnceLock<Regex> = OnceLock::new();
pub fn finalize(raw: &str) -> anyhow::Result<Value> {
let fixed = fix_trailing_commas(raw);
let card: Value = serde_json::from_str(&fixed)
.context("rendered output is not valid JSON — check for template syntax errors")?;
validate_adaptive_card_shape(&card)?;
Ok(card)
}
pub fn fix_trailing_commas(input: &str) -> String {
let re = TRAILING_COMMA_RE.get_or_init(|| Regex::new(r",\s*([\]}])").expect("valid regex"));
re.replace_all(input, "$1").to_string()
}
pub fn validate_adaptive_card_shape(card: &Value) -> anyhow::Result<()> {
let obj = card
.as_object()
.context("rendered card is not a JSON object")?;
match obj.get("type").and_then(|v| v.as_str()) {
Some("AdaptiveCard") => {}
Some(other) => bail!("card type is \"{}\", expected \"AdaptiveCard\"", other),
None => bail!("card missing \"type\" field"),
}
if obj.get("version").is_none() {
bail!("card missing \"version\" field");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fix_trailing_comma_in_array() {
assert_eq!(fix_trailing_commas("[1, 2, 3,]"), "[1, 2, 3]");
}
#[test]
fn test_fix_trailing_comma_in_object() {
assert_eq!(fix_trailing_commas(r#"{"a": 1,}"#), r#"{"a": 1}"#);
}
#[test]
fn test_no_trailing_comma_untouched() {
assert_eq!(fix_trailing_commas("[1, 2]"), "[1, 2]");
}
#[test]
fn test_finalize_valid_card() {
let raw = r#"{"type":"AdaptiveCard","version":"1.5","body":[]}"#;
let result = finalize(raw).unwrap();
assert_eq!(result["type"], "AdaptiveCard");
}
#[test]
fn test_finalize_missing_type() {
let raw = r#"{"version":"1.5"}"#;
assert!(finalize(raw).is_err());
}
#[test]
fn test_finalize_wrong_type() {
let raw = r#"{"type":"Other","version":"1.5"}"#;
assert!(finalize(raw).is_err());
}
}