Skip to content

Commit c8e8479

Browse files
committed
validation: document module, stabilize Schema/BaseModel, add full examples
- Document validation module with clear mental model and usage guide - Stabilize Schema API (FieldSpec, ParsedSpec, cross-field checks) - Introduce BaseModel CRTP for schema-bound models - Add Form helper for key/value validation workflows - Add comprehensive examples for Schema, BaseModel, Form, and cross-field rules - Ensure examples compile and reflect real-world usage patterns
1 parent 72db192 commit c8e8479

14 files changed

Lines changed: 1703 additions & 58 deletions

README.md

Lines changed: 202 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,202 @@
1-
# validation
2-
Composable validation framework for C++: rules, schemas, and structured errors for entities, models, and user input.
1+
# Vix Validation
2+
3+
The **validation module** provides a modern, declarative, and type-safe way to
4+
validate data in C++.
5+
6+
It is designed for:
7+
- backend APIs
8+
- forms and user input
9+
- configuration files
10+
- domain models
11+
- both beginners and advanced C++ developers
12+
13+
This module favors **clarity**, **composability**, and **zero hidden magic**.
14+
15+
---
16+
17+
## Philosophy
18+
19+
Validation in Vix is built around a few strong principles:
20+
21+
- **Declarative**: describe what is valid, not how to check it
22+
- **Type-safe**: errors are caught at compile time when possible
23+
- **Composable**: rules, schemas, and forms can be reused and combined
24+
- **Explicit**: no exceptions, no implicit conversions
25+
- **Beginner-friendly** but **expert-ready**
26+
27+
You can start with simple string validation and grow toward full form binding
28+
and cleaned output without changing your mental model.
29+
30+
---
31+
32+
## Core Concepts
33+
34+
The module is structured around five main building blocks:
35+
36+
| Concept | Purpose |
37+
|------|-------|
38+
| `Validator<T>` | Fluent validation of a single value |
39+
| `Schema<T>` | Declarative validation rules for a struct |
40+
| `Form<T>` | Bind raw input + validate + produce output |
41+
| `BaseModel<T>` | CRTP helper for schema-driven models |
42+
| `ValidationResult / ValidationErrors` | Structured error reporting |
43+
44+
---
45+
46+
## 1. Validating a Single Value
47+
48+
Use `validate(field, value)` for quick checks.
49+
50+
```cpp
51+
#include <vix/validation/Validate.hpp>
52+
53+
auto r = vix::validation::validate("email", email)
54+
.required()
55+
.email()
56+
.length_max(120)
57+
.result();
58+
59+
if (!r.ok())
60+
{
61+
// handle r.errors
62+
}
63+
```
64+
65+
Examples:
66+
- `examples/simple_string.cpp`
67+
- `examples/validate_string.cpp`
68+
- `examples/validate_enum.cpp`
69+
70+
---
71+
72+
## 2. Schema: Declarative Validation for Structs
73+
74+
### FieldSpec (beginner-friendly)
75+
76+
```cpp
77+
struct User
78+
{
79+
std::string email;
80+
std::string password;
81+
82+
static vix::validation::Schema<User> schema()
83+
{
84+
return vix::validation::schema<User>()
85+
.field("email", &User::email,
86+
vix::validation::field<std::string>()
87+
.required()
88+
.email()
89+
.length_max(120))
90+
.field("password", &User::password,
91+
vix::validation::field<std::string>()
92+
.required()
93+
.length_min(8)
94+
.length_max(64));
95+
}
96+
};
97+
```
98+
99+
Examples:
100+
- `examples/schema_fieldspec_basic.cpp`
101+
- `examples/schema.cpp`
102+
103+
---
104+
105+
### Lambda-based (expert-friendly)
106+
107+
```cpp
108+
.field("email", &User::email,
109+
[](std::string_view f, const std::string &v)
110+
{
111+
return vix::validation::validate(f, v)
112+
.required()
113+
.email();
114+
})
115+
```
116+
117+
Examples:
118+
- `examples/schema_lambda_builder.cpp`
119+
120+
---
121+
122+
### Cross-field validation
123+
124+
```cpp
125+
.check([](const User &u, ValidationErrors &errors)
126+
{
127+
if (u.password != u.confirm)
128+
{
129+
errors.add("confirm",
130+
ValidationErrorCode::Custom,
131+
"passwords do not match");
132+
}
133+
});
134+
```
135+
136+
Examples:
137+
- `examples/schema_cross_field_check.cpp`
138+
139+
---
140+
141+
## 3. Parsed Validation (string to typed)
142+
143+
Examples:
144+
- `examples/parsed.cpp`
145+
- `examples/form_parsed_age.cpp`
146+
- `examples/validate_parsed_int.cpp`
147+
148+
---
149+
150+
## 4. Form: Bind + Validate + Output
151+
152+
Examples:
153+
- `examples/form_kv_basic.cpp`
154+
- `examples/form_cleaned_output.cpp`
155+
- `examples/form_bind2_generic_error.cpp`
156+
157+
---
158+
159+
## 5. BaseModel: Schema-driven Models (CRTP)
160+
161+
Examples:
162+
- `examples/basemodel_basic.cpp`
163+
- `examples/basemodel_static_validate.cpp`
164+
- `examples/basemodel_cross_field_check.cpp`
165+
166+
---
167+
168+
## Error Model
169+
170+
Errors are structured, not strings.
171+
172+
ValidationError fields:
173+
- field
174+
- code
175+
- message
176+
177+
Codes:
178+
- Required
179+
- Min, Max
180+
- LengthMin, LengthMax
181+
- Between
182+
- Format
183+
- InSet
184+
- Custom
185+
186+
---
187+
188+
## Tests
189+
190+
Unit tests live in `tests/`.
191+
192+
Run:
193+
```bash
194+
ctest
195+
```
196+
197+
---
198+
199+
## License
200+
201+
MIT
202+
Part of the Vix.cpp project.

