Skip to content

Commit db04198

Browse files
committed
validation: add schema validation and conversion-aware rules
1 parent 2126e16 commit db04198

12 files changed

Lines changed: 1371 additions & 0 deletions

examples/validation_examples.cpp

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/**
2+
* Validation examples for Vix.cpp
3+
*
4+
* This file demonstrates how to use the validation module:
5+
* - simple field validation
6+
* - numeric validation
7+
* - parsed validation (string -> int)
8+
* - schema / form validation
9+
*
10+
* These examples are intended for:
11+
* - HTTP controllers
12+
* - JSON / form validation
13+
* - CLI argument validation
14+
*/
15+
16+
#include <iostream>
17+
#include <optional>
18+
#include <string>
19+
20+
#include <vix/validation/Schema.hpp>
21+
#include <vix/validation/Validate.hpp>
22+
#include <vix/validation/Pipe.hpp>
23+
24+
using namespace vix::validation;
25+
26+
// ------------------------------------------------------------
27+
// Example 1: simple string validation
28+
// ------------------------------------------------------------
29+
void example_simple_string()
30+
{
31+
std::string email = "john@example.com";
32+
33+
auto res = validate("email", email)
34+
.required()
35+
.email()
36+
.length_max(120)
37+
.result();
38+
39+
std::cout << "[example_simple_string] ok=" << res.ok() << "\n";
40+
}
41+
42+
// ------------------------------------------------------------
43+
// Example 2: numeric validation (already typed)
44+
// ------------------------------------------------------------
45+
void example_numeric()
46+
{
47+
int age = 17;
48+
49+
auto res = validate("age", age)
50+
.min(18, "must be adult")
51+
.max(120)
52+
.result();
53+
54+
std::cout << "[example_numeric] ok=" << res.ok() << "\n";
55+
}
56+
57+
// ------------------------------------------------------------
58+
// Example 3: parsed validation (string -> int)
59+
// ------------------------------------------------------------
60+
void example_parsed()
61+
{
62+
std::string age_input = "25"; // try "abc" or "10"
63+
64+
auto res = validate_parsed<int>("age", age_input)
65+
.between(18, 120)
66+
.result("age must be a number");
67+
68+
std::cout << "[example_parsed] ok=" << res.ok() << "\n";
69+
}
70+
71+
// ------------------------------------------------------------
72+
// Example 4: optional field
73+
// ------------------------------------------------------------
74+
void example_optional()
75+
{
76+
std::optional<int> score = std::nullopt;
77+
78+
auto res = validate("score", score)
79+
.required("score is required")
80+
.result();
81+
82+
std::cout << "[example_optional] ok=" << res.ok() << "\n";
83+
}
84+
85+
// ------------------------------------------------------------
86+
// Example 5: in_set validation
87+
// ------------------------------------------------------------
88+
void example_in_set()
89+
{
90+
std::string role = "admin";
91+
92+
auto res = validate("role", role)
93+
.required()
94+
.in_set({"admin", "user", "guest"})
95+
.result();
96+
97+
std::cout << "[example_in_set] ok=" << res.ok() << "\n";
98+
}
99+
100+
// ------------------------------------------------------------
101+
// Example 6: schema validation (form / entity)
102+
// ------------------------------------------------------------
103+
struct RegisterForm
104+
{
105+
std::string email;
106+
std::string password;
107+
std::string age; // raw input
108+
};
109+
110+
void example_schema()
111+
{
112+
auto schema = vix::validation::schema<RegisterForm>()
113+
.field("email", &RegisterForm::email,
114+
[](auto f, const std::string &v)
115+
{
116+
return validate(f, v)
117+
.required()
118+
.email()
119+
.length_max(120)
120+
.result();
121+
})
122+
.field("password", &RegisterForm::password,
123+
[](auto f, const std::string &v)
124+
{
125+
return validate(f, v)
126+
.required()
127+
.length_min(8)
128+
.length_max(64)
129+
.result();
130+
})
131+
.parsed<int>("age", &RegisterForm::age,
132+
[](auto f, std::string_view sv)
133+
{
134+
return validate_parsed<int>(f, sv)
135+
.between(18, 120)
136+
.result("age must be a number");
137+
});
138+
139+
RegisterForm form{
140+
"bad-email",
141+
"123",
142+
"abc"};
143+
144+
auto res = schema.validate(form);
145+
146+
std::cout << "[example_schema] ok=" << res.ok() << "\n";
147+
std::cout << "errors=" << res.errors.size() << "\n";
148+
149+
for (const auto &e : res.errors.all())
150+
{
151+
std::cout
152+
<< " - field=" << e.field
153+
<< " code=" << to_string(e.code)
154+
<< " message=" << e.message
155+
<< "\n";
156+
}
157+
}
158+
159+
// ------------------------------------------------------------
160+
// Main
161+
// ------------------------------------------------------------
162+
int main()
163+
{
164+
example_simple_string();
165+
example_numeric();
166+
example_parsed();
167+
example_optional();
168+
example_in_set();
169+
example_schema();
170+
171+
return 0;
172+
}

