88 lines
3.1 KiB
Python
88 lines
3.1 KiB
Python
"""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
|