examples/basemodel_basic.cpp

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#include <iostream>
2+
#include <string>
3+
4+
#include <vix/validation/BaseModel.hpp>
5+
6+
struct RegisterForm : vix::validation::BaseModel<RegisterForm>
7+
{
8+
std::string email;
9+
std::string password;
10+
11+
static vix::validation::Schema<RegisterForm> schema()
12+
{
13+
return vix::validation::schema<RegisterForm>()
14+
.field("email", &RegisterForm::email,
15+
vix::validation::field<std::string>()
16+
.required()
17+
.email()
18+
.length_max(120))
19+
.field("password", &RegisterForm::password,
20+
vix::validation::field<std::string>()
21+
.required()
22+
.length_min(8)
23+
.length_max(64));
24+
}
25+
};
26+
27+
int main()
28+
{
29+
RegisterForm f;
30+
f.email = "bad-email";
31+
f.password = "123";
32+
33+
auto r = f.validate();
34+
35+
std::cout << "ok=" << static_cast<bool>(r.ok()) << "\n";
36+
std::cout << "is_valid=" << static_cast<bool>(f.is_valid()) << "\n";
37+
38+
if (!r.ok())
39+
{
40+
for (const auto &e : r.errors.all())
41+
{
42+
std::cout << " - field=" << e.field
43+
<< " code=" << vix::validation::to_string(e.code)
44+
<< " message=" << e.message << "\n";
45+
}
46+
return 1;
47+
}
48+
49+
std::cout << "valid form\n";
50+
return 0;
51+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#include <iostream>
2+
#include <string>
3+
4+
#include <vix/validation/BaseModel.hpp>
5+
#include <vix/validation/ValidationErrors.hpp>
6+
#include <vix/validation/ValidationError.hpp>
7+
8+
struct ResetPassword : vix::validation::BaseModel<ResetPassword>
9+
{
10+
std::string password;
11+
std::string confirm;
12+
13+
static vix::validation::Schema<ResetPassword> schema()
14+
{
15+
return vix::validation::schema<ResetPassword>()
16+
.field("password", &ResetPassword::password,
17+
vix::validation::field<std::string>()
18+
.required()
19+
.length_min(8)
20+
.length_max(64))
21+
.field("confirm", &ResetPassword::confirm,
22+
vix::validation::field<std::string>()
23+
.required()
24+
.length_min(8)
25+
.length_max(64))
26+
.check([](const ResetPassword &obj, vix::validation::ValidationErrors &errors)
27+
{
28+
if (!obj.password.empty() && !obj.confirm.empty() && obj.password != obj.confirm)
29+
{
30+
errors.add("confirm",
31+
vix::validation::ValidationErrorCode::Custom,
32+
"passwords do not match");
33+
} });
34+
}
35+
};
36+
37+
int main()
38+
{
39+
ResetPassword f;
40+
f.password = "password123";
41+
f.confirm = "password124";
42+
43+
auto r = f.validate();
44+
45+
std::cout << "ok=" << static_cast<bool>(r.ok()) << "\n";
46+
if (!r.ok())
47+
{
48+
for (const auto &e : r.errors.all())
49+
{
50+
std::cout << " - field=" << e.field
51+
<< " code=" << vix::validation::to_string(e.code)
52+
<< " message=" << e.message << "\n";
53+
}
54+
return 1;
55+
}
56+
57+
std::cout << "valid reset\n";
58+
return 0;
59+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#include <iostream>
2+
#include <string>
3+
4+
#include <vix/validation/BaseModel.hpp>
5+
6+
struct ProductInput : vix::validation::BaseModel<ProductInput>
7+
{
8+
std::string title;
9+
std::string currency;
10+
11+
static vix::validation::Schema<ProductInput> schema()
12+
{
13+
return vix::validation::schema<ProductInput>()
14+
.field("title", &ProductInput::title,
15+
vix::validation::field<std::string>()
16+
.required()
17+
.length_min(3)
18+
.length_max(80))
19+
.field("currency", &ProductInput::currency,
20+
vix::validation::field<std::string>()
21+
.required()
22+
.in_set({"USD", "EUR", "UGX"}, "currency must be USD/EUR/UGX"));
23+
}
24+
};
25+
26+
int main()
27+
{
28+
ProductInput p;
29+
p.title = "TV";
30+
p.currency = "BTC";
31+
32+
// Static validation (no need to call p.validate())
33+
auto r = ProductInput::validate(p);
34+
35+
std::cout << "ok=" << static_cast<bool>(r.ok()) << "\n";
36+
37+
// Cached schema access
38+
const auto &sc = ProductInput::schema();
39+
(void)sc;
40+
41+
if (!r.ok())
42+
{
43+
for (const auto &e : r.errors.all())
44+
{
45+
std::cout << " - field=" << e.field
46+
<< " code=" << vix::validation::to_string(e.code)
47+
<< " message=" << e.message << "\n";
48+
}
49+
return 1;
50+
}
51+
52+
std::cout << "valid product input\n";
53+
return 0;
54+
}

0 commit comments

Comments
 (0)