const ALIASES: &[(&str, &str)] = &[("gateway", "a2a")];
const NESTED_ALIASES: &[(&str, &str)] = &[("providers.zhipu", "providers.zai")];
fn split_nested<'a>(
legacy: &'a str,
canonical: &'a str,
) -> Option<(Vec<&'a str>, &'a str, &'a str)> {
let lp: Vec<&str> = legacy.split('.').collect();
let cp: Vec<&str> = canonical.split('.').collect();
if lp.len() < 2 || lp.len() != cp.len() {
return None;
}
if lp[..lp.len() - 1] != cp[..cp.len() - 1] {
return None;
}
let parents = lp[..lp.len() - 1].to_vec();
Some((parents, *lp.last()?, *cp.last()?))
}
fn navigate_value<'a>(
table: &'a mut toml::value::Table,
path: &[&str],
) -> Option<&'a mut toml::value::Table> {
let mut cur = table;
for &p in path {
cur = cur.get_mut(p)?.as_table_mut()?;
}
Some(cur)
}
fn navigate_edit<'a>(
table: &'a mut toml_edit::Table,
path: &[&str],
) -> Option<&'a mut toml_edit::Table> {
let mut cur = table;
for &p in path {
cur = cur.get_mut(p)?.as_table_mut()?;
}
Some(cur)
}
fn fold_in_value_table(
table: &mut toml::value::Table,
legacy_key: &str,
canonical_key: &str,
label: &'static str,
folded: &mut Vec<&'static str>,
) {
let Some(legacy_val) = table.remove(legacy_key) else {
return;
};
folded.push(label);
match table.get_mut(canonical_key) {
Some(canon_val) => merge_into(canon_val, legacy_val),
None => {
table.insert(canonical_key.to_string(), legacy_val);
}
}
}
fn fold_in_edit_table(
table: &mut toml_edit::Table,
legacy_key: &str,
canonical_key: &str,
label: &'static str,
renamed: &mut Vec<&'static str>,
) {
let Some(legacy_item) = table.remove(legacy_key) else {
return;
};
renamed.push(label);
match table.get_mut(canonical_key) {
Some(existing) => {
if let (Some(into), Some(from)) = (existing.as_table_mut(), legacy_item.as_table()) {
for (k, v) in from.iter() {
if !into.contains_key(k) {
into.insert(k, v.clone());
}
}
}
}
None => {
table.insert(canonical_key, legacy_item);
}
}
}
pub(crate) fn fold_legacy_sections(doc: &mut toml::Value) -> Vec<&'static str> {
let mut folded = Vec::new();
let Some(table) = doc.as_table_mut() else {
return folded;
};
for (legacy, canonical) in ALIASES {
fold_in_value_table(table, legacy, canonical, legacy, &mut folded);
}
for (legacy, canonical) in NESTED_ALIASES {
let Some((parents, legacy_leaf, canonical_leaf)) = split_nested(legacy, canonical) else {
continue;
};
let Some(parent) = navigate_value(table, &parents) else {
continue;
};
fold_in_value_table(parent, legacy_leaf, canonical_leaf, legacy, &mut folded);
}
folded
}
fn merge_into(into: &mut toml::Value, from: toml::Value) {
let (Some(into_t), toml::Value::Table(from_t)) = (into.as_table_mut(), from) else {
return;
};
for (k, v) in from_t {
match into_t.get_mut(&k) {
Some(existing) => merge_into(existing, v),
None => {
into_t.insert(k, v);
}
}
}
}
pub(crate) fn migrate_file(path: &std::path::Path) -> std::io::Result<Vec<&'static str>> {
let contents = std::fs::read_to_string(path)?;
let mut doc = match contents.parse::<toml_edit::DocumentMut>() {
Ok(doc) => doc,
Err(_) => return Ok(Vec::new()),
};
let mut renamed = Vec::new();
for (legacy, canonical) in ALIASES {
fold_in_edit_table(doc.as_table_mut(), legacy, canonical, legacy, &mut renamed);
}
for (legacy, canonical) in NESTED_ALIASES {
let Some((parents, legacy_leaf, canonical_leaf)) = split_nested(legacy, canonical) else {
continue;
};
let Some(parent) = navigate_edit(doc.as_table_mut(), &parents) else {
continue;
};
fold_in_edit_table(parent, legacy_leaf, canonical_leaf, legacy, &mut renamed);
}
if !renamed.is_empty() {
tracing::info!(
"alias_merge: rewrote {} to rename legacy sections: {}",
path.display(),
renamed.join(", ")
);
std::fs::write(path, doc.to_string())?;
}
Ok(renamed)
}