use std::collections::{HashMap, HashSet};
use serde_json::{Map, Value};
use super::{DidTemplate, TemplateError, TemplateVars};
pub(super) fn render(tpl: &DidTemplate, vars: &TemplateVars) -> Result<Value, TemplateError> {
let mut effective: HashMap<String, Value> = tpl
.optional_vars
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
for k in vars.keys() {
if let Some(v) = vars.get(k) {
effective.insert(k.clone(), v.clone());
}
}
if !effective.contains_key("WS_URL")
&& let Some(url) = effective.get("URL").and_then(Value::as_str)
{
if let Some(rest) = url.strip_prefix("https://") {
let trimmed = rest.trim_end_matches('/');
effective.insert(
"WS_URL".into(),
Value::String(format!("wss://{trimmed}/ws")),
);
} else if let Some(rest) = url.strip_prefix("http://") {
let trimmed = rest.trim_end_matches('/');
effective.insert("WS_URL".into(), Value::String(format!("ws://{trimmed}/ws")));
}
}
let mut missing: Vec<String> = Vec::new();
for required in &tpl.required_vars {
if !effective.contains_key(required) {
missing.push(required.clone());
}
}
if !missing.is_empty() {
missing.sort();
return Err(TemplateError::MissingVars(missing.join(", ")));
}
let rendered = substitute_value(&tpl.document, &effective);
let provided: HashSet<&str> = effective.keys().map(String::as_str).collect();
let mut unresolved = HashSet::new();
walk_placeholders(&rendered, &mut unresolved);
let truly_unresolved: Vec<String> = unresolved
.into_iter()
.filter(|name| !provided.contains(name.as_str()))
.collect();
if !truly_unresolved.is_empty() {
let mut names = truly_unresolved;
names.sort();
return Err(TemplateError::Unresolved(names.join(", ")));
}
Ok(rendered)
}
fn substitute_value(value: &Value, vars: &HashMap<String, Value>) -> Value {
match value {
Value::String(s) => substitute_string(s, vars),
Value::Array(items) => {
Value::Array(items.iter().map(|v| substitute_value(v, vars)).collect())
}
Value::Object(map) => {
let mut out = Map::with_capacity(map.len());
for (k, v) in map {
let new_key = match substitute_string(k, vars) {
Value::String(s) => s,
other => other.to_string(),
};
out.insert(new_key, substitute_value(v, vars));
}
Value::Object(out)
}
other => other.clone(),
}
}
fn substitute_string(s: &str, vars: &HashMap<String, Value>) -> Value {
if let Some(name) = whole_string_token(s) {
if let Some(v) = vars.get(name) {
return v.clone();
}
return Value::String(s.to_string());
}
let mut out = String::with_capacity(s.len());
let mut rest = s;
while let Some(brace) = rest.find('{') {
out.push_str(&rest[..brace]);
let after = &rest[brace..];
if let Some(close) = find_token_end(after.as_bytes(), 0) {
let name = &after[1..close];
if is_token_name(name) {
match vars.get(name) {
Some(Value::String(vs)) => out.push_str(vs),
Some(other) => out.push_str(&other.to_string()),
None => out.push_str(&after[..=close]), }
rest = &after[close + 1..];
continue;
}
}
out.push('{');
rest = &after[1..];
}
out.push_str(rest);
Value::String(out)
}
fn whole_string_token(s: &str) -> Option<&str> {
let bytes = s.as_bytes();
if bytes.len() < 3 || bytes[0] != b'{' || bytes[bytes.len() - 1] != b'}' {
return None;
}
let name = &s[1..s.len() - 1];
if is_token_name(name) {
Some(name)
} else {
None
}
}
fn find_token_end(bytes: &[u8], start: usize) -> Option<usize> {
debug_assert_eq!(bytes[start], b'{');
let mut j = start + 1;
while j < bytes.len() {
let c = bytes[j];
if c == b'}' {
return Some(j);
}
if !(c.is_ascii_uppercase() || c.is_ascii_digit() || c == b'_') {
return None;
}
j += 1;
}
None
}
fn is_token_name(s: &str) -> bool {
let bytes = s.as_bytes();
if bytes.is_empty() {
return false;
}
let first = bytes[0];
if !(first.is_ascii_uppercase() || first == b'_') {
return false;
}
bytes[1..]
.iter()
.all(|&c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == b'_')
}
pub(super) fn walk_placeholders(value: &Value, out: &mut HashSet<String>) {
match value {
Value::String(s) => scan_string(s, out),
Value::Array(items) => {
for item in items {
walk_placeholders(item, out);
}
}
Value::Object(map) => {
for (k, v) in map {
scan_string(k, out);
walk_placeholders(v, out);
}
}
_ => {}
}
}
fn scan_string(s: &str, out: &mut HashSet<String>) {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'{'
&& let Some(end) = find_token_end(bytes, i)
{
let name = &s[i + 1..end];
if is_token_name(name) {
out.insert(name.to_string());
i = end + 1;
continue;
}
}
i += 1;
}
}