Add typed task input forms
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"""Validation for task-input manifest values."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Mapping, Sequence
|
||||
from datetime import date, datetime
|
||||
|
||||
from .discovery import InputField
|
||||
|
||||
|
||||
def validate_inputs(
|
||||
fields: Sequence[InputField],
|
||||
values: Mapping[str, list[str]],
|
||||
) -> tuple[dict[str, str | int | list[str]], dict[str, str]]:
|
||||
result: dict[str, str | int | list[str]] = {}
|
||||
errors: dict[str, str] = {}
|
||||
for field in fields:
|
||||
raw_values = values.get(field.name, [])
|
||||
value, error = validate_field(field, raw_values)
|
||||
if error:
|
||||
errors[field.name] = error
|
||||
elif value is not None:
|
||||
result[field.name] = value
|
||||
return result, errors
|
||||
|
||||
|
||||
def validate_field(
|
||||
field: InputField,
|
||||
raw_values: list[str],
|
||||
) -> tuple[str | int | list[str] | None, str | None]:
|
||||
if field.type == "multi_choice":
|
||||
return validate_multi_choice(field, raw_values)
|
||||
if len(raw_values) > 1:
|
||||
return None, "Only one value is allowed."
|
||||
raw = raw_values[0] if raw_values else ""
|
||||
if not raw:
|
||||
if field.default is not None:
|
||||
return field.default, None
|
||||
if field.required:
|
||||
return None, "This field is required."
|
||||
return None, None
|
||||
if field.type == "integer":
|
||||
try:
|
||||
value = int(raw)
|
||||
except ValueError:
|
||||
return None, "Enter a whole number."
|
||||
if field.minimum is not None and value < field.minimum:
|
||||
return None, f"Enter a value of at least {field.minimum}."
|
||||
if field.maximum is not None and value > field.maximum:
|
||||
return None, f"Enter a value no greater than {field.maximum}."
|
||||
if field.step is not None and (value - (field.minimum or 0)) % field.step:
|
||||
return None, f"Enter a value in increments of {field.step}."
|
||||
return value, None
|
||||
if field.type == "date":
|
||||
try:
|
||||
date.fromisoformat(raw)
|
||||
except ValueError:
|
||||
return None, "Enter a valid date."
|
||||
elif field.type == "datetime":
|
||||
try:
|
||||
datetime.fromisoformat(raw)
|
||||
except ValueError:
|
||||
return None, "Enter a valid date and time."
|
||||
elif field.type == "choice" and raw not in {option.value for option in field.options}:
|
||||
return None, "Choose one of the available options."
|
||||
elif field.type == "text" and field.pattern and not re.fullmatch(field.pattern, raw):
|
||||
return None, "Enter a value in the required format."
|
||||
return raw, None
|
||||
|
||||
|
||||
def validate_multi_choice(
|
||||
field: InputField,
|
||||
raw_values: list[str],
|
||||
) -> tuple[list[str] | None, str | None]:
|
||||
if not raw_values:
|
||||
if field.default is not None:
|
||||
return list(field.default), None
|
||||
if field.required:
|
||||
return None, "Choose at least one option."
|
||||
return None, None
|
||||
allowed = {option.value for option in field.options}
|
||||
if any(value not in allowed for value in raw_values):
|
||||
return None, "Choose only from the available options."
|
||||
if len(raw_values) != len(set(raw_values)):
|
||||
return None, "Each option can be selected only once."
|
||||
return raw_values, None
|
||||
Reference in New Issue
Block a user