"""Adapters for WDK parameter specifications."""
from dataclasses import dataclass
from veupath_chatbot.platform.types import JSONArray, JSONObject, JSONValue
[docs]
@dataclass(frozen=True)
class ParamSpecNormalized:
"""Canonical representation of a WDK parameter spec."""
name: str
param_type: str
allow_empty_value: bool = False
min_selected_count: int | None = None
max_selected_count: int | None = None
vocabulary: JSONObject | JSONArray | None = None
count_only_leaves: bool = False
is_number: bool = False
min_value: float | None = None
max_value: float | None = None
increment: float | None = None
max_length: int | None = None
display_type: str = ""
is_visible: bool = True
group: str = ""
dependent_params: tuple[str, ...] = ()
help: str | None = None
[docs]
def unwrap_search_data(details: JSONObject | None) -> JSONObject | None:
"""Normalize WDK/discovery payload shape to the dict that contains parameters.
:param details: Search details from WDK/discovery.
:returns: Search data dict or None.
"""
if not isinstance(details, dict):
return None
search_data_raw = details.get("searchData")
if isinstance(search_data_raw, dict):
return search_data_raw
return details
def _safe_float(raw: JSONValue) -> float | None:
"""Convert a raw JSON value to float, returning None on failure."""
if isinstance(raw, (int, float)) and not isinstance(raw, bool):
return float(raw)
if isinstance(raw, str):
try:
return float(raw)
except ValueError:
return None
return None
def _safe_int(raw: JSONValue) -> int | None:
"""Convert a raw JSON value to int, returning None on failure."""
if isinstance(raw, int) and not isinstance(raw, bool):
return raw
if isinstance(raw, str):
try:
return int(raw)
except ValueError:
return None
return None
[docs]
def adapt_param_specs(payload: JSONObject) -> dict[str, ParamSpecNormalized]:
specs = extract_param_specs(payload or {})
normalized: dict[str, ParamSpecNormalized] = {}
for spec in specs:
if not isinstance(spec, dict):
continue
name_raw = spec.get("name")
if not isinstance(name_raw, str):
continue
name = name_raw
type_raw = spec.get("type") or spec.get("paramType")
param_type = str(type_raw) if isinstance(type_raw, str) else ""
allow_empty_raw = spec.get("allowEmptyValue") or spec.get("allowEmpty")
allow_empty_value = bool(allow_empty_raw)
min_selected = spec.get("minSelectedCount")
max_selected = spec.get("maxSelectedCount")
if isinstance(max_selected, int) and max_selected < 0:
max_selected = None
vocabulary_raw = spec.get("vocabulary")
vocabulary: JSONObject | JSONArray | None = None
if isinstance(vocabulary_raw, (dict, list)):
vocabulary = vocabulary_raw
# WDK StringParam can be numeric (isNumber=true on type="string")
is_number_raw = spec.get("isNumber")
is_number = bool(is_number_raw) if isinstance(is_number_raw, bool) else False
# Numeric constraints: min/max/increment
min_value = _safe_float(spec.get("min"))
max_value = _safe_float(spec.get("max"))
increment_raw = spec.get("increment")
increment = _safe_float(
increment_raw if increment_raw is not None else spec.get("step")
)
# String constraints: maxLength / length (0 means "no limit" in WDK)
max_length_raw = spec.get("maxLength")
max_length = _safe_int(
max_length_raw if max_length_raw is not None else spec.get("length")
)
if max_length is not None and max_length <= 0:
max_length = None
# WDK UI metadata
display_type = str(spec.get("displayType") or "")
is_visible_raw = spec.get("isVisible")
is_visible = bool(is_visible_raw) if isinstance(is_visible_raw, bool) else True
group = str(spec.get("group") or "")
dependent_params_raw = spec.get("dependentParams")
dependent_params = (
tuple(str(p) for p in dependent_params_raw)
if isinstance(dependent_params_raw, list)
else ()
)
help_text = spec.get("help")
help_str = str(help_text) if isinstance(help_text, str) else None
normalized[name] = ParamSpecNormalized(
name=name,
param_type=param_type,
allow_empty_value=allow_empty_value,
min_selected_count=min_selected if isinstance(min_selected, int) else None,
max_selected_count=max_selected if isinstance(max_selected, int) else None,
vocabulary=vocabulary,
count_only_leaves=bool(spec.get("countOnlyLeaves")),
is_number=is_number,
min_value=min_value,
max_value=max_value,
increment=increment,
max_length=max_length,
display_type=display_type,
is_visible=is_visible,
group=group,
dependent_params=dependent_params,
help=help_str,
)
return normalized
[docs]
def find_missing_required_params(
param_specs: JSONArray,
parameters: JSONObject,
) -> list[str]:
"""Find required parameters that are missing or empty in the given values.
Shared by ``validation.py`` and ``param_validation.py`` to keep the
required-check logic in a single place.
:param param_specs: Raw WDK parameter spec dicts.
:param parameters: Parameter values to check.
:returns: List of missing required parameter names.
"""
required_specs: list[JSONObject] = []
for p in param_specs:
if not isinstance(p, dict):
continue
allow_empty_raw = p.get("allowEmptyValue")
allow_empty = (
bool(allow_empty_raw) if isinstance(allow_empty_raw, bool) else True
)
min_selected_raw = p.get("minSelectedCount")
min_selected = min_selected_raw if isinstance(min_selected_raw, int) else 0
if not allow_empty or min_selected >= 1:
required_specs.append(p)
missing: list[str] = []
for spec in required_specs:
if not isinstance(spec, dict):
continue
name_raw = spec.get("name")
name = name_raw if isinstance(name_raw, str) else None
if not name:
continue
if name not in parameters:
missing.append(name)
continue
value = parameters.get(name)
type_raw = spec.get("type")
param_type = (type_raw if isinstance(type_raw, str) else "").lower()
if param_type == "multi-pick-vocabulary":
if value in (None, "", "[]") or (
isinstance(value, list) and len(value) == 0
):
missing.append(name)
continue
if value in (None, "", [], {}):
missing.append(name)
return missing