diff --git a/schema.json b/schema.json index 4d56bead..e94dd684 100644 --- a/schema.json +++ b/schema.json @@ -184,7 +184,7 @@ ] }, "float_rule_placement": { - "description": "Determines the `Placement` to be used when spawning a window that matches a\n'floating_applications' rule (default: None)", + "description": "Determines the `Placement` to be used when spawning a window that matches a\n`floating_applications` rule (default: None)", "anyOf": [ { "$ref": "#/$defs/Placement" @@ -283,7 +283,7 @@ "deprecated": true }, "layered_applications": { - "description": "Identify applications that have the WS_EX_LAYERED extended window style", + "description": "Identify applications that have the `WS_EX_LAYERED` extended window style", "type": [ "array", "null" @@ -349,7 +349,7 @@ ] }, "object_name_change_applications": { - "description": "Identify applications that send EVENT_OBJECT_NAMECHANGE on launch (very rare)", + "description": "Identify applications that send `EVENT_OBJECT_NAMECHANGE` on launch (very rare)", "type": [ "array", "null" @@ -359,7 +359,7 @@ } }, "object_name_change_title_ignore_list": { - "description": "Do not process EVENT_OBJECT_NAMECHANGE events as Show events for identified applications matching these title regexes", + "description": "Do not process `EVENT_OBJECT_NAMECHANGE` events as Show events for identified applications matching these title regexes", "type": [ "array", "null" @@ -638,11 +638,11 @@ "AppSpecificConfigurationPath": { "anyOf": [ { - "description": "A single applications.json file", + "description": "A single `applications.json` file", "$ref": "#/$defs/PathBuf" }, { - "description": "Multiple applications.json files", + "description": "Multiple `applications.json` files", "type": "array", "items": { "$ref": "#/$defs/PathBuf" @@ -1290,13 +1290,13 @@ "HidingBehaviour": { "oneOf": [ { - "description": "END OF LIFE FEATURE: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)", + "description": "END OF LIFE FEATURE: Use the `SW_HIDE` flag to hide windows when switching workspaces (has issues with Electron apps)", "type": "string", "const": "Hide", "deprecated": true }, { - "description": "Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)", + "description": "Use the `SW_MINIMIZE` flag to hide windows when switching workspaces (has issues with frequent workspace switching)", "type": "string", "const": "Minimize" }, diff --git a/src/prepare.rs b/src/prepare.rs index 3f5c8008..3c944afe 100644 --- a/src/prepare.rs +++ b/src/prepare.rs @@ -30,25 +30,28 @@ pub fn prepare_for_template(value: &Value) -> Value { prepared } -/// Add display names to types by stripping numeric suffixes +/// Add display names to wrapper types by stripping numeric suffixes /// -/// For types like `PerAnimationPrefixConfig2` or `DisplayFormat2`, this adds a -/// `_display_name` field with the base name (e.g., `PerAnimationPrefixConfig`). -/// This applies to all types with trailing digits, not just wrapper types. +/// For wrapper types like `PerAnimationPrefixConfig2` or `PerAnimationPrefixConfig3`, +/// this adds a `_display_name` field with the base name (e.g., `PerAnimationPrefixConfig`). +/// This only applies to types marked with `_wrapper_type: true`, not all types ending in digits. +/// Legitimate type names like `Base16` or `Sha256` are left unchanged. fn add_wrapper_display_names(schema: &mut Value) { if let Some(defs) = schema.get_mut("$defs").and_then(|v| v.as_object_mut()) { for (name, def) in defs.iter_mut() { - // Strip trailing digits from ALL type names (not just wrapper types) - let display_name = name.trim_end_matches(|c: char| c.is_ascii_digit()); + // Only strip trailing digits from wrapper types (not all types) + if def.get("_wrapper_type") == Some(&Value::Bool(true)) { + let display_name = name.trim_end_matches(|c: char| c.is_ascii_digit()); - // Only add _display_name if it differs from the original name - if display_name != name - && let Some(obj) = def.as_object_mut() - { - obj.insert( - "_display_name".to_string(), - Value::String(display_name.to_string()), - ); + // Only add _display_name if it differs from the original name + if display_name != name + && let Some(obj) = def.as_object_mut() + { + obj.insert( + "_display_name".to_string(), + Value::String(display_name.to_string()), + ); + } } } } @@ -2307,4 +2310,134 @@ mod tests { "Object with properties (not additionalProperties) should NOT be wrapper" ); } + + #[test] + fn test_type_names_ending_in_digits_not_stripped() { + // Legitimate type names ending in digits should NOT have _display_name added + let schema = json!({ + "$defs": { + "Base16": { "type": "string" }, + "Sha256": { "type": "string" }, + "Level3": { "type": "object" } + } + }); + + let prepared = prepare_for_template(&schema); + let defs = prepared.get("$defs").unwrap().as_object().unwrap(); + + // Type names that legitimately end in digits should NOT have _display_name added + assert!( + defs.get("Base16").unwrap().get("_display_name").is_none(), + "Base16 should not have _display_name" + ); + assert!( + defs.get("Sha256").unwrap().get("_display_name").is_none(), + "Sha256 should not have _display_name" + ); + assert!( + defs.get("Level3").unwrap().get("_display_name").is_none(), + "Level3 should not have _display_name" + ); + } + + #[test] + fn test_wrapper_types_get_display_name_stripped() { + // Real-world pattern from komorebi: PerAnimationPrefixConfig generates + // multiple $defs entries for different type parameters: + // - PerAnimationPrefixConfig (bool) + // - PerAnimationPrefixConfig2 (uint64) + // - PerAnimationPrefixConfig3 (AnimationStyle ref) + // All should display as "PerAnimationPrefixConfig" + let schema = json!({ + "$defs": { + "AnimationStyle": { "type": "string" }, + "PerAnimationPrefixConfig": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { "type": "boolean" } + }, + { "type": "boolean" } + ] + }, + "PerAnimationPrefixConfig2": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + ] + }, + "PerAnimationPrefixConfig3": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { "$ref": "#/$defs/AnimationStyle" } + }, + { "$ref": "#/$defs/AnimationStyle" } + ] + } + } + }); + + let prepared = prepare_for_template(&schema); + let defs = prepared.get("$defs").unwrap().as_object().unwrap(); + + // All three should be detected as wrapper types + assert_eq!( + defs.get("PerAnimationPrefixConfig") + .unwrap() + .get("_wrapper_type"), + Some(&json!(true)), + "PerAnimationPrefixConfig should be detected as wrapper type" + ); + assert_eq!( + defs.get("PerAnimationPrefixConfig2") + .unwrap() + .get("_wrapper_type"), + Some(&json!(true)), + "PerAnimationPrefixConfig2 should be detected as wrapper type" + ); + assert_eq!( + defs.get("PerAnimationPrefixConfig3") + .unwrap() + .get("_wrapper_type"), + Some(&json!(true)), + "PerAnimationPrefixConfig3 should be detected as wrapper type" + ); + + // The base type has no digits, so no _display_name needed + assert!( + defs.get("PerAnimationPrefixConfig") + .unwrap() + .get("_display_name") + .is_none(), + "Base wrapper type should not have _display_name" + ); + + // Numbered variants should have display name with digits stripped + assert_eq!( + defs.get("PerAnimationPrefixConfig2") + .unwrap() + .get("_display_name"), + Some(&json!("PerAnimationPrefixConfig")), + "PerAnimationPrefixConfig2 should have digits stripped" + ); + assert_eq!( + defs.get("PerAnimationPrefixConfig3") + .unwrap() + .get("_display_name"), + Some(&json!("PerAnimationPrefixConfig")), + "PerAnimationPrefixConfig3 should have digits stripped" + ); + } }