#include "Validate.hpp" #include #include "Caps.hpp" #include "Http.hpp" namespace molo::example::fw { ValidationError::ValidationError(std::vector failures) : HttpError(422, "validation_error", "validation failed"), failures_(std::move(failures)) {} const std::vector& 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 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 Model::validate( const std::map& input) const { std::vector failures; std::map 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& Model::fields() const { return fields_; } std::map parse_json_object_flat( const std::string& body) { std::map 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 Form::bind(const Request& req) const { std::string ct = req.headers.get("Content-Type"); std::map 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