include/vix/validation/Pipe.hpp

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#pragma once
2+
3+
#include <string>
4+
#include <string_view>
5+
#include <type_traits>
6+
7+
#include <vix/conversion/ConversionError.hpp>
8+
#include <vix/conversion/Parse.hpp>
9+
10+
#include <vix/validation/Validate.hpp>
11+
#include <vix/validation/ValidationResult.hpp>
12+
13+
namespace vix::validation
14+
{
15+
16+
/**
17+
* @brief Convert a conversion error into a validation error (semantic).
18+
*
19+
* Notes:
20+
* - Validation should not leak low-level parsing details by default.
21+
* - We map it to ValidationErrorCode::Format.
22+
* - conversion error code is stored in meta for debugging/observability.
23+
*/
24+
[[nodiscard]] inline ValidationError
25+
conversion_error_to_validation(
26+
std::string_view field,
27+
const vix::conversion::ConversionError &err,
28+
std::string message = "invalid value")
29+
{
30+
ValidationError ve{
31+
std::string(field),
32+
ValidationErrorCode::Format,
33+
std::move(message)};
34+
35+
ve.meta["conversion_code"] = std::string(vix::conversion::to_string(err.code));
36+
if (!err.input.empty())
37+
{
38+
ve.meta["input"] = std::string(err.input);
39+
}
40+
ve.meta["position"] = std::to_string(err.position);
41+
42+
return ve;
43+
}
44+
45+
/**
46+
* @brief Validate a scalar value that comes as string input:
47+
* - parse input -> T using vix::conversion
48+
* - if parse fails => add a format error
49+
* - else apply validation rules on T
50+
*
51+
* Example:
52+
* auto res = validate_parsed<int>("age", input)
53+
* .between(18, 120)
54+
* .result();
55+
*/
56+
template <typename T>
57+
class ParsedValidator
58+
{
59+
public:
60+
ParsedValidator(std::string_view field, std::string_view input)
61+
: field_(field), input_(input)
62+
{
63+
}
64+
65+
// Add a rule (applies only if parsing succeeds)
66+
ParsedValidator &rule(Rule<T> r)
67+
{
68+
rules_.push_back(std::move(r));
69+
return *this;
70+
}
71+
72+
// Numeric helpers
73+
ParsedValidator &min(T v, std::string message = "value is below minimum")
74+
requires std::is_arithmetic_v<T>
75+
{
76+
return rule(rules::min<T>(v, std::move(message)));
77+
}
78+
79+
ParsedValidator &max(T v, std::string message = "value is above maximum")
80+
requires std::is_arithmetic_v<T>
81+
{
82+
return rule(rules::max<T>(v, std::move(message)));
83+
}
84+
85+
ParsedValidator &between(T a, T b, std::string message = "value is out of range")
86+
requires std::is_arithmetic_v<T>
87+
{
88+
return rule(rules::between<T>(a, b, std::move(message)));
89+
}
90+
91+
[[nodiscard]] ValidationResult result(std::string parse_message = "invalid value") const
92+
{
93+
ValidationErrors out;
94+
95+
auto parsed = vix::conversion::parse<T>(input_);
96+
if (!parsed)
97+
{
98+
out.add(conversion_error_to_validation(field_, parsed.error(), std::move(parse_message)));
99+
return ValidationResult{std::move(out)};
100+
}
101+
102+
const T &value = parsed.value();
103+
104+
for (const auto &r : rules_)
105+
{
106+
if (r)
107+
{
108+
r(field_, value, out);
109+
}
110+
}
111+
112+
return ValidationResult{std::move(out)};
113+
}
114+
115+
private:
116+
std::string_view field_;
117+
std::string_view input_;
118+
std::vector<Rule<T>> rules_;
119+
};
120+
121+
/**
122+
* @brief Factory for ParsedValidator<T>.
123+
*/
124+
template <typename T>
125+
[[nodiscard]] inline ParsedValidator<T>
126+
validate_parsed(std::string_view field, std::string_view input)
127+
{
128+
return ParsedValidator<T>(field, input);
129+
}
130+
131+
} // namespace vix::validation

include/vix/validation/Rule.hpp

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#pragma once
2+
3+
#include <functional>
4+
#include <string_view>
5+
#include <type_traits>
6+
7+
#include <vix/validation/ValidationErrors.hpp>
8+
#include <vix/validation/ValidationResult.hpp>
9+
10+
namespace vix::validation
11+
{
12+
13+
/**
14+
* @brief Rule<T> represents a single validation rule for a value of type T.
15+
*
16+
* A rule is a callable that can push errors into ValidationErrors.
17+
*
18+
* Signature:
19+
* void(std::string_view field, const T& value, ValidationErrors& out)
20+
*/
21+
template <typename T>
22+
using Rule = std::function<void(std::string_view, const T &, ValidationErrors &)>;
23+
24+
/**
25+
* @brief Apply a list of rules to a value and collect errors.
26+
*/
27+
template <typename T>
28+
[[nodiscard]] inline ValidationResult apply_rules(
29+
std::string_view field,
30+
const T &value,
31+
const std::vector<Rule<T>> &rules)
32+
{
33+
ValidationErrors errors;
34+
35+
for (const auto &rule : rules)
36+
{
37+
if (rule)
38+
{
39+
rule(field, value, errors);
40+
}
41+
}
42+
43+
return ValidationResult{std::move(errors)};
44+
}
45+
46+
} // namespace vix::validation

0 commit comments

Comments
 (0)