Coverage for src/lilbee/core/config/schema.py: 100%
31 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-28 17:20 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-28 17:20 +0000
1"""Per-field type and value-set metadata, read off the JSON schema pydantic builds."""
3from __future__ import annotations
5from functools import cache
6from typing import Any
8from .model import Config
10# Where pydantic puts the named sub-schema a field's type resolves to.
11_DEFS_PREFIX = "#/$defs/"
13# JSON schema type name -> the short name every lilbee surface reports.
14_WIRE_TYPE_NAMES: dict[str, str] = {
15 "boolean": "bool",
16 "integer": "int",
17 "number": "float",
18 "string": "str",
19 "array": "list",
20 "object": "dict",
21 "null": "null",
22}
25@cache
26def _properties() -> dict[str, dict[str, Any]]:
27 """Each Config field's JSON schema, keyed by field name."""
28 return dict(Config.model_json_schema()["properties"])
31@cache
32def _definitions() -> dict[str, dict[str, Any]]:
33 """The named sub-schemas a field's ``$ref`` points at, one per enum type."""
34 return dict(Config.model_json_schema().get("$defs", {}))
37def _resolved(schema: dict[str, Any]) -> dict[str, Any]:
38 """The definition a ``$ref`` names, or the schema itself when it holds no ref."""
39 ref = schema.get("$ref")
40 if ref is None:
41 return schema
42 return _definitions()[ref.removeprefix(_DEFS_PREFIX)]
45def _type_name(schema: dict[str, Any]) -> str:
46 branches = schema.get("anyOf")
47 if branches is not None:
48 return "|".join(_type_name(branch) for branch in branches)
49 return _WIRE_TYPE_NAMES[_resolved(schema)["type"]]
52def field_type_name(key: str) -> str:
53 """Render a field's wire type: ``int``, ``str``, ``list``, ``str|null``."""
54 return _type_name(_properties()[key])
57def field_value_set(key: str) -> tuple[str, ...] | None:
58 """The closed set of values a field accepts, or None when the set is open.
60 Only a scalar field has one: a collection's schema holds its member type
61 under ``items``, which this does not descend into, so a list of enum values
62 stays an open field rather than rendering as a single-select.
63 """
64 schema = _properties()[key]
65 for branch in schema.get("anyOf", [schema]):
66 values = _resolved(branch).get("enum")
67 if values is not None:
68 return tuple(str(value) for value in values)
69 return None