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

1"""Per-field type and value-set metadata, read off the JSON schema pydantic builds.""" 

2 

3from __future__ import annotations 

4 

5from functools import cache 

6from typing import Any 

7 

8from .model import Config 

9 

10# Where pydantic puts the named sub-schema a field's type resolves to. 

11_DEFS_PREFIX = "#/$defs/" 

12 

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} 

23 

24 

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"]) 

29 

30 

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", {})) 

35 

36 

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)] 

43 

44 

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"]] 

50 

51 

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]) 

55 

56 

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. 

59 

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