Skip to content

Commit 24eae4e

Browse files
author
Juan Pablo Manson
committed
Refactor code and improve test coverage
- Reordered imports in `registry.py` for consistency. - Updated string formatting to use double quotes for consistency. - Enhanced `_init_builtin_types` function by organizing field imports and registration. - Improved readability of test cases by formatting data dictionaries and assertions. - Added comprehensive tests for JSON Schema export functionality. - Ensured backward compatibility in tests for form construction and validation. - Cleaned up unnecessary whitespace and comments across various test files.
1 parent 29b008b commit 24eae4e

24 files changed

+1784
-573
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ info
1515
# Agents
1616
CLAUDE.md
1717
.claude
18+
.agents
19+
knowledge.md
1820

1921
# Caches and temps
2022
.uv-cache

README.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,11 +167,115 @@ print(dict_output)
167167
| `html` | Semantic HTML |
168168
| `html_bootstrap4` | HTML with Bootstrap 4 classes |
169169
| `html_bootstrap5` | HTML with Bootstrap 5 classes |
170+
| `json_schema` | [JSON Schema](http://json-schema.org/draft-07/schema#) (draft-07) |
170171
| `json` | JSON representation of the form |
171172
| `dict` | Python dictionary representation |
172173

173174
HTML export can also generate a `<script>` block for basic client-side validation.
174175

176+
### JSON Schema Export
177+
178+
Generate a standard [JSON Schema (draft-07)](http://json-schema.org/draft-07/schema#) from any form. The resulting schema is compatible with tools like [React JSON Schema Form](https://github.com/rjsf-team/react-jsonschema-form), [Angular Formly](https://formly.dev/), and any JSON Schema validator.
179+
180+
```python
181+
import json
182+
from codeforms import (
183+
Form, TextField, EmailField, NumberField, SelectField, SelectOption,
184+
CheckboxField, form_to_json_schema,
185+
)
186+
187+
form = Form(
188+
name="registration",
189+
fields=[
190+
TextField(name="name", label="Full Name", required=True, minlength=2, maxlength=100),
191+
EmailField(name="email", label="Email", required=True),
192+
NumberField(name="age", label="Age", min_value=18, max_value=120),
193+
SelectField(
194+
name="country",
195+
label="Country",
196+
required=True,
197+
options=[
198+
SelectOption(value="us", label="United States"),
199+
SelectOption(value="uk", label="United Kingdom"),
200+
],
201+
),
202+
CheckboxField(name="terms", label="Accept Terms", required=True),
203+
],
204+
)
205+
206+
# Option 1: Direct function call
207+
schema = form_to_json_schema(form)
208+
print(json.dumps(schema, indent=2))
209+
210+
# Option 2: Via form.export()
211+
result = form.export("json_schema")
212+
schema = result["output"]
213+
```
214+
215+
Output:
216+
217+
```json
218+
{
219+
"$schema": "http://json-schema.org/draft-07/schema#",
220+
"type": "object",
221+
"title": "registration",
222+
"properties": {
223+
"name": {
224+
"type": "string",
225+
"minLength": 2,
226+
"maxLength": 100,
227+
"title": "Full Name"
228+
},
229+
"email": {
230+
"type": "string",
231+
"format": "email",
232+
"title": "Email"
233+
},
234+
"age": {
235+
"type": "number",
236+
"minimum": 18,
237+
"maximum": 120,
238+
"title": "Age"
239+
},
240+
"country": {
241+
"type": "string",
242+
"enum": ["us", "uk"],
243+
"title": "Country"
244+
},
245+
"terms": {
246+
"type": "boolean",
247+
"title": "Accept Terms"
248+
}
249+
},
250+
"required": ["name", "email", "country", "terms"],
251+
"additionalProperties": false
252+
}
253+
```
254+
255+
#### Field Type Mapping
256+
257+
| codeforms Field | JSON Schema Type | Extra Keywords |
258+
|---|---|---|
259+
| `TextField` | `string` | `minLength`, `maxLength`, `pattern` |
260+
| `EmailField` | `string` (`format: "email"`) ||
261+
| `NumberField` | `number` | `minimum`, `maximum`, `multipleOf` |
262+
| `DateField` | `string` (`format: "date"`) ||
263+
| `SelectField` | `string` + `enum` ||
264+
| `SelectField` (`multiple=True`) | `array` of `enum` strings | `minItems`, `maxItems`, `uniqueItems` |
265+
| `RadioField` | `string` + `enum` ||
266+
| `CheckboxField` | `boolean` ||
267+
| `CheckboxGroupField` | `array` of `enum` strings | `uniqueItems` |
268+
| `FileField` | `string` (`contentEncoding: "base64"`) ||
269+
| `FileField` (`multiple=True`) | `array` of base64 strings ||
270+
| `HiddenField` | `string` ||
271+
| `UrlField` | `string` (`format: "uri"`) | `minLength`, `maxLength` |
272+
| `TextareaField` | `string` | `minLength`, `maxLength` |
273+
| `ListField` | `array` | `minItems`, `maxItems` |
274+
275+
Field annotations like `label`, `help_text`, `default_value`, and `readonly` map to the JSON Schema keywords `title`, `description`, `default`, and `readOnly` respectively.
276+
277+
Fields inside `FieldGroup` and `FormStep` containers are flattened into the top-level `properties` automatically.
278+
175279
## Internationalization (i18n)
176280

177281
All validation and export messages are locale-aware. **English** (`en`) and **Spanish** (`es`) are included out of the box, and you can register any additional language at runtime via `register_locale()`.

examples/basic_usage.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
"""
77

88
from codeforms import (
9-
Form,
10-
TextField,
11-
EmailField,
12-
SelectField,
139
CheckboxField,
1410
CheckboxGroupField,
11+
EmailField,
12+
Form,
1513
RadioField,
14+
SelectField,
1615
SelectOption,
16+
TextField,
1717
ValidationRule,
1818
validate_form_data,
1919
)
@@ -205,7 +205,9 @@ def create_product_form() -> Form:
205205
],
206206
)
207207

208-
contact_form.set_default_values(data={"name": "John Doe", "email": "john@example.com"})
208+
contact_form.set_default_values(
209+
data={"name": "John Doe", "email": "john@example.com"}
210+
)
209211

210212
# Export as Bootstrap 4 HTML
211213
print(contact_form.export(output_format="html_bootstrap4", id="my_form"))

examples/conditional_visibility.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
from codeforms import (
99
Form,
10-
TextField,
1110
SelectField,
1211
SelectOption,
12+
TextField,
1313
VisibilityRule,
1414
validate_form_data,
1515
validate_form_data_dynamic,
@@ -79,7 +79,9 @@ def create_address_form() -> Form:
7979
print(f"US data: success={result['success']}")
8080
if not result["success"]:
8181
print(f" Errors: {result['errors']}")
82-
print(" (Province and County are required but missing — legacy doesn't know they're hidden)")
82+
print(
83+
" (Province and County are required but missing — legacy doesn't know they're hidden)"
84+
)
8385

8486
# --- Dynamic validation: respects visible_when ---
8587
print("\n=== Dynamic validate_form_data_dynamic (respects visible_when) ===")

examples/custom_fields.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,31 @@
1111
from pydantic import field_validator
1212

1313
from codeforms import (
14+
EmailField,
15+
FieldGroup,
1416
Form,
1517
FormFieldBase,
16-
FieldGroup,
1718
TextField,
18-
EmailField,
19-
register_field_type,
2019
get_registered_field_types,
20+
register_field_type,
2121
)
2222

23-
2423
# ---------------------------------------------------------------------------
2524
# 1. Define custom field types
2625
# ---------------------------------------------------------------------------
2726

27+
2828
class PhoneField(FormFieldBase):
2929
"""A phone number field with an optional country code."""
30+
3031
field_type: str = "phone"
3132
country_code: str = "+1"
3233
placeholder: Optional[str] = "e.g. +1-555-0100"
3334

3435

3536
class RatingField(FormFieldBase):
3637
"""A numeric rating field with configurable range."""
38+
3739
field_type: str = "rating"
3840
min_rating: int = 1
3941
max_rating: int = 5
@@ -49,6 +51,7 @@ def max_above_min(cls, v, info):
4951

5052
class ColorField(FormFieldBase):
5153
"""A colour picker field."""
54+
5255
field_type: str = "color"
5356
color_format: str = "hex" # hex | rgb | hsl
5457

examples/dependent_options.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
"""
77

88
from codeforms import (
9+
DependentOptionsConfig,
910
Form,
1011
SelectField,
1112
SelectOption,
12-
DependentOptionsConfig,
1313
)
1414

1515

@@ -62,6 +62,7 @@ def create_location_form() -> Form:
6262

6363
# La metadata de dependencia se serializa a JSON
6464
import json
65+
6566
data = json.loads(form.model_dump_json(exclude_none=True))
6667
city_field = data["content"][1]
6768
print("City field dependent_options:")

examples/i18n_usage.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,18 @@
66
"""
77

88
from codeforms import (
9-
Form,
10-
TextField,
119
EmailField,
10+
Form,
1211
NumberField,
1312
SelectField,
1413
SelectOption,
15-
validate_form_data,
16-
set_locale,
17-
get_locale,
14+
TextField,
1815
get_available_locales,
16+
get_locale,
1917
register_locale,
18+
set_locale,
2019
t,
20+
validate_form_data,
2121
)
2222

2323

@@ -90,15 +90,18 @@ def example_register_custom_locale():
9090
"""Register a new locale (Portuguese) and use it."""
9191
print("=== Custom locale (Portuguese) ===")
9292

93-
register_locale("pt", {
94-
"field.required": "Este campo é obrigatório",
95-
"field.required_named": "O campo {name} é obrigatório",
96-
"email.invalid": "E-mail inválido",
97-
"number.min_value": "O valor deve ser maior ou igual a {min}",
98-
"number.max_value": "O valor deve ser menor ou igual a {max}",
99-
"form.validation_success": "Dados validados com sucesso",
100-
"form.data_validation_error": "Erro na validação dos dados",
101-
})
93+
register_locale(
94+
"pt",
95+
{
96+
"field.required": "Este campo é obrigatório",
97+
"field.required_named": "O campo {name} é obrigatório",
98+
"email.invalid": "E-mail inválido",
99+
"number.min_value": "O valor deve ser maior ou igual a {min}",
100+
"number.max_value": "O valor deve ser menor ou igual a {max}",
101+
"form.validation_success": "Dados validados com sucesso",
102+
"form.data_validation_error": "Erro na validação dos dados",
103+
},
104+
)
102105

103106
set_locale("pt")
104107
print(f"Available locales: {get_available_locales()}")
@@ -114,9 +117,14 @@ def example_register_custom_locale():
114117
print(f"Email error: {result['errors'][0]['message']}")
115118

116119
# Key not in 'pt' catalog → falls back to English
117-
result = validate_form_data(form, {
118-
"name": "João", "email": "a@b.com", "country": "invalid",
119-
})
120+
result = validate_form_data(
121+
form,
122+
{
123+
"name": "João",
124+
"email": "a@b.com",
125+
"country": "invalid",
126+
},
127+
)
120128
print(f"Select error (fallback to English): {result['errors'][0]['message']}")
121129

122130
set_locale("en") # Restore default

examples/wizard_form.py

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,14 @@
66
"""
77

88
from codeforms import (
9+
CheckboxField,
10+
EmailField,
911
Form,
1012
FormStep,
11-
TextField,
12-
EmailField,
1313
NumberField,
1414
SelectField,
1515
SelectOption,
16-
CheckboxField,
17-
FieldGroup,
18-
validate_form_data_dynamic,
16+
TextField,
1917
)
2018

2119

@@ -85,35 +83,45 @@ def create_registration_wizard() -> Form:
8583
for i, step in enumerate(form.get_steps()):
8684
print(f"\n Step {i + 1}: {step.title}")
8785
for field in step.fields:
88-
print(f" - {field.name} ({'required' if field.required else 'optional'})")
86+
print(
87+
f" - {field.name} ({'required' if field.required else 'optional'})"
88+
)
8989

9090
# Validar paso 1
9191
print("\n--- Validating Step 1 ---")
92-
result = form.validate_step(0, {
93-
"first_name": "John",
94-
"last_name": "Doe",
95-
"email": "john@example.com",
96-
})
92+
result = form.validate_step(
93+
0,
94+
{
95+
"first_name": "John",
96+
"last_name": "Doe",
97+
"email": "john@example.com",
98+
},
99+
)
97100
print(f"Step 1 valid: {result['success']}")
98101

99102
# Validar paso 2
100103
print("\n--- Validating Step 2 ---")
101-
result = form.validate_step(1, {
102-
"plan": "pro",
103-
"team_size": 5,
104-
})
104+
result = form.validate_step(
105+
1,
106+
{
107+
"plan": "pro",
108+
"team_size": 5,
109+
},
110+
)
105111
print(f"Step 2 valid: {result['success']}")
106112

107113
# Validar todos los pasos
108114
print("\n--- Validating All Steps ---")
109-
result = form.validate_all_steps({
110-
"first_name": "John",
111-
"last_name": "Doe",
112-
"email": "john@example.com",
113-
"plan": "pro",
114-
"team_size": 5,
115-
"terms": True,
116-
})
115+
result = form.validate_all_steps(
116+
{
117+
"first_name": "John",
118+
"last_name": "Doe",
119+
"email": "john@example.com",
120+
"plan": "pro",
121+
"team_size": 5,
122+
"terms": True,
123+
}
124+
)
117125
print(f"All steps valid: {result['success']}")
118126

119127
# Exportar HTML

0 commit comments

Comments
 (0)