diff --git a/ftf_cli/utils.py b/ftf_cli/utils.py index 6fbd72b..41970d0 100644 --- a/ftf_cli/utils.py +++ b/ftf_cli/utils.py @@ -373,7 +373,7 @@ def check_conflicting_ui_properties(spec_obj, path="spec"): Recursively check for conflicting UI properties in spec fields. Validates that: - 1. patternProperties and x-ui-yaml-editor: true are not both present on the same field + 1. patternProperties with object type and x-ui-yaml-editor: true are not both present on the same field 2. x-ui-override-disable: true and x-ui-overrides-only: true are not both present on the same field Raises UsageError with clear error message if conflicts are found. @@ -383,17 +383,22 @@ def check_conflicting_ui_properties(spec_obj, path="spec"): for key, value in spec_obj.items(): if isinstance(value, dict): - # Check for patternProperties + x-ui-yaml-editor conflict + # Check for patternProperties + x-ui-yaml-editor conflict based on pattern type has_pattern_properties = "patternProperties" in value has_yaml_editor = value.get("x-ui-yaml-editor", False) if has_pattern_properties and has_yaml_editor: - raise click.UsageError( - f"Configuration conflict at {path}.{key}: " - f"Fields cannot have both 'patternProperties' and 'x-ui-yaml-editor: true'. " - f"These properties are mutually exclusive - use either patternProperties for " - f"dynamic key-value structures or x-ui-yaml-editor for free-form YAML editing." - ) + # Check the types of all patternProperties + pp = value["patternProperties"] + for pattern_key, pp_val in pp.items(): + pattern_type = pp_val.get("type") + if pattern_type == "object": + raise click.UsageError( + f"Configuration conflict at {path}.{key}: " + f"Fields with patternProperties of type 'object' cannot have 'x-ui-yaml-editor: true'. " + f"Use either patternProperties with object type for structured dynamic content " + f"or x-ui-yaml-editor for free-form YAML editing." + ) # Check for x-ui-override-disable + x-ui-overrides-only conflict has_override_disable = value.get("x-ui-override-disable", False) diff --git a/tests/test_utils_validation.py b/tests/test_utils_validation.py index 375ff02..ed0dfb0 100644 --- a/tests/test_utils_validation.py +++ b/tests/test_utils_validation.py @@ -62,14 +62,15 @@ def test_no_conflicting_properties_pass(): "field2": {"type": "string", "x-ui-yaml-editor": True}, "field3": {"type": "object", "patternProperties": {"^.*$": {"type": "object"}}}, "field4": {"type": "string", "x-ui-override-disable": True}, - "field5": {"type": "string", "x-ui-overrides-only": True} + "field5": {"type": "string", "x-ui-overrides-only": True}, + "field6": {"type": "object", "patternProperties": {"^.*$": {"type": "string"}}, "x-ui-yaml-editor": True} } # Should pass silently check_conflicting_ui_properties(spec) -def test_pattern_properties_with_yaml_editor_raises(): - """Test that patternProperties + x-ui-yaml-editor conflict is detected.""" +def test_pattern_properties_object_type_with_yaml_editor_raises(): + """Test that patternProperties with object type + x-ui-yaml-editor conflict is detected.""" spec = { "field": { "type": "object", @@ -80,9 +81,8 @@ def test_pattern_properties_with_yaml_editor_raises(): with pytest.raises(click.UsageError) as excinfo: check_conflicting_ui_properties(spec) assert "Configuration conflict at spec.field" in str(excinfo.value) - assert "patternProperties" in str(excinfo.value) + assert "patternProperties of type 'object'" in str(excinfo.value) assert "x-ui-yaml-editor: true" in str(excinfo.value) - assert "mutually exclusive" in str(excinfo.value) def test_override_disable_with_overrides_only_raises(): @@ -121,6 +121,19 @@ def test_nested_conflicting_properties_raises(): assert "Configuration conflict at spec.level1.level2.field" in str(excinfo.value) +def test_pattern_properties_string_type_with_yaml_editor_pass(): + """Test that patternProperties with string type + x-ui-yaml-editor is allowed.""" + spec = { + "field": { + "type": "object", + "patternProperties": {"^.*$": {"type": "string"}}, + "x-ui-yaml-editor": True + } + } + # Should pass silently + check_conflicting_ui_properties(spec) + + def test_pattern_properties_with_yaml_editor_false_pass(): """Test that patternProperties with x-ui-yaml-editor: false is allowed.""" spec = { @@ -196,3 +209,38 @@ def test_non_dict_values_ignored(): } # Should pass silently check_conflicting_ui_properties(spec) + + +def test_pattern_properties_mixed_types_with_yaml_editor(): + """Test behavior when patternProperties has mixed types (string and object).""" + spec = { + "field": { + "type": "object", + "patternProperties": { + "^string_.*$": {"type": "string"}, + "^object_.*$": {"type": "object"} + }, + "x-ui-yaml-editor": True + } + } + # Should raise error because one of the patternProperties is object type + with pytest.raises(click.UsageError) as excinfo: + check_conflicting_ui_properties(spec) + assert "Configuration conflict at spec.field" in str(excinfo.value) + assert "patternProperties of type 'object'" in str(excinfo.value) + + +def test_pattern_properties_only_string_types_with_yaml_editor_pass(): + """Test that multiple patternProperties all with string type + yaml-editor passes.""" + spec = { + "field": { + "type": "object", + "patternProperties": { + "^config_.*$": {"type": "string"}, + "^setting_.*$": {"type": "string"} + }, + "x-ui-yaml-editor": True + } + } + # Should pass silently + check_conflicting_ui_properties(spec)