Compare commits

...

7 Commits

Author SHA1 Message Date
Paulus Schoutsen da89290c07 Simplify more 2025-11-26 23:22:34 -05:00
Claude b6cb993b07 Refactor: create reusable NON_EMPTY_STRING validator
Create a module-level NON_EMPTY_STRING constant to avoid repeatedly
initializing the same voluptuous validator. This improves performance
and code maintainability by reusing a single validator instance across
all selectors that require non-empty string validation.

Replaced all instances of vol.All(cv.string, vol.Length(min=1)) with
the new NON_EMPTY_STRING constant.
2025-11-16 02:50:36 +00:00
Claude e52b71a9a4 Add validation to string-based selectors
Add non-empty string validation to the following selectors that
previously only validated type but accepted empty strings:
- AttributeSelector
- ConfigEntrySelector
- ConversationAgentSelector
- IconSelector
- ThemeSelector

All now properly validate that input strings are non-empty.
2025-11-16 02:43:39 +00:00
Claude d7d3fd89db Add validation to AssistPipelineSelector
Previously, AssistPipelineSelector only validated type but accepted
empty strings. This change adds proper validation to ensure pipeline
IDs are non-empty strings.

Also updated tests to validate empty strings are rejected and that
values are properly coerced to strings.
2025-11-16 02:42:18 +00:00
Claude 647d7617aa Add validation to AreaSelector
Previously, AreaSelector only validated type but accepted empty strings.
This change adds proper validation to ensure area IDs are non-empty
strings for both single and multiple selection modes.

Also updated tests to validate empty strings are rejected and added
a converter function to handle both single values and lists properly.
2025-11-16 02:40:30 +00:00
Claude 27614e3759 Add validation to AddonSelector
Previously, AddonSelector only validated type but accepted empty strings.
This change adds proper validation to ensure addon names are non-empty
strings.

Also updated tests to validate empty strings are rejected and that
numbers are coerced to strings as expected.
2025-11-16 02:38:42 +00:00
Claude abbb247f8a Add validation to ActionSelector
Previously, ActionSelector did not validate incoming data and simply
returned it as-is. This change adds proper validation using the
SCRIPT_SCHEMA to ensure action sequences are well-formed.

