use crate::common::{FlowError, FlowResult};
use schemars::schema_for;
use super::types::{FlowConfig, WindowRulesConfig};
pub fn generate_config_schema() -> FlowResult<String> {
let schema = schema_for!(FlowConfig);
serde_json::to_string_pretty(&schema)
.map_err(|e| FlowError::Config(format!("schema generation failed: {e}")))
}
pub fn generate_rules_schema() -> FlowResult<String> {
let schema = schema_for!(WindowRulesConfig);
serde_json::to_string_pretty(&schema)
.map_err(|e| FlowError::Config(format!("rules schema generation failed: {e}")))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn schema_is_valid_json() {
let json = generate_config_schema().expect("schema gen");
let parsed: serde_json::Value =
serde_json::from_str(&json).expect("schema should be valid JSON");
assert!(parsed.is_object());
let obj = parsed.as_object().expect("schema is an object");
assert!(obj.contains_key("properties"));
}
#[test]
fn schema_references_flow_config() {
let json = generate_config_schema().expect("schema gen");
assert!(json.contains("column_width"));
assert!(json.contains("padding"));
assert!(json.contains("animation"));
}
#[test]
fn schema_references_all_top_level_properties() {
let json = generate_config_schema().expect("schema gen");
let parsed: serde_json::Value = serde_json::from_str(&json).expect("schema is valid JSON");
let props = parsed
.pointer("/properties")
.expect("has /properties")
.as_object()
.expect("properties is object");
let expected_keys = [
"column_width",
"min_column_width_px",
"padding",
"animation",
"minimize_restore",
];
for key in &expected_keys {
assert!(
props.contains_key(*key),
"schema missing top-level property: {key}"
);
}
}
#[test]
fn schema_padding_has_window_up_down() {
let json = generate_config_schema().expect("schema gen");
let parsed: serde_json::Value = serde_json::from_str(&json).expect("schema is valid JSON");
let padding_schema = parsed
.pointer("/properties/padding")
.expect("has /properties/padding");
let ref_path = padding_schema
.get("$ref")
.and_then(|v| v.as_str())
.expect("padding has $ref");
let ref_path = ref_path.trim_start_matches("#/");
let padding_props = parsed
.pointer(&format!("/{ref_path}/properties"))
.unwrap_or_else(|| panic!("resolved ref {ref_path} has properties"));
let obj = padding_props
.as_object()
.expect("padding properties is object");
assert!(
obj.contains_key("window_gap"),
"padding missing 'window_gap'"
);
assert!(obj.contains_key("up"), "padding missing 'up'");
assert!(obj.contains_key("down"), "padding missing 'down'");
}
#[test]
fn schema_animation_has_enabled_duration_easing() {
let json = generate_config_schema().expect("schema gen");
let parsed: serde_json::Value = serde_json::from_str(&json).expect("schema is valid JSON");
let anim_schema = parsed
.pointer("/properties/animation")
.expect("has /properties/animation");
let ref_path = anim_schema
.get("$ref")
.and_then(|v| v.as_str())
.expect("animation has $ref");
let ref_path = ref_path.trim_start_matches("#/");
let anim_props = parsed
.pointer(&format!("/{ref_path}/properties"))
.unwrap_or_else(|| panic!("resolved ref {ref_path} has properties"));
let obj = anim_props
.as_object()
.expect("animation properties is object");
assert!(obj.contains_key("enabled"));
assert!(obj.contains_key("duration_ms"));
assert!(obj.contains_key("easing"));
let easing_schema = obj
.get("easing")
.expect("easing property exists")
.as_object()
.expect("easing property is an object");
let easing_ref = easing_schema
.get("$ref")
.and_then(|v| v.as_str())
.expect("easing has $ref");
let easing_ref_path = easing_ref.trim_start_matches("#/");
let easing_def = parsed
.pointer(&format!("/{easing_ref_path}"))
.unwrap_or_else(|| panic!("definition {easing_ref_path} exists"))
.as_object()
.unwrap_or_else(|| panic!("definition {easing_ref_path} is object"));
assert!(
easing_def.contains_key("oneOf"),
"easing schema should have 'oneOf' for enum variants; \
got keys: {:?}",
easing_def.keys().collect::<Vec<_>>()
);
let one_of = easing_def
.get("oneOf")
.expect("oneOf")
.as_array()
.expect("oneOf is array");
let mut enum_strings: Vec<&str> = Vec::new();
for entry in one_of {
let entry_obj = entry.as_object().expect("oneOf entry is object");
if let Some(s) = entry_obj.get("const").and_then(|v| v.as_str()) {
enum_strings.push(s);
}
}
assert!(
enum_strings.contains(&"linear"),
"enum should contain 'linear'"
);
assert!(
enum_strings.contains(&"ease-out-expo"),
"enum should contain 'ease-out-expo'"
);
assert!(
enum_strings.contains(&"ease-in-out-bounce"),
"enum should contain 'ease-in-out-bounce'"
);
assert_eq!(
enum_strings.len(),
31,
"enum should have exactly 31 variants, got: {enum_strings:?}"
);
}
#[test]
fn schema_color_fields_are_inlined_not_refd() {
let json = generate_config_schema().expect("schema gen");
let parsed: serde_json::Value = serde_json::from_str(&json).expect("schema is valid JSON");
if let Some(defs) = parsed.pointer("/$defs").and_then(|v| v.as_object()) {
assert!(
!defs.contains_key("Color"),
"Color should be inlined (inline_schema=true), but #/$defs/Color exists"
);
}
let borders_ref = parsed
.pointer("/properties/borders")
.and_then(|v| v.get("$ref"))
.and_then(|v| v.as_str())
.expect("borders has $ref");
let borders_path = borders_ref.trim_start_matches("#/");
let focused = parsed
.pointer(&format!("/{borders_path}/properties/focused_color"))
.expect("focused_color schema exists");
let focused_obj = focused.as_object().expect("focused_color is an object");
assert!(
!focused_obj.contains_key("$ref"),
"inline_schema=true must not emit a $ref for Color fields"
);
assert_eq!(
focused_obj.get("type").and_then(|v| v.as_str()),
Some("string"),
"inlined Color field should be type:string, got: {focused:?}"
);
assert_eq!(
focused_obj.get("pattern").and_then(|v| v.as_str()),
Some("^#[0-9A-Fa-f]{6}$")
);
assert_eq!(
focused_obj.get("format").and_then(|v| v.as_str()),
Some("hex-color")
);
}
#[test]
fn all_borders_color_fields_are_inlined() {
let json = generate_config_schema().expect("schema gen");
let parsed: serde_json::Value = serde_json::from_str(&json).expect("schema is valid JSON");
let borders_ref = parsed
.pointer("/properties/borders")
.and_then(|v| v.get("$ref"))
.and_then(|v| v.as_str())
.expect("borders has $ref");
let borders_path = borders_ref.trim_start_matches("#/");
let borders_props = parsed
.pointer(&format!("/{borders_path}/properties"))
.and_then(|v| v.as_object())
.expect("borders struct has properties");
for field in ["focused_color", "unfocused_color", "floating_color"] {
let field_schema = borders_props
.get(field)
.unwrap_or_else(|| panic!("borders has {field}"));
let obj = field_schema
.as_object()
.unwrap_or_else(|| panic!("{field} schema is an object"));
assert!(
!obj.contains_key("$ref"),
"{field} should be inlined (inline_schema=true), got: {field_schema}"
);
assert_eq!(
obj.get("type").and_then(|v| v.as_str()),
Some("string"),
"{field} should be type:string, got: {field_schema}"
);
assert_eq!(
obj.get("pattern").and_then(|v| v.as_str()),
Some("^#[0-9A-Fa-f]{6}$"),
"{field} should carry the hex pattern, got: {field_schema}"
);
assert_eq!(
obj.get("format").and_then(|v| v.as_str()),
Some("hex-color"),
"{field} should carry the hex-color format, got: {field_schema}"
);
}
}
#[test]
fn schema_meta_schema_is_draft_2020_12() {
let json = generate_config_schema().expect("schema gen");
let parsed: serde_json::Value = serde_json::from_str(&json).expect("schema is valid JSON");
let meta = parsed
.get("$schema")
.and_then(|v| v.as_str())
.expect("schema has $schema");
assert!(
meta.contains("2020-12"),
"expected Draft 2020-12 $schema URI, got: {meta}"
);
}
#[test]
fn rules_schema_is_valid_json() {
let json = generate_rules_schema().expect("rules schema gen");
let parsed: serde_json::Value =
serde_json::from_str(&json).expect("schema should be valid JSON");
assert!(parsed.is_object());
let obj = parsed.as_object().expect("schema is an object");
assert!(obj.contains_key("properties"));
}
#[test]
fn rules_schema_has_default_action_and_rules() {
let json = generate_rules_schema().expect("rules schema gen");
let parsed: serde_json::Value = serde_json::from_str(&json).expect("schema is valid JSON");
let props = parsed
.pointer("/properties")
.expect("has /properties")
.as_object()
.expect("properties is object");
assert!(props.contains_key("default_action"));
assert!(props.contains_key("rules"));
}
#[test]
fn rules_schema_rules_is_array() {
let json = generate_rules_schema().expect("rules schema gen");
let parsed: serde_json::Value = serde_json::from_str(&json).expect("schema is valid JSON");
let rules_type = parsed
.pointer("/properties/rules")
.and_then(|v| v.get("type"))
.expect("rules has a type");
assert_eq!(rules_type.as_str(), Some("array"));
}
#[test]
fn rules_schema_match_rule_has_regex_fields() {
let json = generate_rules_schema().expect("rules schema gen");
let parsed: serde_json::Value = serde_json::from_str(&json).expect("schema is valid JSON");
let match_ref = parsed
.pointer("/$defs/MatchRule/properties")
.expect("has MatchRule definition with properties");
let obj = match_ref
.as_object()
.expect("MatchRule properties is object");
assert!(obj.contains_key("exe"));
assert!(obj.contains_key("exe_regex"));
assert!(obj.contains_key("title"));
assert!(obj.contains_key("title_contains"));
assert!(obj.contains_key("title_regex"));
assert!(obj.contains_key("class"));
assert!(obj.contains_key("class_regex"));
assert!(obj.contains_key("process_path"));
assert!(obj.contains_key("process_path_regex"));
}
}