Full C++23 framework tree: runtime, observability, security, data, app, realtime, supervisor, clients, CLI, example plus enterprise tenancy, audit, operability, governance, supplychain. Postgres via docker compose for tests.
190 lines
5.4 KiB
C++
190 lines
5.4 KiB
C++
#include "Validate.hpp"
|
|
|
|
#include <cctype>
|
|
|
|
#include "Caps.hpp"
|
|
#include "Http.hpp"
|
|
|
|
namespace molo::example::fw {
|
|
|
|
ValidationError::ValidationError(std::vector<FieldError> failures)
|
|
: HttpError(422, "validation_error", "validation failed"),
|
|
failures_(std::move(failures)) {}
|
|
|
|
const std::vector<FieldError>& ValidationError::failures() const {
|
|
return failures_;
|
|
}
|
|
|
|
std::string ValidationError::to_json() const {
|
|
std::string out = "{\"error\":\"validation_error\",\"failures\":[";
|
|
bool first = true;
|
|
for (const auto& f : failures_) {
|
|
if (!first) out += ",";
|
|
first = false;
|
|
out += "{\"field\":\"" + json_escape(f.field) + "\",\"message\":\"" +
|
|
json_escape(f.message) + "\"}";
|
|
}
|
|
return out + "]}";
|
|
}
|
|
|
|
Model::Model(std::vector<Field> fields) : fields_(std::move(fields)) {}
|
|
|
|
namespace {
|
|
|
|
bool looks_like_email(const std::string& v) {
|
|
auto at = v.find('@');
|
|
if (at == std::string::npos || at == 0) return false;
|
|
return v.find('.', at) != std::string::npos;
|
|
}
|
|
|
|
bool parse_long(const std::string& v, long& out) {
|
|
if (v.empty()) return false;
|
|
std::size_t i = 0;
|
|
bool neg = false;
|
|
if (v[0] == '-' || v[0] == '+') {
|
|
neg = v[0] == '-';
|
|
i = 1;
|
|
}
|
|
if (i >= v.size()) return false;
|
|
long acc = 0;
|
|
for (; i < v.size(); ++i) {
|
|
if (!std::isdigit((unsigned char)v[i])) return false;
|
|
acc = acc * 10 + (v[i] - '0');
|
|
}
|
|
out = neg ? -acc : acc;
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::map<std::string, std::string> Model::validate(
|
|
const std::map<std::string, std::string>& input) const {
|
|
std::vector<FieldError> failures;
|
|
std::map<std::string, std::string> clean;
|
|
for (const auto& f : fields_) {
|
|
auto it = input.find(f.name);
|
|
std::string value = it == input.end() ? "" : it->second;
|
|
bool missing = it == input.end() || value.empty();
|
|
if (missing) {
|
|
if (!f.required) {
|
|
if (!f.default_value.empty()) clean[f.name] = f.default_value;
|
|
continue;
|
|
}
|
|
failures.push_back({f.name, "field is required"});
|
|
continue;
|
|
}
|
|
if (f.type != FieldType::Integer && value.size() < f.min_len)
|
|
failures.push_back({f.name, "too short"});
|
|
else if (f.type != FieldType::Integer && value.size() > f.max_len)
|
|
failures.push_back({f.name, "too long"});
|
|
else if (f.type == FieldType::Integer) {
|
|
long n = 0;
|
|
if (!parse_long(value, n)) {
|
|
failures.push_back({f.name, "not an integer"});
|
|
} else if (f.has_range && (n < f.min_value || n > f.max_value)) {
|
|
failures.push_back({f.name, "out of range"});
|
|
} else {
|
|
clean[f.name] = value;
|
|
}
|
|
continue;
|
|
} else if (f.type == FieldType::Email && !looks_like_email(value)) {
|
|
failures.push_back({f.name, "not an email address"});
|
|
continue;
|
|
} else if (f.type == FieldType::Boolean &&
|
|
value != "true" && value != "false" && value != "1" &&
|
|
value != "0") {
|
|
failures.push_back({f.name, "not a boolean"});
|
|
continue;
|
|
} else {
|
|
clean[f.name] = value;
|
|
}
|
|
}
|
|
if (!failures.empty()) throw ValidationError(std::move(failures));
|
|
return clean;
|
|
}
|
|
|
|
const std::vector<Field>& Model::fields() const { return fields_; }
|
|
|
|
std::map<std::string, std::string> parse_json_object_flat(
|
|
const std::string& body) {
|
|
std::map<std::string, std::string> out;
|
|
std::size_t i = 0;
|
|
int depth = 0;
|
|
auto skip_ws = [&]() {
|
|
while (i < body.size() &&
|
|
std::isspace((unsigned char)body[i]))
|
|
++i;
|
|
};
|
|
skip_ws();
|
|
if (i >= body.size() || body[i] != '{') return out;
|
|
++i;
|
|
++depth;
|
|
std::size_t fields = 0;
|
|
while (i < body.size() && fields < caps::kMaxFormFields) {
|
|
skip_ws();
|
|
if (i < body.size() && body[i] == '}') break;
|
|
if (i >= body.size() || body[i] != '"') break;
|
|
++i;
|
|
std::string key;
|
|
while (i < body.size() && body[i] != '"') {
|
|
if (body[i] == '\\' && i + 1 < body.size()) {
|
|
key.push_back(body[i + 1]);
|
|
i += 2;
|
|
} else {
|
|
key.push_back(body[i++]);
|
|
}
|
|
}
|
|
if (i < body.size()) ++i;
|
|
skip_ws();
|
|
if (i >= body.size() || body[i] != ':') break;
|
|
++i;
|
|
skip_ws();
|
|
std::string value;
|
|
if (i < body.size() && body[i] == '"') {
|
|
++i;
|
|
while (i < body.size() && body[i] != '"') {
|
|
if (body[i] == '\\' && i + 1 < body.size()) {
|
|
value.push_back(body[i + 1]);
|
|
i += 2;
|
|
} else {
|
|
value.push_back(body[i++]);
|
|
}
|
|
}
|
|
if (i < body.size()) ++i;
|
|
} else {
|
|
while (i < body.size() && body[i] != ',' && body[i] != '}') {
|
|
if (body[i] == '{' || body[i] == '[') {
|
|
if (++depth > caps::kMaxJsonDepth) return out;
|
|
}
|
|
value.push_back(body[i++]);
|
|
}
|
|
while (!value.empty() &&
|
|
std::isspace((unsigned char)value.back()))
|
|
value.pop_back();
|
|
}
|
|
out[key] = value;
|
|
++fields;
|
|
skip_ws();
|
|
if (i < body.size() && body[i] == ',') ++i;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
Form::Form(Model model) : model_(std::move(model)) {}
|
|
|
|
std::map<std::string, std::string> Form::bind(const Request& req) const {
|
|
std::string ct = req.headers.get("Content-Type");
|
|
std::map<std::string, std::string> input;
|
|
if (ct.find("application/json") != std::string::npos) {
|
|
input = parse_json_object_flat(req.body);
|
|
} else {
|
|
input = parse_form(req.body);
|
|
for (const auto& [k, v] : req.query) {
|
|
if (!input.count(k)) input[k] = v;
|
|
}
|
|
}
|
|
return model_.validate(input);
|
|
}
|
|
|
|
} // namespace molo::example::fw
|