Also updated tests to properly validate both valid action sequences
and reject invalid inputs.
2025-11-16 02:36:05 +00:00
2 changed files with 61 additions and 23 deletions
+13 -10
View File
@@ -22,6 +22,9 @@ from . import config_validation as cv
SELECTORS: decorator.Registry[str, type[Selector]] = decorator.Registry()
# Reusable validator for non-empty strings
NON_EMPTY_STRING_SCHEMA = vol.All(cv.string, vol.Length(min=1))
def _get_selector_type_and_class(config: Any) -> tuple[str, type[Selector]]:
"""Get selector type and class."""
@@ -236,7 +239,7 @@ class ActionSelector(Selector[ActionSelectorConfig]):
def __call__(self, data: Any) -> Any:
"""Validate the passed selection."""
return data
return vol.Schema(cv.SCRIPT_SCHEMA)(data)
class AddonSelectorConfig(BaseSelectorConfig, total=False):
@@ -265,7 +268,7 @@ class AddonSelector(Selector[AddonSelectorConfig]):
def __call__(self, data: Any) -> str:
"""Validate the passed selection."""
addon: str = vol.Schema(str)(data)
addon: str = NON_EMPTY_STRING_SCHEMA(data)
return addon
@@ -304,11 +307,11 @@ class AreaSelector(Selector[AreaSelectorConfig]):
def __call__(self, data: Any) -> str | list[str]:
"""Validate the passed selection."""
if not self.config["multiple"]:
area_id: str = vol.Schema(str)(data)
area_id: str = NON_EMPTY_STRING_SCHEMA(data)
return area_id
if not isinstance(data, list):
raise vol.Invalid("Value should be a list")
return [vol.Schema(str)(val) for val in data]
return [NON_EMPTY_STRING_SCHEMA(val) for val in data]
class AssistPipelineSelectorConfig(BaseSelectorConfig, total=False):
@@ -329,7 +332,7 @@ class AssistPipelineSelector(Selector[AssistPipelineSelectorConfig]):
def __call__(self, data: Any) -> str:
"""Validate the passed selection."""
pipeline: str = vol.Schema(str)(data)
pipeline: str = NON_EMPTY_STRING_SCHEMA(data)
return pipeline
@@ -361,7 +364,7 @@ class AttributeSelector(Selector[AttributeSelectorConfig]):
def __call__(self, data: Any) -> str:
"""Validate the passed selection."""
attribute: str = vol.Schema(str)(data)
attribute: str = NON_EMPTY_STRING_SCHEMA(data)
return attribute
@@ -536,7 +539,7 @@ class ConfigEntrySelector(Selector[ConfigEntrySelectorConfig]):
def __call__(self, data: Any) -> str:
"""Validate the passed selection."""
config: str = vol.Schema(str)(data)
config: str = NON_EMPTY_STRING_SCHEMA(data)
return config
@@ -596,7 +599,7 @@ class ConversationAgentSelector(Selector[ConversationAgentSelectorConfig]):
def __call__(self, data: Any) -> str:
"""Validate the passed selection."""
agent: str = vol.Schema(str)(data)
agent: str = NON_EMPTY_STRING_SCHEMA(data)
return agent
@@ -922,7 +925,7 @@ class IconSelector(Selector[IconSelectorConfig]):
def __call__(self, data: Any) -> str:
"""Validate the passed selection."""
icon: str = vol.Schema(str)(data)
icon: str = NON_EMPTY_STRING_SCHEMA(data)
return icon
@@ -1595,7 +1598,7 @@ class ThemeSelector(Selector[ThemeSelectorConfig]):
def __call__(self, data: Any) -> str:
"""Validate the passed selection."""
theme: str = vol.Schema(str)(data)
theme: str = NON_EMPTY_STRING_SCHEMA(data)
return theme
+48 -13
View File
@@ -385,13 +385,20 @@ def test_entity_selector_schema_error(schema) -> None:
(
{"multiple": True},
((["abc123", "def456"],)),
(None, "abc123", ["abc123", None]),
(None, "abc123", ["abc123", None], ["abc123", ""]),
),
],
)
def test_area_selector_schema(schema, valid_selections, invalid_selections) -> None:
"""Test area selector."""
_test_selector("area", schema, valid_selections, invalid_selections)
def converter(val):
"""Convert to str or list[str]."""
if isinstance(val, list):
return [str(v) for v in val]
return str(val)
_test_selector("area", schema, valid_selections, invalid_selections, converter)
@pytest.mark.parametrize(
@@ -399,8 +406,8 @@ def test_area_selector_schema(schema, valid_selections, invalid_selections) -> N
[
(
{},
("23ouih2iu23ou2", "2j4hp3uy4p87wyrpiuhk34"),
(None, True, 1),
("23ouih2iu23ou2", "2j4hp3uy4p87wyrpiuhk34", 123, True),
(None, "", [], {}),
),
],
)
@@ -408,7 +415,7 @@ def test_assist_pipeline_selector_schema(
schema, valid_selections, invalid_selections
) -> None:
"""Test assist pipeline selector."""
_test_selector("assist_pipeline", schema, valid_selections, invalid_selections)
_test_selector("assist_pipeline", schema, valid_selections, invalid_selections, str)
@pytest.mark.parametrize(
@@ -480,11 +487,15 @@ def test_number_selector_schema_error(schema) -> None:
@pytest.mark.parametrize(
("schema", "valid_selections", "invalid_selections"),
[({}, ("abc123",), (None,))],
[
({}, ("abc123", "local_addon", 123), (None, "", [], {})),
({"name": "test"}, ("abc123",), (None, "", [])),
({"slug": "test_slug"}, ("addon_123",), (None, "", {})),
],
)
def test_addon_selector_schema(schema, valid_selections, invalid_selections) -> None:
"""Test add-on selector."""
_test_selector("addon", schema, valid_selections, invalid_selections)
_test_selector("addon", schema, valid_selections, invalid_selections, str)
@pytest.mark.parametrize(
@@ -643,13 +654,37 @@ def test_target_selector_schema(schema, valid_selections, invalid_selections) ->
_test_selector("target", schema, valid_selections, invalid_selections)
@pytest.mark.parametrize(
("schema", "valid_selections", "invalid_selections"),
[({}, ("abc123",), ())],
)
def test_action_selector_schema(schema, valid_selections, invalid_selections) -> None:
def test_action_selector_schema() -> None:
"""Test action sequence selector."""
_test_selector("action", schema, valid_selections, invalid_selections)
config = {"action": {}}
selector.validate_selector(config)
selector_instance = selector.selector(config)
# Valid action sequences
valid_selections = [
[{"action": "test.automation"}],
[{"action": "test.automation", "data": {"hello": "world"}}],
[{"service": "test.service"}],
[{"service": "test.service", "data": {"key": "value"}}],
]
vol_schema = vol.Schema({"selection": selector_instance})
for selection in valid_selections:
vol_schema({"selection": selection})
# Invalid action sequences
invalid_selections = [
"abc123",
123,
{"invalid": "action"},
[{"invalid": "action"}],
[123],
["string"],
]
for selection in invalid_selections:
with pytest.raises(vol.Invalid):
vol_schema({"selection": selection})
@pytest.mark.parametrize(