Coverage for src/lexigram/admin/openapi/field_converter.py: 97%
129 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
1from __future__ import annotations
3from typing import Any
5from lexigram.admin.schema import (
6 AvatarField,
7 BelongsToField,
8 BooleanField,
9 ColorField,
10 CurrencyField,
11 DateField,
12 DateTimeField,
13 EmailField,
14 EnumField,
15 FileField,
16 FloatField,
17 HasManyField,
18 HiddenField,
19 ImageField,
20 IntegerField,
21 JsonField,
22 KeyValueField,
23 MarkdownField,
24 MorphField,
25 MultiSelectField,
26 NumberField,
27 PasswordField,
28 RadioField,
29 RatingField,
30 RelationField,
31 RichTextField,
32 SelectField,
33 TagsField,
34 TextAreaField,
35 TextField,
36 TimeField,
37 ToggleField,
38 URLField,
39)
40from lexigram.admin.schema.base import SchemaField
41from lexigram.admin.schema.validators import (
42 LengthValidator,
43 PatternValidator,
44 RangeValidator,
45 RequiredValidator,
46)
48_CONVERTERS: dict[type[SchemaField], Any] = {}
51def _register(cls: type[SchemaField]) -> Any:
52 """Decorator to register a converter for a SchemaField subclass."""
54 def decorator(fn: Any) -> Any:
55 _CONVERTERS[cls] = fn
56 return fn
58 return decorator
61# -- String types --
64@_register(TextField)
65@_register(TextAreaField)
66@_register(MarkdownField)
67@_register(RichTextField)
68@_register(ColorField)
69@_register(HiddenField)
70def _string(_field: SchemaField) -> dict[str, Any]:
71 return {"type": "string"}
74@_register(EmailField)
75def _email(_field: SchemaField) -> dict[str, Any]:
76 return {"type": "string", "format": "email"}
79@_register(PasswordField)
80def _password(_field: SchemaField) -> dict[str, Any]:
81 return {"type": "string", "format": "password"}
84@_register(URLField)
85def _url(_field: SchemaField) -> dict[str, Any]:
86 return {"type": "string", "format": "uri"}
89# -- Numeric types --
92@_register(IntegerField)
93def _integer(_field: SchemaField) -> dict[str, Any]:
94 return {"type": "integer", "format": "int32"}
97@_register(FloatField)
98def _float(_field: SchemaField) -> dict[str, Any]:
99 return {"type": "number", "format": "float"}
102@_register(CurrencyField)
103@_register(NumberField)
104def _number(_field: SchemaField) -> dict[str, Any]:
105 return {"type": "number"}
108@_register(RatingField)
109def _rating(_field: SchemaField) -> dict[str, Any]:
110 return {"type": "integer", "minimum": 1, "maximum": 5}
113# -- Boolean types --
116@_register(BooleanField)
117@_register(ToggleField)
118def _boolean(_field: SchemaField) -> dict[str, Any]:
119 return {"type": "boolean"}
122# -- Date/time types --
125@_register(DateField)
126def _date(_field: SchemaField) -> dict[str, Any]:
127 return {"type": "string", "format": "date"}
130@_register(DateTimeField)
131def _datetime(_field: SchemaField) -> dict[str, Any]:
132 return {"type": "string", "format": "date-time"}
135@_register(TimeField)
136def _time(_field: SchemaField) -> dict[str, Any]:
137 return {"type": "string", "format": "time"}
140# -- Selection types --
143@_register(SelectField)
144@_register(RadioField)
145def _select(field: SchemaField) -> dict[str, Any]:
146 schema: dict[str, Any] = {"type": "string"}
147 _add_enum_if_options(field, schema)
148 return schema
151@_register(EnumField)
152def _enum(field: SchemaField) -> dict[str, Any]:
153 schema: dict[str, Any] = {"type": "string"}
154 _add_enum_if_options(field, schema)
155 return schema
158@_register(MultiSelectField)
159@_register(HasManyField)
160def _multi_select(field: SchemaField) -> dict[str, Any]:
161 schema: dict[str, Any] = {"type": "array", "items": {"type": "string"}}
162 if hasattr(field, "options") and field.options:
163 schema["items"]["enum"] = [opt[0] for opt in field.options]
164 return schema
167@_register(TagsField)
168def _tags(_field: SchemaField) -> dict[str, Any]:
169 return {"type": "array", "items": {"type": "string"}}
172# -- Relation types --
175@_register(RelationField)
176@_register(BelongsToField)
177@_register(MorphField)
178def _relation(_field: SchemaField) -> dict[str, Any]:
179 return {"type": "string"}
182# -- Complex types --
185@_register(JsonField)
186def _json(_field: SchemaField) -> dict[str, Any]:
187 return {"type": "object"}
190@_register(KeyValueField)
191def _key_value(_field: SchemaField) -> dict[str, Any]:
192 return {"type": "object", "additionalProperties": {"type": "string"}}
195# -- File types --
198@_register(FileField)
199@_register(ImageField)
200@_register(AvatarField)
201def _file(_field: SchemaField) -> dict[str, Any]:
202 return {"type": "string", "format": "binary"}
205# -- Helpers --
208def _add_enum_if_options(field: SchemaField, schema: dict[str, Any]) -> None:
209 """Add enum values to the schema if the field has options."""
210 if hasattr(field, "options") and field.options:
211 schema["enum"] = [opt[0] for opt in field.options]
214def _extract_validator_constraints(
215 field: SchemaField,
216) -> dict[str, Any]:
217 """Extract OpenAPI constraints from field validators."""
218 constraints: dict[str, Any] = {}
219 for validator in field.validators:
220 if isinstance(validator, RequiredValidator):
221 pass
222 elif isinstance(validator, LengthValidator):
223 if validator.min_length is not None:
224 constraints["minLength"] = validator.min_length
225 if validator.max_length is not None:
226 constraints["maxLength"] = validator.max_length
227 elif isinstance(validator, RangeValidator):
228 if validator.min_value is not None:
229 constraints["minimum"] = validator.min_value
230 if validator.max_value is not None:
231 constraints["maximum"] = validator.max_value
232 elif isinstance(validator, PatternValidator):
233 constraints["pattern"] = validator._regex.pattern
234 return constraints
237def field_to_openapi_property(field: SchemaField) -> dict[str, Any]:
238 """Convert a SchemaField to an OpenAPI property schema.
240 Args:
241 field: A SchemaField instance.
243 Returns:
244 An OpenAPI Schema Object dict representing the field's property
245 specification.
246 """
247 converter = _CONVERTERS.get(type(field))
248 if converter is None:
249 schema: dict[str, Any] = {"type": "string"}
250 else:
251 schema = converter(field)
253 if not field.nullable:
254 schema["nullable"] = False
256 if field.readonly:
257 schema["readOnly"] = True
259 if field.default is not None:
260 schema["default"] = field.default
262 if field.help_text:
263 schema["description"] = field.help_text
265 constraints = _extract_validator_constraints(field)
266 schema.update(constraints)
268 return schema
271__all__ = ["field_to_openapi_property"]