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

1from __future__ import annotations 

2 

3from typing import Any 

4 

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) 

47 

48_CONVERTERS: dict[type[SchemaField], Any] = {} 

49 

50 

51def _register(cls: type[SchemaField]) -> Any: 

52 """Decorator to register a converter for a SchemaField subclass.""" 

53 

54 def decorator(fn: Any) -> Any: 

55 _CONVERTERS[cls] = fn 

56 return fn 

57 

58 return decorator 

59 

60 

61# -- String types -- 

62 

63 

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

72 

73 

74@_register(EmailField) 

75def _email(_field: SchemaField) -> dict[str, Any]: 

76 return {"type": "string", "format": "email"} 

77 

78 

79@_register(PasswordField) 

80def _password(_field: SchemaField) -> dict[str, Any]: 

81 return {"type": "string", "format": "password"} 

82 

83 

84@_register(URLField) 

85def _url(_field: SchemaField) -> dict[str, Any]: 

86 return {"type": "string", "format": "uri"} 

87 

88 

89# -- Numeric types -- 

90 

91 

92@_register(IntegerField) 

93def _integer(_field: SchemaField) -> dict[str, Any]: 

94 return {"type": "integer", "format": "int32"} 

95 

96 

97@_register(FloatField) 

98def _float(_field: SchemaField) -> dict[str, Any]: 

99 return {"type": "number", "format": "float"} 

100 

101 

102@_register(CurrencyField) 

103@_register(NumberField) 

104def _number(_field: SchemaField) -> dict[str, Any]: 

105 return {"type": "number"} 

106 

107 

108@_register(RatingField) 

109def _rating(_field: SchemaField) -> dict[str, Any]: 

110 return {"type": "integer", "minimum": 1, "maximum": 5} 

111 

112 

113# -- Boolean types -- 

114 

115 

116@_register(BooleanField) 

117@_register(ToggleField) 

118def _boolean(_field: SchemaField) -> dict[str, Any]: 

119 return {"type": "boolean"} 

120 

121 

122# -- Date/time types -- 

123 

124 

125@_register(DateField) 

126def _date(_field: SchemaField) -> dict[str, Any]: 

127 return {"type": "string", "format": "date"} 

128 

129 

130@_register(DateTimeField) 

131def _datetime(_field: SchemaField) -> dict[str, Any]: 

132 return {"type": "string", "format": "date-time"} 

133 

134 

135@_register(TimeField) 

136def _time(_field: SchemaField) -> dict[str, Any]: 

137 return {"type": "string", "format": "time"} 

138 

139 

140# -- Selection types -- 

141 

142 

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 

149 

150 

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 

156 

157 

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 

165 

166 

167@_register(TagsField) 

168def _tags(_field: SchemaField) -> dict[str, Any]: 

169 return {"type": "array", "items": {"type": "string"}} 

170 

171 

172# -- Relation types -- 

173 

174 

175@_register(RelationField) 

176@_register(BelongsToField) 

177@_register(MorphField) 

178def _relation(_field: SchemaField) -> dict[str, Any]: 

179 return {"type": "string"} 

180 

181 

182# -- Complex types -- 

183 

184 

185@_register(JsonField) 

186def _json(_field: SchemaField) -> dict[str, Any]: 

187 return {"type": "object"} 

188 

189 

190@_register(KeyValueField) 

191def _key_value(_field: SchemaField) -> dict[str, Any]: 

192 return {"type": "object", "additionalProperties": {"type": "string"}} 

193 

194 

195# -- File types -- 

196 

197 

198@_register(FileField) 

199@_register(ImageField) 

200@_register(AvatarField) 

201def _file(_field: SchemaField) -> dict[str, Any]: 

202 return {"type": "string", "format": "binary"} 

203 

204 

205# -- Helpers -- 

206 

207 

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] 

212 

213 

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 

235 

236 

237def field_to_openapi_property(field: SchemaField) -> dict[str, Any]: 

238 """Convert a SchemaField to an OpenAPI property schema. 

239 

240 Args: 

241 field: A SchemaField instance. 

242 

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) 

252 

253 if not field.nullable: 

254 schema["nullable"] = False 

255 

256 if field.readonly: 

257 schema["readOnly"] = True 

258 

259 if field.default is not None: 

260 schema["default"] = field.default 

261 

262 if field.help_text: 

263 schema["description"] = field.help_text 

264 

265 constraints = _extract_validator_constraints(field) 

266 schema.update(constraints) 

267 

268 return schema 

269 

270 

271__all__ = ["field_to_openapi_property"]