use super::companion_specs::{CompanionSpec, companions_for};
use super::expand::ExpandReport;
use std::collections::HashSet;
fn type_norm_str(s: &str) -> String {
s.to_lowercase().replace('_', "")
}
fn asset_type_norm(v: &serde_json::Value) -> String {
v.get("type")
.and_then(|t| t.as_str())
.map(type_norm_str)
.unwrap_or_default()
}
fn record_if_overridden(
assets: &mut [serde_json::Value],
claimed_names: &HashSet<String>,
report: &mut ExpandReport,
spec: &CompanionSpec,
) {
let claimed = claimed_names.contains(spec.name);
let ours = report.injected.iter().any(|i| i.name == spec.name);
if claimed && !ours {
report.record_shadowed(spec.name, spec.asset_type, "companion", spec.args.clone());
super::shadow::merge_into_authored(assets, spec.name, &spec.args);
}
}
fn companions_for_type(asset_type: &str) -> Vec<CompanionSpec> {
companions_for(&type_norm_str(asset_type))
}
pub(crate) fn inject_companions(assets: &mut Vec<serde_json::Value>, report: &mut ExpandReport) {
loop {
let present_types: HashSet<String> = assets.iter().map(asset_type_norm).collect();
let claimed_names: HashSet<String> = assets
.iter()
.filter_map(|v| v.get("name").and_then(|n| n.as_str()))
.map(str::to_string)
.collect();
let mut candidates: Vec<CompanionSpec> = Vec::new();
for value in assets.iter() {
let Some(t) = value.get("type").and_then(|s| s.as_str()) else {
continue;
};
candidates.extend(companions_for_type(t));
}
let mut seen_names: HashSet<String> = HashSet::new();
let mut to_inject = Vec::new();
for spec in candidates {
if present_types.contains(&type_norm_str(spec.asset_type)) {
record_if_overridden(assets, &claimed_names, report, &spec);
continue;
}
if !seen_names.insert(spec.name.to_string()) {
continue;
}
to_inject.push(spec);
}
if to_inject.is_empty() {
break;
}
for spec in to_inject {
assets.push(serde_json::json!({
"name": spec.name,
"type": spec.asset_type,
"args": spec.args.clone(),
}));
report.record(spec.name, spec.asset_type, spec.args, "companion");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn inject(assets: &mut Vec<serde_json::Value>) {
let mut report = ExpandReport::default();
inject_companions(assets, &mut report);
}
fn type_norm(v: &serde_json::Value) -> String {
asset_type_norm(v)
}
#[test]
fn a_world_supplied_companion_is_recorded_as_an_override() {
let mut assets = vec![
serde_json::json!({"name":"gfx","type":"GraphicsConfig","args":{}}),
serde_json::json!({"name":"Window","type":"Window","args":{}}),
];
let mut report = ExpandReport::default();
inject_companions(&mut assets, &mut report);
assert_eq!(
assets.iter().filter(|v| type_norm(v) == "window").count(),
1,
"the world's own Window wins, and is not duplicated"
);
let shadow = report
.shadowed
.iter()
.find(|s| s.name == "Window")
.expect("the overridden companion is recorded");
assert_eq!(shadow.asset_type, "Window");
assert_eq!(shadow.generated_by, "companion");
}
#[test]
fn an_injected_companion_is_not_recorded_as_an_override() {
let mut assets = vec![serde_json::json!({"name":"gfx","type":"GraphicsConfig","args":{}})];
let mut report = ExpandReport::default();
inject_companions(&mut assets, &mut report);
assert!(
report.injected.iter().any(|i| i.name == "Window"),
"the Window companion was injected"
);
assert!(
report.shadowed.is_empty(),
"our own injection is not the user overriding it: {:?}",
report.shadowed
);
}
#[test]
fn a_differently_named_asset_of_the_same_type_is_not_an_override() {
let mut assets = vec![
serde_json::json!({"name":"gfx","type":"GraphicsConfig","args":{}}),
serde_json::json!({"name":"main_window","type":"Window","args":{}}),
];
let mut report = ExpandReport::default();
inject_companions(&mut assets, &mut report);
assert_eq!(
assets.iter().filter(|v| type_norm(v) == "window").count(),
1
);
assert!(
report.shadowed.is_empty(),
"no asset named Window exists to be the override: {:?}",
report.shadowed
);
}
#[test]
fn no_injection_without_trigger() {
let mut assets = vec![serde_json::json!({"name":"w","type":"Window","args":{}})];
inject(&mut assets);
assert!(!assets.iter().any(|v| type_norm(v) == "graphicsconfig"));
}
#[test]
fn text_injects_graphics_config() {
let mut assets =
vec![serde_json::json!({"name":"t","type":"TextLabel","args":{"content":"hi"}})];
inject(&mut assets);
assert!(assets.iter().any(|v| type_norm(v) == "graphicsconfig"));
}
#[test]
fn text_does_not_inject_duplicate_graphics_config() {
let mut assets = vec![
serde_json::json!({"name":"t","type":"TextLabel","args":{"content":"hi"}}),
serde_json::json!({"name":"gfx","type":"GraphicsConfig","args":{}}),
];
inject(&mut assets);
let gfx_count = assets
.iter()
.filter(|v| type_norm(v) == "graphicsconfig")
.count();
assert_eq!(gfx_count, 1);
}
#[test]
fn text_naming_no_font_injects_none() {
let mut assets =
vec![serde_json::json!({"name":"t","type":"TextLabel","args":{"content":"hi"}})];
inject(&mut assets);
assert!(!assets.iter().any(|v| type_norm(v) == "font"));
}
#[test]
fn a_font_less_label_is_left_exactly_as_authored() {
let authored = serde_json::json!({
"name": "t",
"type": "TextLabel",
"args": {"content": "hi", "x": 40.0, "y": 80.0}
});
let mut assets = vec![authored.clone()];
inject(&mut assets);
let label = assets.iter().find(|v| type_norm(v) == "textlabel").unwrap();
assert_eq!(label, &authored);
}
#[test]
fn a_declared_font_is_not_duplicated() {
let mut assets = vec![
serde_json::json!({"name":"t","type":"TextLabel","args":{"content":"hi"}}),
serde_json::json!({"name":"f","type":"Font","args":{"path":"my.ttf","size_px":20}}),
];
inject(&mut assets);
let font_count = assets.iter().filter(|v| type_norm(v) == "font").count();
assert_eq!(font_count, 1);
}
#[test]
fn text_does_not_override_explicit_font_on_label() {
let mut assets = vec![serde_json::json!({
"name": "t",
"type": "TextLabel",
"args": {"content": "hi", "font": "myfont"}
})];
inject(&mut assets);
let label = assets.iter().find(|v| type_norm(v) == "textlabel").unwrap();
assert_eq!(label["args"]["font"].as_str().unwrap(), "myfont");
}
#[test]
fn graphics_config_injects_window() {
let mut assets = vec![serde_json::json!({"name":"gfx","type":"GraphicsConfig","args":{}})];
inject(&mut assets);
assert!(assets.iter().any(|v| type_norm(v) == "window"));
}
#[test]
fn a_typeless_entry_is_skipped() {
let mut assets = vec![
serde_json::json!({"name":"junk","args":{}}),
serde_json::json!({"name":"gfx","type":"GraphicsConfig","args":{}}),
];
inject(&mut assets);
assert!(assets.iter().any(|v| type_norm(v) == "window"));
assert!(assets.iter().any(|v| v["name"] == "junk"));
}
#[test]
fn camera3d_injects_no_companions() {
let mut assets = vec![serde_json::json!({"name":"c","type":"Camera3D","args":{}})];
inject(&mut assets);
assert_eq!(assets.len(), 1);
assert!(type_norm(&assets[0]) == "camera3d");
}
}