"""
``--json field1,field2`` field projection.
The CLI's ``--json field1,field2`` flag (``plans/03 ยง9``) is a
post-processor over the JSON renderer's output: it keeps only the
named top-level fields. Unknown fields raise a typed
:class:`ApiError` so a typo at the call site is loud, not silent.
"""
from __future__ import annotations
from typing import Any, Dict, List
from animedex.models.common import ApiError
[docs]
def parse_field_string(spec: str) -> List[str]:
"""Split a comma-separated field spec into a list of field names.
Strips whitespace, drops empty entries.
:param spec: Comma-separated field spec
(e.g. ``"id, title, episodes"``).
:type spec: str
:return: List of field names; empty list when ``spec`` is
empty.
:rtype: list of str
"""
if not spec:
return []
return [token.strip() for token in spec.split(",") if token.strip()]
[docs]
def project_fields(payload: Dict[str, Any], fields: List[str]) -> Dict[str, Any]:
"""Return a dict containing only ``fields`` keys from ``payload``.
An empty ``fields`` list returns ``payload`` unchanged. An
unknown field raises :class:`ApiError` with
``reason="unknown-field"`` naming the offending field.
:param payload: A dict-shaped JSON-decoded payload.
:type payload: dict
:param fields: Top-level field names to keep.
:type fields: list of str
:return: The projected dict.
:rtype: dict
:raises ApiError: When ``fields`` names a key not present in
``payload``.
"""
if not fields:
return payload
out: Dict[str, Any] = {}
for field in fields:
if field not in payload:
raise ApiError(
f"unknown field: {field!r}",
reason="unknown-field",
)
out[field] = payload[field]
return out
[docs]
def selftest() -> bool:
"""Smoke-test field projection.
:return: ``True`` on success.
:rtype: bool
"""
assert parse_field_string("a, b ,c") == ["a", "b", "c"]
assert project_fields({"a": 1, "b": 2}, ["a"]) == {"a": 1}
return True