Coverage for src / lexigram / contracts / cli / parsers.py: 31%
54 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-19 05:41 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-19 05:41 +0800
1"""Field type specification parser for CLI code generators."""
3from __future__ import annotations
5from dataclasses import dataclass
6import re
8_FIELD_PATTERN = re.compile(
9 r"^(?P<name>[a-zA-Z_][a-zA-Z0-9_]*):(?P<type>[a-zA-Z_][a-zA-Z0-9_]*)(?P<rest>.*)$",
10)
11_FIELD_SPLIT_PATTERN = re.compile(r",(?=\s*[a-zA-Z_][a-zA-Z0-9_]*:)")
14@dataclass(slots=True, frozen=True)
15class FieldSpec:
16 """Specification for a single field parsed from a CLI field string."""
18 name: str
19 type: str
20 required: bool = True
21 unique: bool = False
22 fk: str | None = None
23 default: str | None = None
26def parse_fields(fields_str: str) -> list[FieldSpec]:
27 """Parse a comma-separated field spec string.
29 Format: ``name:type[?][!unique][!fk=Model][=default]``
30 """
31 if not fields_str.strip():
32 return []
33 fields: list[FieldSpec] = []
34 for raw in _FIELD_SPLIT_PATTERN.split(fields_str):
35 raw = raw.strip()
36 if not raw:
37 raise ValueError("Invalid field specification: empty segment")
38 fields.append(_parse_field(raw))
39 return fields
42def _parse_field(field_text: str) -> FieldSpec:
43 m = _FIELD_PATTERN.match(field_text)
44 if m is None:
45 raise ValueError(f"Invalid field specification: {field_text!r}")
46 name, type_, rest = m.group("name"), m.group("type"), m.group("rest")
47 required, unique, fk, default = True, False, None, None
48 if rest.startswith("?"):
49 required = False
50 rest = rest[1:]
51 while rest:
52 if rest.startswith("!unique"):
53 unique = True
54 rest = rest[len("!unique") :]
55 elif rest.startswith("!fk="):
56 fk, rest = _parse_foreign_key(rest, field_text)
57 elif rest.startswith("="):
58 default = rest[1:]
59 rest = ""
60 else:
61 raise ValueError(f"Invalid field specification: {field_text!r}")
62 return FieldSpec(
63 name=name, type=type_, required=required, unique=unique, fk=fk, default=default
64 )
67def _parse_foreign_key(remainder: str, field_text: str) -> tuple[str, str]:
68 value = remainder[len("!fk=") :]
69 idxs = [i for i in (value.find("!"), value.find("=")) if i != -1]
70 split = min(idxs, default=-1)
71 if split == -1:
72 fk, rest = value, ""
73 else:
74 fk, rest = value[:split], value[split:]
75 if not fk:
76 raise ValueError(f"Invalid field specification: {field_text!r}")
77 return fk, rest
80__all__ = ["FieldSpec", "parse_fields"]