diff --git a/.gitmodules b/.gitmodules index 0dbaf91..2a33fde 100644 --- a/.gitmodules +++ b/.gitmodules @@ -71,3 +71,11 @@ path = modules/async url = https://github.com/vixcpp/async.git branch = dev +[submodule "modules/conversion"] + path = modules/conversion + url = https://github.com/vixcpp/conversion.git + branch = dev +[submodule "modules/validation"] + path = modules/validation + url = https://github.com/vixcpp/validation.git + branch = dev diff --git a/CMakeLists.txt b/CMakeLists.txt index 4cdf5e1..a8b7a02 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -129,6 +129,7 @@ option(VIX_ENABLE_P2P_HTTP "Build Vix P2P HTTP adapter module" ON) option(VIX_ENABLE_CACHE "Build Vix Cache module" ON) option(VIX_FETCH_DEPS "Allow fetching missing deps from the internet" OFF) option(VIX_ENABLE_ASYNC "Build Vix Async module" ON) +option(VIX_ENABLE_VALIDATION "Build Vix Validation module" ON) # ---------------------------------------------------- # Tooling / Static analysis @@ -395,6 +396,53 @@ else() message(FATAL_ERROR "Missing 'modules/core'. Run: git submodule update --init --recursive") endif() +# --- Conversion (required by Validation) --- +set(VIX_HAS_CONVERSION OFF) + +if (TARGET vix::conversion) + set(VIX_HAS_CONVERSION ON) +else() + # If you have it as a module, add it here (umbrella must not fetch) + if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/conversion/CMakeLists.txt") + message(STATUS "Adding 'modules/conversion'...") + add_subdirectory(modules/conversion conversion_build) + + if (TARGET vix::conversion OR TARGET vix_conversion) + set(VIX_HAS_CONVERSION ON) + if (TARGET vix_conversion AND NOT TARGET vix::conversion) + add_library(vix::conversion ALIAS vix_conversion) + endif() + else() + message(FATAL_ERROR "Conversion module added but no vix::conversion target was exported.") + endif() + else() + message(STATUS "Conversion: not present as module. Expecting it to be provided by core or another module.") + endif() +endif() + +# --- Validation (optional) --- +set(VIX_HAS_VALIDATION OFF) + +if (VIX_ENABLE_VALIDATION AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/validation/CMakeLists.txt") + if (NOT TARGET vix::conversion) + message(FATAL_ERROR "Validation requires vix::conversion. Ensure conversion is added before validation in the umbrella.") + endif() + + message(STATUS "Adding 'modules/validation'...") + add_subdirectory(modules/validation validation_build) + + if (TARGET vix::validation OR TARGET vix_validation) + set(VIX_HAS_VALIDATION ON) + if (TARGET vix_validation AND NOT TARGET vix::validation) + add_library(vix::validation ALIAS vix_validation) + endif() + else() + message(WARNING "Validation module added but no vix::validation target was exported.") + endif() +else() + message(STATUS "Validation: disabled or not present.") +endif() + # --- Async (optional) --- set(VIX_HAS_ASYNC OFF) if (VIX_ENABLE_ASYNC AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/async/CMakeLists.txt") @@ -626,6 +674,14 @@ target_link_libraries(vix INTERFACE ${JSON_TARGET} ) +if (TARGET vix::conversion) + target_link_libraries(vix INTERFACE vix::conversion) +endif() + +if (TARGET vix::validation) + target_link_libraries(vix INTERFACE vix::validation) +endif() + if (TARGET vix::net) target_link_libraries(vix INTERFACE vix::net) endif() @@ -894,6 +950,16 @@ if (VIX_ENABLE_INSTALL) FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h") endif() + if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/conversion/include") + install(DIRECTORY modules/conversion/include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h") + endif() + + if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/validation/include") + install(DIRECTORY modules/validation/include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h") + endif() + if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/json/include") install(DIRECTORY modules/json/include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h") @@ -1003,6 +1069,16 @@ if (VIX_ENABLE_INSTALL) set(VIX_HAS_P2P_HTTP ${VIX_HAS_P2P_HTTP}) set(VIX_WITH_P2P_HTTP ${VIX_WITH_P2P_HTTP}) + set(VIX_WITH_CONVERSION OFF) + if (VIX_HAS_CONVERSION) + set(VIX_WITH_CONVERSION ON) + endif() + + set(VIX_WITH_VALIDATION OFF) + if (VIX_HAS_VALIDATION) + set(VIX_WITH_VALIDATION ON) + endif() + configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/cmake/VixConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/VixConfig.cmake" @@ -1041,6 +1117,8 @@ message(STATUS "WebSocket built : ${VIX_HAS_WEBSOCKET}") message(STATUS "ORM packaged : ${VIX_HAS_ORM}") message(STATUS "DB built : ${VIX_HAS_DB}") message(STATUS "P2P built : ${VIX_HAS_P2P}") +message(STATUS "Conversion built : ${VIX_HAS_CONVERSION}") +message(STATUS "Validation built : ${VIX_HAS_VALIDATION}") message(STATUS "Middleware built : ${VIX_HAS_MIDDLEWARE}") message(STATUS "Examples : ${VIX_BUILD_EXAMPLES}") message(STATUS "Tests : ${VIX_BUILD_TESTS}") @@ -1048,6 +1126,6 @@ message(STATUS "Sanitizers : ${VIX_ENABLE_SANITIZERS}") message(STATUS "Coverage : ${VIX_ENABLE_COVERAGE}") message(STATUS "LTO : ${VIX_ENABLE_LTO}") message(STATUS "Install/Export : ${VIX_ENABLE_INSTALL}") -message(STATUS "Flags exported : JSON=${VIX_WITH_JSON} BOOST_FS=${VIX_WITH_BOOST_FS} OPENSSL=${VIX_WITH_OPENSSL} SQLITE=${VIX_WITH_SQLITE} MYSQL=${VIX_WITH_MYSQL} HAS_ORM=${VIX_HAS_ORM} CACHE=${VIX_WITH_CACHE} P2P=${VIX_WITH_P2P}") +message(STATUS "Flags exported : JSON=${VIX_WITH_JSON} CONVERSION=${VIX_WITH_CONVERSION} VALIDATION=${VIX_WITH_VALIDATION} BOOST_FS=${VIX_WITH_BOOST_FS} OPENSSL=${VIX_WITH_OPENSSL} SQLITE=${VIX_WITH_SQLITE} MYSQL=${VIX_WITH_MYSQL} HAS_ORM=${VIX_HAS_ORM} CACHE=${VIX_WITH_CACHE} P2P=${VIX_WITH_P2P}") message(STATUS "------------------------------------------------------") diff --git a/examples/conversion/conversion_cli_config.cpp b/examples/conversion/conversion_cli_config.cpp new file mode 100644 index 0000000..e39b2c6 --- /dev/null +++ b/examples/conversion/conversion_cli_config.cpp @@ -0,0 +1,321 @@ +/** + * + * @file conversion_cli_config.cpp + * @author Gaspard Kirira + * + * Vix.cpp - Conversion Patterns (CLI + Config) (examples/conversion/) + * + * Goal: + * Real-world patterns you copy/paste: + * - parse CLI arguments like "--port=8080" or "--debug=true" + * - parse env-like strings ("PORT=8080") + * - parse optional values safely (fallbacks) + * - parse enum values with explicit mapping + * - print errors with code + position + input + * + * Notes: + * - This file is intentionally dependency-free (only vix::conversion + std). + * - No exceptions: errors are values (expected). + * - Beginner friendly, but the patterns scale to production CLIs. + * + * Vix.cpp + * + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace vix::conversion; + +// --------------------------------------------------------------------------- +// Error printing +// --------------------------------------------------------------------------- + +static void print_err(std::string_view label, const ConversionError &e) +{ + std::cout << label << " -> error: " << to_string(e.code) + << " position=" << e.position + << " input='" << e.input << "'\n"; +} + +// --------------------------------------------------------------------------- +// Simple helpers for parsing "--key=value" arguments +// --------------------------------------------------------------------------- + +static std::pair split_kv(std::string_view s, char sep) +{ + const std::size_t pos = s.find(sep); + if (pos == std::string_view::npos) + return {s, {}}; + return {s.substr(0, pos), s.substr(pos + 1)}; +} + +static bool starts_with(std::string_view s, std::string_view prefix) +{ + return s.size() >= prefix.size() && s.substr(0, prefix.size()) == prefix; +} + +// Normalize: +// "--port=8080" -> key="port", value="8080" +// "--debug" -> key="debug", value="" (treated as "true" by convention) +static std::pair parse_arg_kv(std::string_view arg) +{ + if (!starts_with(arg, "--")) + return {{}, {}}; + + arg.remove_prefix(2); + + auto [k, v] = split_kv(arg, '='); + return {k, v}; +} + +// --------------------------------------------------------------------------- +// A small "Config" struct, as you would do in a real CLI +// --------------------------------------------------------------------------- + +enum class Mode +{ + Dev, + Prod, +}; + +static constexpr EnumEntry modes[] = { + {"dev", Mode::Dev}, + {"prod", Mode::Prod}, +}; + +static const char *mode_name(Mode m) +{ + switch (m) + { + case Mode::Dev: + return "dev"; + case Mode::Prod: + return "prod"; + default: + return "unknown"; + } +} + +struct AppConfig +{ + int port{8080}; + bool debug{false}; + Mode mode{Mode::Dev}; + int workers{4}; + + void print() const + { + std::cout << "Config:\n"; + std::cout << " port = " << port << "\n"; + std::cout << " debug = " << (debug ? "true" : "false") << "\n"; + std::cout << " mode = " << mode_name(mode) << "\n"; + std::cout << " workers = " << workers << "\n"; + } +}; + +// --------------------------------------------------------------------------- +// Parsing patterns (beginner friendly) +// --------------------------------------------------------------------------- + +static bool parse_bool_flag(std::string_view raw, bool default_if_empty) +{ + if (raw.empty()) + return default_if_empty; + + auto r = parse(raw); + if (!r) + return default_if_empty; + + return r.value(); +} + +static int parse_int_or(std::string_view raw, int fallback) +{ + auto r = parse(raw); + if (!r) + return fallback; + return r.value(); +} + +// --------------------------------------------------------------------------- +// Apply CLI args into config (expert pattern but readable) +// --------------------------------------------------------------------------- + +static bool apply_cli_args(AppConfig &cfg, const std::vector &args) +{ + // Returns false if a fatal parsing error happened. + // In a real CLI you would accumulate errors; here we keep it simple and strict. + + for (auto a : args) + { + auto [key, value] = parse_arg_kv(a); + if (key.empty()) + continue; + + if (key == "port") + { + auto r = parse(value); + if (!r) + { + print_err("--port", r.error()); + return false; + } + cfg.port = r.value(); + } + else if (key == "debug") + { + // "--debug" alone => true + cfg.debug = parse_bool_flag(value, true); + } + else if (key == "workers") + { + auto r = parse(value); + if (!r) + { + print_err("--workers", r.error()); + return false; + } + cfg.workers = r.value(); + } + else if (key == "mode") + { + auto r = to_enum(value, modes); // case-insensitive default=true + if (!r) + { + print_err("--mode", r.error()); + return false; + } + cfg.mode = r.value(); + } + else + { + std::cout << "warning: unknown flag '--" << key << "'\n"; + } + } + + return true; +} + +// --------------------------------------------------------------------------- +// Parse "env-like" variables (PORT=..., DEBUG=..., MODE=..., WORKERS=...) +// --------------------------------------------------------------------------- + +static bool apply_env_map(AppConfig &cfg, const std::unordered_map &env) +{ + if (auto it = env.find("PORT"); it != env.end()) + { + auto r = parse(it->second); + if (!r) + { + print_err("ENV PORT", r.error()); + return false; + } + cfg.port = r.value(); + } + + if (auto it = env.find("DEBUG"); it != env.end()) + { + auto r = parse(it->second); + if (!r) + { + print_err("ENV DEBUG", r.error()); + return false; + } + cfg.debug = r.value(); + } + + if (auto it = env.find("WORKERS"); it != env.end()) + { + auto r = parse(it->second); + if (!r) + { + print_err("ENV WORKERS", r.error()); + return false; + } + cfg.workers = r.value(); + } + + if (auto it = env.find("MODE"); it != env.end()) + { + auto r = to_enum(it->second, modes); + if (!r) + { + print_err("ENV MODE", r.error()); + return false; + } + cfg.mode = r.value(); + } + + return true; +} + +// --------------------------------------------------------------------------- +// Demo runner +// --------------------------------------------------------------------------- + +static void run_demo(std::vector cli_args, + std::unordered_map env) +{ + std::cout << "Vix.cpp - Conversion CLI/Config Pattern Demo\n"; + + AppConfig cfg; + + std::cout << "\n== Defaults\n"; + cfg.print(); + + std::cout << "\n== Apply ENV\n"; + if (!apply_env_map(cfg, env)) + { + std::cout << "fatal: failed to parse env\n"; + return; + } + cfg.print(); + + std::cout << "\n== Apply CLI\n"; + if (!apply_cli_args(cfg, cli_args)) + { + std::cout << "fatal: failed to parse cli args\n"; + return; + } + cfg.print(); + + std::cout << "\nDone.\n"; +} + +int main() +{ + // Simulated CLI args: + // --port=9090 --debug --mode=prod --workers=8 + // + // Try breaking them to see errors: + // --port=12x + // --mode=unknown + // --workers=999999999999999999999 + std::vector cli = { + "--port=9090", + "--debug", + "--mode=prod", + "--workers=8", + }; + + // Simulated environment: + // These are typically strings in the real OS environment. + std::unordered_map env = { + {"PORT", "8081"}, + {"DEBUG", "false"}, + {"MODE", "dev"}, + {"WORKERS", "4"}, + }; + + run_demo(cli, env); + return 0; +} diff --git a/examples/conversion/parse_bool.cpp b/examples/conversion/parse_bool.cpp new file mode 100644 index 0000000..cc2854c --- /dev/null +++ b/examples/conversion/parse_bool.cpp @@ -0,0 +1,32 @@ +#include +#include + +#include + +using namespace vix::conversion; + +static void print_err(const ConversionError &e) +{ + std::cout << "error: " << to_string(e.code) + << " position=" << e.position + << " input='" << e.input << "'\n"; +} + +int main() +{ + for (std::string_view s : {"true", "FALSE", "1", "0", "yes", "off", "maybe"}) + { + auto r = to_bool(s); + + std::cout << "input='" << s << "' -> "; + if (!r) + { + print_err(r.error()); + continue; + } + + std::cout << "value=" << (r.value() ? "true" : "false") << "\n"; + } + + return 0; +} diff --git a/examples/conversion/parse_enum.cpp b/examples/conversion/parse_enum.cpp new file mode 100644 index 0000000..e5bfe7c --- /dev/null +++ b/examples/conversion/parse_enum.cpp @@ -0,0 +1,56 @@ +#include +#include + +#include + +using namespace vix::conversion; + +enum class Role +{ + Admin, + User, +}; + +static constexpr EnumEntry roles[] = { + {"admin", Role::Admin}, + {"user", Role::User}, +}; + +static void print_err(const ConversionError &e) +{ + std::cout << "error: " << to_string(e.code) + << " position=" << e.position + << " input='" << e.input << "'\n"; +} + +static const char *role_name(Role r) +{ + switch (r) + { + case Role::Admin: + return "Admin"; + case Role::User: + return "User"; + default: + return "Unknown"; + } +} + +int main() +{ + for (std::string_view s : {"admin", "USER", "guest"}) + { + auto r = to_enum(s, roles); // case-insensitive default=true + + std::cout << "input='" << s << "' -> "; + if (!r) + { + print_err(r.error()); + continue; + } + + std::cout << "value=" << role_name(r.value()) << "\n"; + } + + return 0; +} diff --git a/examples/conversion/parse_float.cpp b/examples/conversion/parse_float.cpp new file mode 100644 index 0000000..f19d6df --- /dev/null +++ b/examples/conversion/parse_float.cpp @@ -0,0 +1,32 @@ +#include +#include + +#include + +using namespace vix::conversion; + +static void print_err(const ConversionError &e) +{ + std::cout << "error: " << to_string(e.code) + << " position=" << e.position + << " input='" << e.input << "'\n"; +} + +int main() +{ + for (std::string_view s : {"3.14", " 1e-3 ", "abc", "1.2.3"}) + { + auto r = to_float(s); + + std::cout << "input='" << s << "' -> "; + if (!r) + { + print_err(r.error()); + continue; + } + + std::cout << "value=" << r.value() << "\n"; + } + + return 0; +} diff --git a/examples/conversion/parse_generic.cpp b/examples/conversion/parse_generic.cpp new file mode 100644 index 0000000..a137afb --- /dev/null +++ b/examples/conversion/parse_generic.cpp @@ -0,0 +1,45 @@ +#include +#include + +#include + +using namespace vix::conversion; + +static void print_err(const ConversionError &e) +{ + std::cout << "error: " << to_string(e.code) + << " position=" << e.position + << " input='" << e.input << "'\n"; +} + +int main() +{ + { + auto r = parse(" 123 "); + std::cout << "parse(' 123 ') -> "; + if (!r) + print_err(r.error()); + else + std::cout << r.value() << "\n"; + } + + { + auto r = parse(" 3.14 "); + std::cout << "parse(' 3.14 ') -> "; + if (!r) + print_err(r.error()); + else + std::cout << r.value() << "\n"; + } + + { + auto r = parse("yes"); + std::cout << "parse('yes') -> "; + if (!r) + print_err(r.error()); + else + std::cout << (r.value() ? "true" : "false") << "\n"; + } + + return 0; +} diff --git a/examples/conversion/parse_int.cpp b/examples/conversion/parse_int.cpp new file mode 100644 index 0000000..42f3112 --- /dev/null +++ b/examples/conversion/parse_int.cpp @@ -0,0 +1,32 @@ +#include +#include + +#include + +using namespace vix::conversion; + +static void print_err(const ConversionError &e) +{ + std::cout << "error: " << to_string(e.code) + << " position=" << e.position + << " input='" << e.input << "'\n"; +} + +int main() +{ + for (std::string_view s : {"42", " -7 ", "999999999999999999999", "12x"}) + { + auto r = to_int(s); + + std::cout << "input='" << s << "' -> "; + if (!r) + { + print_err(r.error()); + continue; + } + + std::cout << "value=" << r.value() << "\n"; + } + + return 0; +} diff --git a/examples/conversion/to_string.cpp b/examples/conversion/to_string.cpp new file mode 100644 index 0000000..a8582bb --- /dev/null +++ b/examples/conversion/to_string.cpp @@ -0,0 +1,64 @@ +#include + +#include + +using namespace vix::conversion; + +enum class Role +{ + Admin, + User, +}; + +static constexpr EnumEntry roles[] = { + {"admin", Role::Admin}, + {"user", Role::User}, +}; + +static void print_err(const ConversionError &e) +{ + std::cout << "error: " << to_string(e.code) + << " position=" << e.position + << " input='" << e.input << "'\n"; +} + +int main() +{ + { + auto r = to_string(42); + std::cout << "to_string(42) -> "; + if (!r) + print_err(r.error()); + else + std::cout << r.value() << "\n"; + } + + { + auto r = to_string(3.14); + std::cout << "to_string(3.14) -> "; + if (!r) + print_err(r.error()); + else + std::cout << r.value() << "\n"; + } + + { + auto r = to_string(true); + std::cout << "to_string(true) -> "; + if (!r) + print_err(r.error()); + else + std::cout << r.value() << "\n"; + } + + { + auto r = to_string(Role::Admin, roles); + std::cout << "to_string(Role::Admin) -> "; + if (!r) + print_err(r.error()); + else + std::cout << r.value() << "\n"; + } + + return 0; +} diff --git a/examples/conversion/vix_conversion_showcase.cpp b/examples/conversion/vix_conversion_showcase.cpp new file mode 100644 index 0000000..adf0b5b --- /dev/null +++ b/examples/conversion/vix_conversion_showcase.cpp @@ -0,0 +1,260 @@ +/** + * + * @file vix_conversion_showcase.cpp + * @author Gaspard Kirira + * + * Vix.cpp - Conversion Showcase (examples/conversion/) + * + * Goal: + * A single, self-contained file that showcases how conversions look in Vix.cpp: + * - parse int / float / bool / enum from string_view + * - generic parse + * - to_string for int / float / bool / enum + * - consistent error printing (code, position, input) + * - copy/paste friendly patterns for real projects (CLI, config, APIs) + * + * Notes: + * - This file focuses on the public conversion API style. + * - No exceptions, no iostream parsing, errors as values (expected). + * - main() contains no business logic: it only calls functions. + * + * Vix.cpp + * + */ + +// ============================================================================ +// QUICK MAP (console demo) +// ---------------------------------------------------------------------------- +// 1) integers: to_int +// 2) floats: to_float +// 3) bool: to_bool +// 4) enum: to_enum (mapping table) +// 5) generic: parse +// 6) to_string: int / float / bool / enum +// ============================================================================ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace vix::conversion; + +// --------------------------------------------------------------------------- +// Error helper +// --------------------------------------------------------------------------- + +static void print_err(std::string_view label, const ConversionError &e) +{ + std::cout << label << " -> error: " << to_string(e.code) + << " position=" << e.position + << " input='" << e.input << "'\n"; +} + +// --------------------------------------------------------------------------- +// Enum demo +// --------------------------------------------------------------------------- + +enum class Role +{ + Admin, + User, + Guest +}; + +static constexpr EnumEntry roles[] = { + {"admin", Role::Admin}, + {"user", Role::User}, + {"guest", Role::Guest}, +}; + +static const char *role_name(Role r) +{ + switch (r) + { + case Role::Admin: + return "Admin"; + case Role::User: + return "User"; + case Role::Guest: + return "Guest"; + default: + return "Unknown"; + } +} + +// --------------------------------------------------------------------------- +// Demo blocks +// --------------------------------------------------------------------------- + +static void demo_int() +{ + std::cout << "\n== to_int (integers)\n"; + for (std::string_view s : {"42", " -7 ", "999999999999999999999", "12x", ""}) + { + auto r = to_int(s); + + std::cout << "input='" << s << "' -> "; + if (!r) + { + print_err("to_int", r.error()); + continue; + } + + std::cout << "value=" << r.value() << "\n"; + } +} + +static void demo_float() +{ + std::cout << "\n== to_float (floats)\n"; + for (std::string_view s : {"3.14", " 1e-3 ", "abc", "1.2.3", ""}) + { + auto r = to_float(s); + + std::cout << "input='" << s << "' -> "; + if (!r) + { + print_err("to_float", r.error()); + continue; + } + + std::cout << "value=" << r.value() << "\n"; + } +} + +static void demo_bool() +{ + std::cout << "\n== to_bool (booleans)\n"; + for (std::string_view s : {"true", "FALSE", "1", "0", "yes", "off", "maybe", ""}) + { + auto r = to_bool(s); + + std::cout << "input='" << s << "' -> "; + if (!r) + { + print_err("to_bool", r.error()); + continue; + } + + std::cout << "value=" << (r.value() ? "true" : "false") << "\n"; + } +} + +static void demo_enum() +{ + std::cout << "\n== to_enum (enums)\n"; + for (std::string_view s : {"admin", "USER", "guest", "unknown", ""}) + { + auto r = to_enum(s, roles); // case-insensitive default=true + + std::cout << "input='" << s << "' -> "; + if (!r) + { + print_err("to_enum", r.error()); + continue; + } + + std::cout << "value=" << role_name(r.value()) << "\n"; + } +} + +static void demo_parse_generic() +{ + std::cout << "\n== parse (generic facade)\n"; + + { + auto r = parse(" 123 "); + std::cout << "parse(' 123 ') -> "; + if (!r) + print_err("parse", r.error()); + else + std::cout << r.value() << "\n"; + } + + { + auto r = parse(" 3.14 "); + std::cout << "parse(' 3.14 ') -> "; + if (!r) + print_err("parse", r.error()); + else + std::cout << r.value() << "\n"; + } + + { + auto r = parse("yes"); + std::cout << "parse('yes') -> "; + if (!r) + print_err("parse", r.error()); + else + std::cout << (r.value() ? "true" : "false") << "\n"; + } +} + +static void demo_to_string() +{ + std::cout << "\n== to_string (formatting)\n"; + + { + auto r = to_string(42); + std::cout << "to_string(42) -> "; + if (!r) + print_err("to_string(int)", r.error()); + else + std::cout << r.value() << "\n"; + } + + { + auto r = to_string(3.14); + std::cout << "to_string(3.14) -> "; + if (!r) + print_err("to_string(float)", r.error()); + else + std::cout << r.value() << "\n"; + } + + { + auto r = to_string(true); + std::cout << "to_string(true) -> "; + if (!r) + print_err("to_string(bool)", r.error()); + else + std::cout << r.value() << "\n"; + } + + { + auto r = to_string(Role::Admin, roles); + std::cout << "to_string(Role::Admin) -> "; + if (!r) + print_err("to_string(enum)", r.error()); + else + std::cout << r.value() << "\n"; + } +} + +// --------------------------------------------------------------------------- +// Bootstrap +// --------------------------------------------------------------------------- + +int main() +{ + std::cout << "Vix.cpp - Conversion Showcase\n"; + + demo_int(); + demo_float(); + demo_bool(); + demo_enum(); + demo_parse_generic(); + demo_to_string(); + + std::cout << "\nDone.\n"; + return 0; +} diff --git a/examples/json/builders.cpp b/examples/json/builders.cpp new file mode 100644 index 0000000..31ca58e --- /dev/null +++ b/examples/json/builders.cpp @@ -0,0 +1,72 @@ +/** + * + * @file builders.cpp + * @author Gaspard Kirira + * + * Copyright 2025, Gaspard Kirira. All rights reserved. + * https://github.com/vixcpp/vix + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Vix.cpp + * @brief Example usage of the Vix.cpp JSON module. + * + * This example demonstrates how to use the high-level JSON helpers + * provided by `vix/json/json.hpp`, including: + * - `o()` — for building JSON objects from key/value pairs. + * - `a()` — for building arrays. + * - `kv()` — for constructing an object from initializer pairs. + * - `dumps()` — for pretty-printing JSON to a string. + * + * These helpers wrap `nlohmann::json` to make JSON creation + * and formatting much more expressive and concise. + * + * ### Example Output + * ``` + * { + * "id": 42, + * "name": "Ada", + * "tags": [ + * "pro", + * "admin" + * ] + * } + * { + * "host": "localhost", + * "port": 8080 + * } + * ``` + * + * @see vix::json::o + * @see vix::json::a + * @see vix::json::kv + * @see vix::json::dumps + */ + +#include +#include + +int main() +{ + using namespace vix::json; + + // --------------------------------------------------------------------- + // Build a JSON object using `o()` and an array using `a()` + // --------------------------------------------------------------------- + auto user = o( + "id", 42, + "name", "Ada", + "tags", a("pro", "admin")); + + // --------------------------------------------------------------------- + // Build another JSON object using `kv()` (initializer list syntax) + // --------------------------------------------------------------------- + auto conf = kv({{"host", "localhost"}, + {"port", 8080}}); + + // --------------------------------------------------------------------- + // Pretty-print both JSON objects with indentation + // --------------------------------------------------------------------- + std::cout << dumps(user, 2) << "\n" + << dumps(conf, 2) << "\n"; +} diff --git a/examples/json/io.cpp b/examples/json/io.cpp new file mode 100644 index 0000000..497e543 --- /dev/null +++ b/examples/json/io.cpp @@ -0,0 +1,67 @@ +/** + * + * @file io.cpp + * @author Gaspard Kirira + * + * Copyright 2025, Gaspard Kirira. All rights reserved. + * https://github.com/vixcpp/vix + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Vix.cpp + * @brief Demonstrates parsing, writing, and reloading JSON with Vix.cpp helpers. + * + * This example shows how to: + * - Parse a JSON string with `loads()`. + * - Write it safely to disk using `dump_file()` (atomic operation). + * - Reload the same file using `load_file()`. + * - Print the formatted JSON using `dumps()`. + * + * The `dump_file()` function writes to a temporary file (with `.tmp` suffix) + * before renaming it atomically to prevent corruption — making it suitable + * for configuration storage, caching, or transactional file operations. + * + * ### Example Output + * ``` + * { + * "a": 1, + * "b": [ + * 10, + * 20 + * ] + * } + * ``` + * + * @see vix::json::loads + * @see vix::json::dump_file + * @see vix::json::load_file + * @see vix::json::dumps + */ + +#include +#include + +int main() +{ + using namespace vix::json; + + // --------------------------------------------------------------------- + // Parse a JSON string into a Json object + // --------------------------------------------------------------------- + auto j = loads(R"({"a":1,"b":[10,20]})"); + + // --------------------------------------------------------------------- + // Write JSON to disk safely (atomic write using .tmp) + // --------------------------------------------------------------------- + dump_file("out.json", j, 2); + + // --------------------------------------------------------------------- + // Read the same file back into a new Json object + // --------------------------------------------------------------------- + auto j2 = load_file("out.json"); + + // --------------------------------------------------------------------- + // Pretty-print the reloaded JSON to stdout + // --------------------------------------------------------------------- + std::cout << dumps(j2, 2) << "\n"; +} diff --git a/examples/json/jpath.cpp b/examples/json/jpath.cpp new file mode 100644 index 0000000..55d375d --- /dev/null +++ b/examples/json/jpath.cpp @@ -0,0 +1,81 @@ +/** + * + * @file jpath.cpp + * @author Gaspard Kirira + * + * Copyright 2025, Gaspard Kirira. All rights reserved. + * https://github.com/vixcpp/vix + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Vix.cpp + * @brief Demonstrates JSON navigation and mutation with JPath helpers. + * + * This example showcases how to use `jset()` and `jget()` to manipulate + * deeply nested JSON structures via a human-readable *JPath* syntax. + * + * The JPath system supports: + * - Dot notation (`user.profile.name`) + * - Array indexing (`user.langs[2]`) + * - Quoted keys for special characters (`user["display.name"]`) + * + * Missing intermediate objects and arrays are automatically created by `jset()`. + * Invalid paths return `nullptr` when accessed via `jget()`. + * + * ### Example Output + * ``` + * cpp + * { + * "user": { + * "display.name": "Ada L.", + * "langs": [ + * null, + * null, + * "cpp" + * ], + * "profile": { + * "name": "Gaspard" + * } + * } + * } + * ``` + * + * @see vix::json::jget + * @see vix::json::jset + * @see vix::json::tokenize_path + * @see vix::json::dumps + */ + +#include +#include + +int main() +{ + using namespace vix::json; + + // --------------------------------------------------------------------- + // Start with an empty JSON object + // --------------------------------------------------------------------- + Json j = obj(); + + // --------------------------------------------------------------------- + // Use jset() to assign values via JPath + // Automatically creates missing objects/arrays + // --------------------------------------------------------------------- + jset(j, "user.langs[2]", "cpp"); // -> [null, null, "cpp"] + jset(j, "user.profile.name", "Gaspard"); // creates nested object + jset(j, R"(user["display.name"])", "Ada L."); // handles quoted keys + + // --------------------------------------------------------------------- + // Access nested values via jget() + // --------------------------------------------------------------------- + if (auto v = jget(j, "user.langs[2]")) + { + std::cout << v->get() << "\n"; // cpp + } + + // --------------------------------------------------------------------- + // Pretty-print the resulting JSON + // --------------------------------------------------------------------- + std::cout << dumps(j, 2) << "\n"; +} diff --git a/examples/json/json_simple/01_basic_values.cpp b/examples/json/json_simple/01_basic_values.cpp new file mode 100644 index 0000000..8189336 --- /dev/null +++ b/examples/json/json_simple/01_basic_values.cpp @@ -0,0 +1,21 @@ +#include +#include + +using namespace vix::json; + +int main() +{ + token t1; // null + token t2 = true; // bool + token t3 = 42; // int -> int64 + token t4 = 3.14; // double + token t5 = "hello"; // string + + std::cout << "is_null=" << t1.is_null() << "\n"; + std::cout << "bool=" << t2.as_bool_or(false) << "\n"; + std::cout << "int64=" << t3.as_i64_or(-1) << "\n"; + std::cout << "double=" << t4.as_f64_or(0.0) << "\n"; + std::cout << "string=" << t5.as_string_or("empty") << "\n"; + + return 0; +} diff --git a/examples/json/json_simple/02_arrays.cpp b/examples/json/json_simple/02_arrays.cpp new file mode 100644 index 0000000..095272a --- /dev/null +++ b/examples/json/json_simple/02_arrays.cpp @@ -0,0 +1,27 @@ +#include +#include + +using namespace vix::json; + +int main() +{ + array_t arr; + + arr.push_int(1); + arr.push_int(2); + arr.push_string("three"); + arr.push_bool(true); + + arr.reserve(10); + arr.ensure(6).set_string("auto-filled"); + + for (std::size_t i = 0; i < arr.size(); ++i) + { + if (arr[i].is_string()) + std::cout << "string: " << arr[i].as_string_or("") << "\n"; + else if (arr[i].is_i64()) + std::cout << "int: " << arr[i].as_i64_or(0) << "\n"; + } + + return 0; +} diff --git a/examples/json/json_simple/03_objects.cpp b/examples/json/json_simple/03_objects.cpp new file mode 100644 index 0000000..76d5418 --- /dev/null +++ b/examples/json/json_simple/03_objects.cpp @@ -0,0 +1,19 @@ +#include +#include + +using namespace vix::json; + +int main() +{ + kvs user; + + user.set_string("name", "Alice"); + user.set_int("age", 30); + user.set_bool("active", true); + + std::cout << user.get_string_or("name", "unknown") << "\n"; + std::cout << user.get_i64_or("age", 0) << "\n"; + std::cout << user.get_bool_or("active", false) << "\n"; + + return 0; +} diff --git a/examples/json/json_simple/04_nested.cpp b/examples/json/json_simple/04_nested.cpp new file mode 100644 index 0000000..90a35fb --- /dev/null +++ b/examples/json/json_simple/04_nested.cpp @@ -0,0 +1,23 @@ +#include +#include + +using namespace vix::json; + +int main() +{ + kvs root; + + kvs &user = root.ensure_object("user"); + user.set_string("name", "Gaspard"); + user.set_int("id", 42); + + array_t &skills = user.ensure_array("skills"); + skills.push_string("C++"); + skills.push_string("P2P"); + skills.push_string("Systems"); + + std::cout << user.get_string_or("name", "") << "\n"; + std::cout << skills[0].as_string_or("") << "\n"; + + return 0; +} diff --git a/examples/json/json_simple/05_mutation.cpp b/examples/json/json_simple/05_mutation.cpp new file mode 100644 index 0000000..839008d --- /dev/null +++ b/examples/json/json_simple/05_mutation.cpp @@ -0,0 +1,26 @@ +#include +#include + +using namespace vix::json; + +int main() +{ + token root; + + // Transforme dynamiquement en objet + kvs &obj = root.ensure_object(); + obj.set_string("mode", "dev"); + + // Puis en array + array_t &logs = obj.ensure_array("logs"); + logs.push_string("boot"); + logs.push_string("init"); + logs.push_string("ready"); + + logs.erase_at(1); + + for (const auto &t : logs) + std::cout << t.as_string_or("") << "\n"; + + return 0; +} diff --git a/examples/json/json_simple/06_iteration.cpp b/examples/json/json_simple/06_iteration.cpp new file mode 100644 index 0000000..4cb83d8 --- /dev/null +++ b/examples/json/json_simple/06_iteration.cpp @@ -0,0 +1,24 @@ +#include +#include + +using namespace vix::json; + +int main() +{ + kvs cfg = obj({"host", "localhost", + "port", 8080, + "secure", false}); + + cfg.for_each_pair([](std::string_view key, const token &value) + { + std::cout << key << " -> "; + if (value.is_string()) + std::cout << value.as_string_or(""); + else if (value.is_i64()) + std::cout << value.as_i64_or(0); + else if (value.is_bool()) + std::cout << value.as_bool_or(false); + std::cout << "\n"; }); + + return 0; +} diff --git a/examples/json/json_simple/07_merge_and_erase.cpp b/examples/json/json_simple/07_merge_and_erase.cpp new file mode 100644 index 0000000..9940f8c --- /dev/null +++ b/examples/json/json_simple/07_merge_and_erase.cpp @@ -0,0 +1,22 @@ +#include +#include + +using namespace vix::json; + +int main() +{ + kvs base = obj({"env", "prod", + "debug", false}); + + kvs override = obj({"debug", true, + "trace", true}); + + base.merge_from(override, true); + + base.erase("env"); + + for (auto &k : base.keys()) + std::cout << "key=" << k << "\n"; + + return 0; +} diff --git a/examples/json/json_simple/08_to_nlohmann_json.cpp b/examples/json/json_simple/08_to_nlohmann_json.cpp new file mode 100644 index 0000000..a61f2d7 --- /dev/null +++ b/examples/json/json_simple/08_to_nlohmann_json.cpp @@ -0,0 +1,111 @@ +/** + * + * @file 08_to_nlohmann_json.cpp + * @author Gaspard Kirira + * + * Copyright 2025, Gaspard Kirira. + * All rights reserved. + * https://github.com/vixcpp/vix + * + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Vix.cpp + */ + +#include + +#include +#include // to_json(Simple) -> Json +#include // vix::json::Json + +namespace +{ + void example_basic_object() + { + using namespace vix::json; + + kvs user = obj({ + "name", + "Alice", + "age", + 30, + "ok", + true, + "score", + 12.5, + "skills", + array({"C++", "P2P", "Offline-first"}), + }); + + Json j = to_json(user); + + std::cout << "\n[example_basic_object]\n"; + std::cout << j.dump(2) << "\n"; + } + + void example_nested() + { + using namespace vix::json; + + kvs root = obj({ + "app", + "Vix.cpp", + "meta", + obj({ + "country", + "UG", + "version", + "1.0.0", + "features", + array({"http", "ws", "p2p"}), + }), + "users", + array({ + obj({"id", 1, "name", "Ada"}), + obj({"id", 2, "name", "Gaspard"}), + }), + }); + + token t = root; // token root holding object + + Json j = to_json(t); + + std::cout << "\n[example_nested]\n"; + std::cout << j.dump(2) << "\n"; + } + + void example_roundtrip_style_usage() + { + using namespace vix::json; + + kvs payload = obj({ + "type", + "notification", + "data", + obj({ + "title", + "Build OK", + "code", + 200, + "tags", + array({"ci", "release", "vix"}), + }), + }); + + Json j = to_json(payload); + + std::cout << "\n[example_roundtrip_style_usage]\n"; + std::cout << "type=" << j.value("type", "missing") << "\n"; + std::cout << j.dump(2) << "\n"; + } + +} // namespace + +int main() +{ + example_basic_object(); + example_nested(); + example_roundtrip_style_usage(); + return 0; +} diff --git a/examples/json/json_simple/README.md b/examples/json/json_simple/README.md new file mode 100644 index 0000000..f5881b4 --- /dev/null +++ b/examples/json/json_simple/README.md @@ -0,0 +1,21 @@ +# Simple.json examples + +These examples demonstrate the usage of `vix::json::Simple`, +a lightweight in-memory JSON-like model. + +## When to use Simple.json +- Internal data exchange +- No JSON parsing/serialization needed +- Predictable memory layout +- Cheap copies and mutations + +## Files +- `01_basic_values.cpp` – primitive values +- `02_arrays.cpp` – array container API +- `03_objects.cpp` – object container API +- `04_nested.cpp` – nested structures +- `05_mutation.cpp` – dynamic mutation +- `06_iteration.cpp` – iteration helpers +- `07_merge_and_erase.cpp` – advanced object ops + +If you need parsing or dumping JSON text, use `` instead. diff --git a/examples/json/json_simple/bench_simple_vs_nlohmann.cpp b/examples/json/json_simple/bench_simple_vs_nlohmann.cpp new file mode 100644 index 0000000..39b4b16 --- /dev/null +++ b/examples/json/json_simple/bench_simple_vs_nlohmann.cpp @@ -0,0 +1,322 @@ +/** + * + * @file bench_simple_vs_nlohmann.cpp + * @author Gaspard Kirira + * + * Copyright 2025, Gaspard Kirira. + * All rights reserved. + * https://github.com/vixcpp/vix + * + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Vix.cpp + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace +{ + using Clock = std::chrono::steady_clock; + + static volatile std::uint64_t g_sink_u64 = 0; + static volatile std::size_t g_sink_sz = 0; + + inline std::uint64_t hash64(std::uint64_t x) + { + x ^= x >> 33; + x *= 0xff51afd7ed558ccdULL; + x ^= x >> 33; + x *= 0xc4ceb9fe1a85ec53ULL; + x ^= x >> 33; + return x; + } + + inline void sink_u64(std::uint64_t x) + { + g_sink_u64 += hash64(x + 0x9e3779b97f4a7c15ULL); + } + + inline void sink_sz(std::size_t x) + { + g_sink_sz += static_cast( + hash64(static_cast(x) + 0x9e3779b97f4a7c15ULL)); + } + + struct Timer + { + std::string name; + Clock::time_point t0; + + explicit Timer(std::string n) : name(std::move(n)), t0(Clock::now()) {} + + ~Timer() + { + const auto t1 = Clock::now(); + const auto us = + std::chrono::duration_cast(t1 - t0).count(); + std::cout << name << ": " << us << " us\n"; + } + }; + + // Build a fixed payload size to stress arrays and objects. + // This keeps the comparison fair: same logical structure. + inline vix::json::kvs build_simple_payload(int idx, int items) + { + using namespace vix::json; + + array_t tags; + tags.reserve(static_cast(items)); + for (int i = 0; i < items; ++i) + tags.push_string("tag_" + std::to_string(i)); + + array_t nums; + nums.reserve(static_cast(items)); + for (int i = 0; i < items; ++i) + nums.push_i64(static_cast(idx * 1000 + i)); + + kvs meta = obj({ + "version", + "1.0.0", + "country", + "UG", + "active", + true, + "score", + 12.5, + }); + + kvs user = obj({ + "id", + idx, + "name", + std::string("user_") + std::to_string(idx), + "tags", + tags, + "nums", + nums, + "meta", + meta, + }); + + kvs root = obj({ + "type", + "bench", + "user", + user, + "ok", + true, + }); + + return root; + } + + inline vix::json::Json build_nlohmann_payload(int idx, int items) + { + using namespace vix::json; + + Json tags = Json::array(); + for (int i = 0; i < items; ++i) + tags.push_back(std::string("tag_") + std::to_string(i)); + + Json nums = Json::array(); + for (int i = 0; i < items; ++i) + nums.push_back(static_cast(idx * 1000 + i)); + + Json meta = Json::object(); + meta["version"] = "1.0.0"; + meta["country"] = "UG"; + meta["active"] = true; + meta["score"] = 12.5; + + Json user = Json::object(); + user["id"] = idx; + user["name"] = std::string("user_") + std::to_string(idx); + user["tags"] = std::move(tags); + user["nums"] = std::move(nums); + user["meta"] = std::move(meta); + + Json root = Json::object(); + root["type"] = "bench"; + root["user"] = std::move(user); + root["ok"] = true; + + return root; + } + + inline void bench_build_simple(int iters, int items) + { + using namespace vix::json; + + Timer t("build Simple"); + std::uint64_t acc = 0; + + for (int i = 0; i < iters; ++i) + { + kvs s = build_simple_payload(i, items); + + const token *u = s.get_ptr("user"); + if (u && u->is_object()) + { + auto up = u->as_object_ptr(); + if (up) + { + const auto id = up->get_i64_or("id", -1); + acc ^= hash64(static_cast(id)); + } + } + } + + sink_u64(acc); + } + + inline void bench_build_nlohmann(int iters, int items) + { + using namespace vix::json; + + Timer t("build nlohmann::json"); + std::uint64_t acc = 0; + + for (int i = 0; i < iters; ++i) + { + Json j = build_nlohmann_payload(i, items); + + if (j.contains("user") && j["user"].contains("id")) + { + const auto id = j["user"]["id"].get(); + acc ^= hash64(static_cast(id)); + } + } + + sink_u64(acc); + } + + inline void bench_convert_simple_to_json(int iters, int items) + { + using namespace vix::json; + + Timer t("convert Simple -> Json"); + std::uint64_t acc = 0; + + for (int i = 0; i < iters; ++i) + { + kvs s = build_simple_payload(i, items); + Json j = to_json(s); + + if (j.contains("user") && j["user"].contains("name")) + { + const auto &name = j["user"]["name"].get_ref(); + acc ^= hash64(static_cast(name.size())); + } + } + + sink_u64(acc); + } + + inline void bench_dump_from_simple(int iters, int items) + { + using namespace vix::json; + + Timer t("dump from Simple (Simple -> Json -> dump)"); + std::size_t acc = 0; + + for (int i = 0; i < iters; ++i) + { + kvs s = build_simple_payload(i, items); + Json j = to_json(s); + std::string out = j.dump(); + acc ^= out.size(); + } + + sink_sz(acc); + } + + inline void bench_dump_from_nlohmann(int iters, int items) + { + using namespace vix::json; + + Timer t("dump from nlohmann::json (build -> dump)"); + std::size_t acc = 0; + + for (int i = 0; i < iters; ++i) + { + Json j = build_nlohmann_payload(i, items); + std::string out = j.dump(); + acc ^= out.size(); + } + + sink_sz(acc); + } + + inline void bench_dump_only_json(int iters, int items) + { + using namespace vix::json; + + const int pool_n = std::max(1, iters / 10); + + std::vector pool; + pool.reserve(static_cast(pool_n)); + for (int i = 0; i < pool_n; ++i) + pool.push_back(build_nlohmann_payload(i, items)); + + Timer t("dump only (prebuilt nlohmann::json)"); + std::size_t acc = 0; + + for (int i = 0; i < iters; ++i) + { + const Json &j = pool[static_cast(i) % pool.size()]; + std::string out = j.dump(); + acc ^= out.size(); + } + + sink_sz(acc); + } + + inline void run_all(int iters, int items) + { + bench_build_simple(100, 16); + bench_build_nlohmann(100, 16); + bench_convert_simple_to_json(100, 16); + bench_dump_from_simple(50, 16); + bench_dump_from_nlohmann(50, 16); + bench_dump_only_json(50, 16); + + std::cout << "\n--- bench (iters=" << iters << ", items=" << items << ") ---\n"; + bench_build_simple(iters, items); + bench_build_nlohmann(iters, items); + bench_convert_simple_to_json(iters, items); + bench_dump_from_simple(iters, items); + bench_dump_from_nlohmann(iters, items); + bench_dump_only_json(iters, items); + + std::cout << "\n(sink_u64=0x" << std::hex << g_sink_u64 + << ", sink_sz=0x" << g_sink_sz << std::dec << ")\n"; + } + +} // namespace + +int main(int argc, char **argv) +{ + int iters = 2000; + int items = 64; + + if (argc >= 2) + iters = std::max(1, std::atoi(argv[1])); + if (argc >= 3) + items = std::max(1, std::atoi(argv[2])); + + run_all(iters, items); + return 0; +} diff --git a/examples/json/quick_start.cpp b/examples/json/quick_start.cpp new file mode 100644 index 0000000..08ba824 --- /dev/null +++ b/examples/json/quick_start.cpp @@ -0,0 +1,59 @@ +/** + * + * @file quick_start.cpp + * @author Gaspard Kirira + * + * Copyright 2025, Gaspard Kirira. All rights reserved. + * https://github.com/vixcpp/vix + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Vix.cpp + * @brief Demonstrates JSON object and array creation with Vix.cpp JSON builders. + * + * This example shows how to use the high-level helper functions: + * - `o()` to build JSON objects using key/value pairs. + * - `a()` to build JSON arrays. + * - `dumps()` to pretty-print JSON with indentation. + * + * The Vix.cpp JSON builder syntax provides a clean, functional way + * to construct JSON documents in C++ without verbose object creation. + * + * ### Example Output + * ``` + * { + * "arr": [ + * 1, + * 2, + * 3 + * ], + * "count": 3, + * "message": "Hello" + * } + * ``` + * + * @see vix::json::o + * @see vix::json::a + * @see vix::json::dumps + */ + +#include +#include + +int main() +{ + using namespace vix::json; + + // --------------------------------------------------------------------- + // Build a JSON object and array using concise builder syntax + // --------------------------------------------------------------------- + auto j = o( + "message", "Hello", + "count", 3, + "arr", a(1, 2, 3)); + + // --------------------------------------------------------------------- + // Pretty-print the JSON result + // --------------------------------------------------------------------- + std::cout << dumps(j, 2) << "\n"; +} diff --git a/examples/validation/basemodel_basic.cpp b/examples/validation/basemodel_basic.cpp new file mode 100644 index 0000000..5986f30 --- /dev/null +++ b/examples/validation/basemodel_basic.cpp @@ -0,0 +1,51 @@ +#include +#include + +#include + +struct RegisterForm : vix::validation::BaseModel +{ + std::string email; + std::string password; + + static vix::validation::Schema schema() + { + return vix::validation::schema() + .field("email", &RegisterForm::email, + vix::validation::field() + .required() + .email() + .length_max(120)) + .field("password", &RegisterForm::password, + vix::validation::field() + .required() + .length_min(8) + .length_max(64)); + } +}; + +int main() +{ + RegisterForm f; + f.email = "bad-email"; + f.password = "123"; + + auto r = f.validate(); + + std::cout << "ok=" << static_cast(r.ok()) << "\n"; + std::cout << "is_valid=" << static_cast(f.is_valid()) << "\n"; + + if (!r.ok()) + { + for (const auto &e : r.errors.all()) + { + std::cout << " - field=" << e.field + << " code=" << vix::validation::to_string(e.code) + << " message=" << e.message << "\n"; + } + return 1; + } + + std::cout << "valid form\n"; + return 0; +} diff --git a/examples/validation/basemodel_cross_field_check.cpp b/examples/validation/basemodel_cross_field_check.cpp new file mode 100644 index 0000000..881cf9a --- /dev/null +++ b/examples/validation/basemodel_cross_field_check.cpp @@ -0,0 +1,59 @@ +#include +#include + +#include +#include +#include + +struct ResetPassword : vix::validation::BaseModel +{ + std::string password; + std::string confirm; + + static vix::validation::Schema schema() + { + return vix::validation::schema() + .field("password", &ResetPassword::password, + vix::validation::field() + .required() + .length_min(8) + .length_max(64)) + .field("confirm", &ResetPassword::confirm, + vix::validation::field() + .required() + .length_min(8) + .length_max(64)) + .check([](const ResetPassword &obj, vix::validation::ValidationErrors &errors) + { + if (!obj.password.empty() && !obj.confirm.empty() && obj.password != obj.confirm) + { + errors.add("confirm", + vix::validation::ValidationErrorCode::Custom, + "passwords do not match"); + } }); + } +}; + +int main() +{ + ResetPassword f; + f.password = "password123"; + f.confirm = "password124"; + + auto r = f.validate(); + + std::cout << "ok=" << static_cast(r.ok()) << "\n"; + if (!r.ok()) + { + for (const auto &e : r.errors.all()) + { + std::cout << " - field=" << e.field + << " code=" << vix::validation::to_string(e.code) + << " message=" << e.message << "\n"; + } + return 1; + } + + std::cout << "valid reset\n"; + return 0; +} diff --git a/examples/validation/basemodel_static_validate.cpp b/examples/validation/basemodel_static_validate.cpp new file mode 100644 index 0000000..5bcaeee --- /dev/null +++ b/examples/validation/basemodel_static_validate.cpp @@ -0,0 +1,54 @@ +#include +#include + +#include + +struct ProductInput : vix::validation::BaseModel +{ + std::string title; + std::string currency; + + static vix::validation::Schema schema() + { + return vix::validation::schema() + .field("title", &ProductInput::title, + vix::validation::field() + .required() + .length_min(3) + .length_max(80)) + .field("currency", &ProductInput::currency, + vix::validation::field() + .required() + .in_set({"USD", "EUR", "UGX"}, "currency must be USD/EUR/UGX")); + } +}; + +int main() +{ + ProductInput p; + p.title = "TV"; + p.currency = "BTC"; + + // Static validation (no need to call p.validate()) + auto r = ProductInput::validate(p); + + std::cout << "ok=" << static_cast(r.ok()) << "\n"; + + // Cached schema access + const auto &sc = ProductInput::schema(); + (void)sc; + + if (!r.ok()) + { + for (const auto &e : r.errors.all()) + { + std::cout << " - field=" << e.field + << " code=" << vix::validation::to_string(e.code) + << " message=" << e.message << "\n"; + } + return 1; + } + + std::cout << "valid product input\n"; + return 0; +} diff --git a/examples/validation/form_bind2_generic_error.cpp b/examples/validation/form_bind2_generic_error.cpp new file mode 100644 index 0000000..3e25efc --- /dev/null +++ b/examples/validation/form_bind2_generic_error.cpp @@ -0,0 +1,56 @@ +#include +#include +#include +#include +#include + +#include +#include + +struct SimpleForm +{ + std::string email; + + static bool bind( + SimpleForm &out, + const std::vector> &in) + { + for (const auto &kv : in) + { + if (kv.first == "email") + { + out.email.assign(kv.second); + return true; + } + } + return false; + } + + static vix::validation::Schema schema() + { + return vix::validation::schema() + .field("email", &SimpleForm::email, + [](std::string_view f, const std::string &v) + { + return vix::validation::validate(f, v).email(); + }); + } +}; + +int main() +{ + using Input = std::vector>; + + Input in = { + {"x", "y"}, + }; + + auto r = vix::validation::Form::validate(in); + + std::cout << "ok=" << static_cast(r) << "\n"; + if (!r) + { + for (const auto &e : r.errors().all()) + std::cout << " - field=" << e.field << " message=" << e.message << "\n"; + } +} diff --git a/examples/validation/form_cleaned_output.cpp b/examples/validation/form_cleaned_output.cpp new file mode 100644 index 0000000..dea2ac6 --- /dev/null +++ b/examples/validation/form_cleaned_output.cpp @@ -0,0 +1,96 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +struct UserClean +{ + std::string email; + int age{0}; +}; + +struct RegisterForm +{ + using cleaned_type = UserClean; + + std::string email; + std::string age; + + static bool bind( + RegisterForm &out, + const std::vector> &in, + vix::validation::ValidationErrors &errors) + { + auto get = [&](std::string_view key) -> std::string_view + { + for (const auto &kv : in) + { + if (kv.first == key) + return kv.second; + } + return {}; + }; + + auto email = get("email"); + if (email.empty()) + errors.add({"email", vix::validation::ValidationErrorCode::Required, "email is required"}); + else + out.email.assign(email); + + auto age = get("age"); + if (age.empty()) + errors.add({"age", vix::validation::ValidationErrorCode::Required, "age is required"}); + else + out.age.assign(age); + + return errors.size() == 0; + } + + static vix::validation::Schema schema() + { + return vix::validation::schema() + .field("email", &RegisterForm::email, + [](std::string_view f, const std::string &v) + { + return vix::validation::validate(f, v).email(); + }) + .parsed("age", &RegisterForm::age, + [](std::string_view f, std::string_view sv) + { + return vix::validation::validate_parsed(f, sv) + .between(18, 120) + .result("age must be a number"); + }); + } + + UserClean clean() const + { + return UserClean{email, std::stoi(age)}; + } +}; + +int main() +{ + using Input = std::vector>; + + Input in = { + {"email", "john@doe.com"}, + {"age", "25"}, + }; + + auto r = vix::validation::Form::validate(in); + if (!r) + { + for (const auto &e : r.errors().all()) + std::cout << " - " << e.field << ": " << e.message << "\n"; + return 1; + } + + const auto &u = r.value(); + std::cout << "email=" << u.email << " age=" << u.age << "\n"; +} diff --git a/examples/validation/form_kv_basic.cpp b/examples/validation/form_kv_basic.cpp new file mode 100644 index 0000000..a264117 --- /dev/null +++ b/examples/validation/form_kv_basic.cpp @@ -0,0 +1,92 @@ +#include +#include +#include +#include +#include + +#include +#include + +struct RegisterForm +{ + std::string email; + std::string password; + + static bool bind( + RegisterForm &out, + const std::vector> &in, + vix::validation::ValidationErrors &errors) + { + auto get = [&](std::string_view key) -> std::string_view + { + for (const auto &kv : in) + { + if (kv.first == key) + return kv.second; + } + return {}; + }; + + auto email = get("email"); + if (email.empty()) + { + errors.add({"email", vix::validation::ValidationErrorCode::Required, "email is required"}); + } + else + { + out.email.assign(email); + } + + auto pass = get("password"); + if (pass.empty()) + { + errors.add({"password", vix::validation::ValidationErrorCode::Required, "password is required"}); + } + else + { + out.password.assign(pass); + } + + return errors.size() == 0; + } + + static vix::validation::Schema schema() + { + return vix::validation::schema() + .field("email", &RegisterForm::email, + [](std::string_view f, const std::string &v) + { + return vix::validation::validate(f, v).email(); + }) + .field("password", &RegisterForm::password, + [](std::string_view f, const std::string &v) + { + return vix::validation::validate(f, v).length_min(8); + }); + } +}; + +int main() +{ + using Input = std::vector>; + + Input in = { + {"email", "bad-email"}, + {"password", "123"}, + }; + + auto r = vix::validation::Form::validate(in); + + std::cout << "ok=" << static_cast(r) << "\n"; + if (!r) + { + for (const auto &e : r.errors().all()) + { + std::cout << " - field=" << e.field << " message=" << e.message << "\n"; + } + return 1; + } + + std::cout << "email=" << r.value().email << "\n"; + return 0; +} diff --git a/examples/validation/form_parsed_age.cpp b/examples/validation/form_parsed_age.cpp new file mode 100644 index 0000000..6f12042 --- /dev/null +++ b/examples/validation/form_parsed_age.cpp @@ -0,0 +1,83 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +struct RegisterForm +{ + std::string email; + std::string age; // raw input + + static bool bind( + RegisterForm &out, + const std::vector> &in, + vix::validation::ValidationErrors &errors) + { + auto get = [&](std::string_view key) -> std::string_view + { + for (const auto &kv : in) + { + if (kv.first == key) + return kv.second; + } + return {}; + }; + + auto email = get("email"); + if (email.empty()) + errors.add({"email", vix::validation::ValidationErrorCode::Required, "email is required"}); + else + out.email.assign(email); + + auto age = get("age"); + if (age.empty()) + errors.add({"age", vix::validation::ValidationErrorCode::Required, "age is required"}); + else + out.age.assign(age); + + return errors.size() == 0; + } + + static vix::validation::Schema schema() + { + return vix::validation::schema() + .field("email", &RegisterForm::email, + [](std::string_view f, const std::string &v) + { + return vix::validation::validate(f, v).email(); + }) + .parsed("age", &RegisterForm::age, + [](std::string_view f, std::string_view sv) + { + return vix::validation::validate_parsed(f, sv) + .between(18, 120) + .result("age must be a number"); + }); + } +}; + +int main() +{ + using Input = std::vector>; + + Input in = { + {"email", "john@doe.com"}, + {"age", "abc"}, + }; + + auto r = vix::validation::Form::validate(in); + + std::cout << "ok=" << static_cast(r) << "\n"; + if (!r) + { + for (const auto &e : r.errors().all()) + { + std::cout << " - field=" << e.field << " message=" << e.message << "\n"; + } + } +} diff --git a/examples/validation/http/vix_validation_showcase.cpp b/examples/validation/http/vix_validation_showcase.cpp new file mode 100644 index 0000000..a6e8268 --- /dev/null +++ b/examples/validation/http/vix_validation_showcase.cpp @@ -0,0 +1,443 @@ +/** + * + * @file vix_validation_showcase.cpp + * @author Gaspard Kirira + * + * Vix.cpp - Validation Showcase (examples/validation/) + * + * Goal: + * One self-contained file showing the validation API style: + * - query params validation + * - path params + conversion + * - JSON body validation + * - Schema with FieldSpec (no lambdas) + * - Schema with lambdas (expert) + * - cross-field checks + * - BaseModel (CRTP) + * - Form (bind + schema + errors) + * - consistent HTTP 400 JSON error shape + * + * Notes: + * - copy/paste friendly + * - main() has no business logic + * + * Vix.cpp + * + */ + +// ============================================================================ +// QUICK MAP +// ---------------------------------------------------------------------------- +// GET /validation/health +// GET /validation/query/register?email=..&password=..&confirm=.. +// GET /validation/path/user/{id} +// POST /validation/json/register +// POST /validation/json/login +// POST /validation/json/profile +// POST /validation/form/kv/register +// GET /validation/basemodel/demo +// ============================================================================ + +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace vix; +namespace J = vix::json; + +// --------------------------------------------------------------------------- +// Helpers: consistent 400 error responses +// --------------------------------------------------------------------------- + +static inline J::Json error_item_json(const vix::validation::ValidationError &e) +{ + // build.hpp: kv({{key, Json(...)}, ...}) + return J::kv({ + {"field", J::Json(std::string(e.field))}, + {"code", J::Json(std::string(vix::validation::to_string(e.code)))}, + {"message", J::Json(std::string(e.message))}, + }); +} + +static inline void respond_validation_errors(Response &res, const vix::validation::ValidationErrors &errors) +{ + J::Json items = J::Json::array(); + + for (const auto &e : errors.all()) + items.push_back(error_item_json(e)); + + res.status(400).json(J::kv({ + {"ok", J::Json(false)}, + {"error", J::Json("validation_failed")}, + {"errors", std::move(items)}, + })); +} + +static inline void respond_conversion_error(Response &res, + const vix::conversion::ConversionError &e, + std::string field = "__input__") +{ + res.status(400).json(J::kv({ + {"ok", J::Json(false)}, + {"error", J::Json("conversion_failed")}, + {"field", J::Json(std::move(field))}, + {"code", J::Json(std::string(vix::conversion::to_string(e.code)))}, + {"position", J::Json(static_cast(e.position))}, + {"input", J::Json(std::string(e.input))}, + })); +} + +// --------------------------------------------------------------------------- +// 1) Schema examples +// --------------------------------------------------------------------------- + +struct RegisterBody +{ + std::string email; + std::string password; + std::string confirm; + + static vix::validation::Schema schema() + { + using namespace vix::validation; + + return vix::validation::schema() + .field("email", &RegisterBody::email, + field() + .required("email is required") + .email("invalid email format") + .length_max(120, "email too long")) + .field("password", &RegisterBody::password, + field() + .required("password is required") + .length_min(8, "password must be at least 8 characters") + .length_max(64, "password too long")) + .field("confirm", &RegisterBody::confirm, + field() + .required("confirm is required") + .length_min(8, "confirm must be at least 8 characters") + .length_max(64, "confirm too long")) + .check([](const RegisterBody &obj, ValidationErrors &out) -> void + { + if (!obj.password.empty() && !obj.confirm.empty() && obj.password != obj.confirm) + { + out.add("confirm", + ValidationErrorCode::Custom, + "passwords do not match"); + } }); + } +}; + +struct LoginBody : vix::validation::BaseModel +{ + std::string email; + std::string password; + + static vix::validation::Schema schema() + { + using namespace vix::validation; + + return vix::validation::schema() + .field("email", &LoginBody::email, + field() + .required("email is required") + .email("invalid email format")) + .field("password", &LoginBody::password, + field() + .required("password is required") + .length_min(8, "password too short")); + } +}; + +struct ProfileBody +{ + std::string username; + std::string age; // parsed via Schema::parsed + + static vix::validation::Schema schema() + { + using namespace vix::validation; + + return vix::validation::schema() + .field("username", &ProfileBody::username, + field() + .required("username is required") + .length_min(3, "username too short") + .length_max(24, "username too long")) + .parsed("age", &ProfileBody::age, + parsed() + .between(13, 120, "age out of range") + .parse_message("age must be a number")); + } +}; + +// --------------------------------------------------------------------------- +// 2) Form example: key/value binding + schema validation +// --------------------------------------------------------------------------- + +struct RegisterForm +{ + std::string email; + std::string password; + + using Input = std::vector>; + + static bool bind(RegisterForm &out, const Input &in, vix::validation::ValidationErrors &errors) + { + auto get = [&](std::string_view key) -> std::string_view + { + for (const auto &kv : in) + { + if (kv.first == key) + return kv.second; + } + return {}; + }; + + const auto email_sv = get("email"); + if (email_sv.empty()) + errors.add("email", vix::validation::ValidationErrorCode::Required, "email is required"); + else + out.email.assign(email_sv); + + const auto pass_sv = get("password"); + if (pass_sv.empty()) + errors.add("password", vix::validation::ValidationErrorCode::Required, "password is required"); + else + out.password.assign(pass_sv); + + return errors.ok(); + } + + static vix::validation::Schema schema() + { + using namespace vix::validation; + + return vix::validation::schema() + .field("email", &RegisterForm::email, + field() + .required("email is required") + .email("invalid email format")) + .field("password", &RegisterForm::password, + field() + .required("password is required") + .length_min(8, "password too short")); + } +}; + +// --------------------------------------------------------------------------- +// 3) JSON helper for vix::json::Simple token +// --------------------------------------------------------------------------- + +static inline std::string json_string_or( + const J::Json &j, + std::string_view key, + std::string fallback = "") +{ + if (!j.is_object()) + return fallback; + + auto it = j.find(std::string(key)); + if (it == j.end() || !it->is_string()) + return fallback; + + return it->get(); +} + +// --------------------------------------------------------------------------- +// 4) Routes +// --------------------------------------------------------------------------- + +static void register_validation_routes(App &app) +{ + app.get("/validation/health", [](Request &, Response &res) + { res.json(J::kv({ + {"ok", J::Json(true)}, + {"module", J::Json("validation")}, + })); }); + + app.get("/validation/query/register", [](Request &req, Response &res) + { + RegisterBody body; + body.email = req.query_value("email", ""); + body.password = req.query_value("password", ""); + body.confirm = req.query_value("confirm", ""); + + auto vr = RegisterBody::schema().validate(body); + if (!vr.ok()) + { + respond_validation_errors(res, vr.errors); + return; + } + + res.json(J::kv({ + {"ok", J::Json(true)}, + {"email", J::Json(body.email)}, + {"hint", J::Json("validated from query params")}, + })); }); + + app.get("/validation/path/user/{id}", [](Request &req, Response &res) + { + const std::string id_str = req.param("id", "0"); + + auto idr = vix::conversion::parse(id_str); + if (!idr) + { + respond_conversion_error(res, idr.error(), "id"); + return; + } + + const int id = idr.value(); + if (id <= 0) + { + vix::validation::ValidationErrors errors; + errors.add("id", vix::validation::ValidationErrorCode::Min, "id must be >= 1"); + respond_validation_errors(res, errors); + return; + } + + res.json(J::kv({ + {"ok", J::Json(true)}, + {"id", J::Json(id)}, + {"hint", J::Json("parsed via conversion::parse")}, + })); }); + + app.post("/validation/json/register", [](Request &req, Response &res) + { + const auto &j = req.json(); + + RegisterBody body; + body.email = json_string_or(j, "email"); + body.password = json_string_or(j, "password"); + body.confirm = json_string_or(j, "confirm"); + + auto vr = RegisterBody::schema().validate(body); + if (!vr.ok()) + { + respond_validation_errors(res, vr.errors); + return; + } + + res.status(201).json(J::kv({ + {"ok", J::Json(true)}, + {"message", J::Json("registered")}, + {"email", J::Json(body.email)}, + })); }); + + app.post("/validation/json/login", [](Request &req, Response &res) + { + const auto &j = req.json(); + + LoginBody body; + body.email = json_string_or(j, "email"); + body.password = json_string_or(j, "password"); + + auto vr = body.validate(); + if (!vr.ok()) + { + respond_validation_errors(res, vr.errors); + return; + } + + res.json(J::kv({ + {"ok", J::Json(true)}, + {"message", J::Json("login ok")}, + {"email", J::Json(body.email)}, + })); }); + + app.post("/validation/json/profile", [](Request &req, Response &res) + { + const auto &j = req.json(); + + ProfileBody body; + body.username = json_string_or(j, "username"); + body.age = json_string_or(j, "age"); + + auto vr = ProfileBody::schema().validate(body); + if (!vr.ok()) + { + respond_validation_errors(res, vr.errors); + return; + } + + res.json(J::kv({ + {"ok", J::Json(true)}, + {"message", J::Json("profile ok")}, + {"username", J::Json(body.username)}, + {"age_raw", J::Json(body.age)}, + })); }); + + app.post("/validation/form/kv/register", [](Request &req, Response &res) + { + const auto &j = req.json(); + + const std::string email = json_string_or(j, "email"); + const std::string password = json_string_or(j, "password"); + + RegisterForm::Input input; + input.reserve(2); + input.push_back({"email", std::string_view(email)}); + input.push_back({"password", std::string_view(password)}); + + auto r = vix::validation::Form::validate(input); + if (!r) + { + respond_validation_errors(res, r.errors()); + return; + } + + res.status(201).json(J::kv({ + {"ok", J::Json(true)}, + {"message", J::Json("registered via Form")}, + {"email", J::Json(r.value().email)}, + })); }); + + app.get("/validation/basemodel/demo", [](Request &req, Response &res) + { + LoginBody body; + body.email = req.query_value("email", ""); + body.password = req.query_value("password", ""); + + auto vr = LoginBody::validate(body); + if (!vr.ok()) + { + respond_validation_errors(res, vr.errors); + return; + } + + res.json(J::kv({ + {"ok", J::Json(true)}, + {"message", J::Json("BaseModel static validate ok")}, + {"email", J::Json(body.email)}, + })); }); +} + +// --------------------------------------------------------------------------- +// Bootstrap +// --------------------------------------------------------------------------- + +static int run_server() +{ + App app; + register_validation_routes(app); + app.run(8080); + return 0; +} + +int main() +{ + return run_server(); +} diff --git a/examples/validation/in_set.cpp b/examples/validation/in_set.cpp new file mode 100644 index 0000000..fac81fa --- /dev/null +++ b/examples/validation/in_set.cpp @@ -0,0 +1,20 @@ +#include +#include +#include + +#include + +using namespace vix::validation; + +int main() +{ + std::string role = "admin"; + + auto res = validate("role", role) + .required() + .in_set({"admin", "user", "guest"}) + .result(); + + std::cout << "ok=" << res.ok() << "\n"; + return res.ok() ? 0 : 1; +} diff --git a/examples/validation/numeric.cpp b/examples/validation/numeric.cpp new file mode 100644 index 0000000..65ab41b --- /dev/null +++ b/examples/validation/numeric.cpp @@ -0,0 +1,18 @@ +#include + +#include + +using namespace vix::validation; + +int main() +{ + int age = 17; + + auto res = validate("age", age) + .min(18, "must be adult") + .max(120) + .result(); + + std::cout << "ok=" << res.ok() << "\n"; + return res.ok() ? 0 : 1; +} diff --git a/examples/validation/optional.cpp b/examples/validation/optional.cpp new file mode 100644 index 0000000..f9a3100 --- /dev/null +++ b/examples/validation/optional.cpp @@ -0,0 +1,19 @@ +#include +#include + +#include +#include + +using namespace vix::validation; + +int main() +{ + std::optional score = std::nullopt; + + auto res = validate("score", score) + .rule(rules::required("score is required")) + .result(); + + std::cout << "ok=" << res.ok() << "\n"; + return res.ok() ? 0 : 1; +} diff --git a/examples/validation/parsed.cpp b/examples/validation/parsed.cpp new file mode 100644 index 0000000..95cd507 --- /dev/null +++ b/examples/validation/parsed.cpp @@ -0,0 +1,18 @@ +#include +#include + +#include + +using namespace vix::validation; + +int main() +{ + std::string input = "25"; // try "abc" + + auto res = validate_parsed("age", input) + .between(18, 120) + .result("age must be a number"); + + std::cout << "ok=" << res.ok() << "\n"; + return res.ok() ? 0 : 1; +} diff --git a/examples/validation/schema.cpp b/examples/validation/schema.cpp new file mode 100644 index 0000000..69a66fb --- /dev/null +++ b/examples/validation/schema.cpp @@ -0,0 +1,67 @@ +#include +#include +#include + +#include +#include +#include +#include + +struct RegisterForm : vix::validation::BaseModel +{ + std::string email; + std::string password; + std::string age; + + RegisterForm(std::string e, std::string p, std::string a) + : email(std::move(e)), password(std::move(p)), age(std::move(a)) + { + } + + static vix::validation::Schema schema() + { + return vix::validation::schema() + .field("email", &RegisterForm::email, + [](std::string_view f, const std::string &v) + { + return vix::validation::validate(f, v) + .required() + .email() + .length_max(120); + }) + .field("password", &RegisterForm::password, + [](std::string_view f, const std::string &v) + { + return vix::validation::validate(f, v) + .required() + .length_min(8) + .length_max(64); + }) + .parsed("age", &RegisterForm::age, + [](std::string_view f, std::string_view sv) + { + return vix::validation::validate_parsed(f, sv) + .between(18, 120) + .result("age must be a number"); + }); + } +}; + +int main() +{ + RegisterForm form{"bad-email", "123", "abc"}; + + auto res = form.validate(); + std::cout << "ok=" << res.ok() << "\n"; + std::cout << "errors=" << res.errors.size() << "\n"; + + for (const auto &e : res.errors.all()) + { + std::cout << " - field=" << e.field + << " code=" << vix::validation::to_string(e.code) + << " message=" << e.message + << "\n"; + } + + return res.ok() ? 0 : 1; +} diff --git a/examples/validation/schema_cross_field_check.cpp b/examples/validation/schema_cross_field_check.cpp new file mode 100644 index 0000000..e34bedd --- /dev/null +++ b/examples/validation/schema_cross_field_check.cpp @@ -0,0 +1,58 @@ +#include +#include + +#include +#include +#include + +struct ResetPassword +{ + std::string password; + std::string confirm; + + static vix::validation::Schema schema() + { + return vix::validation::schema() + .field("password", &ResetPassword::password, + vix::validation::field() + .required() + .length_min(8) + .length_max(64)) + .field("confirm", &ResetPassword::confirm, + vix::validation::field() + .required() + .length_min(8) + .length_max(64)) + .check([](const ResetPassword &obj, vix::validation::ValidationErrors &errors) + { + if (!obj.password.empty() && !obj.confirm.empty() && obj.password != obj.confirm) + { + errors.add("confirm", + vix::validation::ValidationErrorCode::Custom, + "passwords do not match"); + } }); + } +}; + +int main() +{ + ResetPassword in; + in.password = "password123"; + in.confirm = "password124"; + + auto r = ResetPassword::schema().validate(in); + + std::cout << "ok=" << static_cast(r.ok()) << "\n"; + if (!r.ok()) + { + for (const auto &e : r.errors.all()) + { + std::cout << " - field=" << e.field << " code=" << vix::validation::to_string(e.code) + << " message=" << e.message << "\n"; + } + return 1; + } + + std::cout << "valid reset\n"; + return 0; +} diff --git a/examples/validation/schema_fieldspec_basic.cpp b/examples/validation/schema_fieldspec_basic.cpp new file mode 100644 index 0000000..4480484 --- /dev/null +++ b/examples/validation/schema_fieldspec_basic.cpp @@ -0,0 +1,47 @@ +#include +#include + +#include + +struct User +{ + std::string email; + std::string password; + + static vix::validation::Schema schema() + { + return vix::validation::schema() + .field("email", &User::email, + vix::validation::field() + .required() + .email() + .length_max(120)) + .field("password", &User::password, + vix::validation::field() + .required() + .length_min(8) + .length_max(64)); + } +}; + +int main() +{ + User u; + u.email = "bad-email"; + u.password = "123"; + + auto r = User::schema().validate(u); + + std::cout << "ok=" << static_cast(r.ok()) << "\n"; + if (!r.ok()) + { + for (const auto &e : r.errors.all()) + { + std::cout << " - field=" << e.field << " message=" << e.message << "\n"; + } + return 1; + } + + std::cout << "valid user\n"; + return 0; +} diff --git a/examples/validation/schema_lambda_builder.cpp b/examples/validation/schema_lambda_builder.cpp new file mode 100644 index 0000000..9b7b75e --- /dev/null +++ b/examples/validation/schema_lambda_builder.cpp @@ -0,0 +1,55 @@ +#include +#include +#include + +#include +#include + +struct Register +{ + std::string email; + std::string password; + + static vix::validation::Schema schema() + { + return vix::validation::schema() + .field("email", &Register::email, + [](std::string_view f, const std::string &v) + { + return vix::validation::validate(f, v) + .required() + .email() + .length_max(120); + }) + .field("password", &Register::password, + [](std::string_view f, const std::string &v) + { + return vix::validation::validate(f, v) + .required() + .length_min(8) + .length_max(64); + }); + } +}; + +int main() +{ + Register in; + in.email = "hello@example.com"; + in.password = "short"; + + auto r = Register::schema().validate(in); + + std::cout << "ok=" << static_cast(r.ok()) << "\n"; + if (!r.ok()) + { + for (const auto &e : r.errors.all()) + { + std::cout << " - field=" << e.field << " message=" << e.message << "\n"; + } + return 1; + } + + std::cout << "valid register\n"; + return 0; +} diff --git a/examples/validation/simple_string.cpp b/examples/validation/simple_string.cpp new file mode 100644 index 0000000..89c059f --- /dev/null +++ b/examples/validation/simple_string.cpp @@ -0,0 +1,20 @@ +#include +#include + +#include + +using namespace vix::validation; + +int main() +{ + std::string email = "john@example.com"; + + auto res = validate("email", email) + .required() + .email() + .length_max(120) + .result(); + + std::cout << "ok=" << res.ok() << "\n"; + return res.ok() ? 0 : 1; +} diff --git a/examples/validation/validate_enum.cpp b/examples/validation/validate_enum.cpp new file mode 100644 index 0000000..37344fb --- /dev/null +++ b/examples/validation/validate_enum.cpp @@ -0,0 +1,35 @@ +#include +#include + +#include +#include + +using namespace vix::validation; +using namespace vix::conversion; + +enum class Role +{ + Admin, + User +}; + +static constexpr EnumEntry roles[] = { + {"admin", Role::Admin}, + {"user", Role::User}, +}; + +int main() +{ + std::string input = "admin"; // essaye "guest" + + auto parsed = parse_enum(input, roles); + + if (!parsed) + { + std::cout << "invalid role\n"; + return 1; + } + + std::cout << "role parsed OK\n"; + return 0; +} diff --git a/examples/validation/validate_parsed_int.cpp b/examples/validation/validate_parsed_int.cpp new file mode 100644 index 0000000..354f733 --- /dev/null +++ b/examples/validation/validate_parsed_int.cpp @@ -0,0 +1,29 @@ +#include +#include + +#include + +using namespace vix::validation; + +int main() +{ + std::string input = "25"; // essaye "abc", "5", "200" + + auto res = validate_parsed("age", input) + .between(18, 120, "age must be between 18 and 120") + .result("age must be a number"); + + std::cout << "ok = " << res.ok() << "\n"; + + if (!res.ok()) + { + for (const auto &e : res.errors.all()) + { + std::cout << "- field=" << e.field + << " code=" << to_string(e.code) + << " msg=" << e.message << "\n"; + } + } + + return res.ok() ? 0 : 1; +} diff --git a/examples/validation/validate_string.cpp b/examples/validation/validate_string.cpp new file mode 100644 index 0000000..520af76 --- /dev/null +++ b/examples/validation/validate_string.cpp @@ -0,0 +1,29 @@ +#include +#include + +#include + +using namespace vix::validation; + +int main() +{ + std::string email = "test@example.com"; // essaye "", "abc" + + auto res = validate("email", email) + .required("email is required") + .email("invalid email format") + .length_max(64, "email too long") + .result(); + + std::cout << "ok = " << res.ok() << "\n"; + + if (!res.ok()) + { + for (const auto &e : res.errors.all()) + { + std::cout << "- " << e.field << ": " << e.message << "\n"; + } + } + + return res.ok() ? 0 : 1; +} diff --git a/modules/cli b/modules/cli index b288d39..f0cb00d 160000 --- a/modules/cli +++ b/modules/cli @@ -1 +1 @@ -Subproject commit b288d39d804884e96914b5d904734cae6d8ef8b2 +Subproject commit f0cb00d0dca24aa0021db712a7c9a9106e4e28f2 diff --git a/modules/conversion b/modules/conversion new file mode 160000 index 0000000..333580d --- /dev/null +++ b/modules/conversion @@ -0,0 +1 @@ +Subproject commit 333580d46d73d9c58d5c75f86b6190aa41c38857 diff --git a/modules/json b/modules/json index 6ae65b0..315d086 160000 --- a/modules/json +++ b/modules/json @@ -1 +1 @@ -Subproject commit 6ae65b03606438595a712cf330376e09cc788d99 +Subproject commit 315d0862b8ba0a739a2facd135f8db88048eb323 diff --git a/modules/validation b/modules/validation new file mode 160000 index 0000000..c8e8479 --- /dev/null +++ b/modules/validation @@ -0,0 +1 @@ +Subproject commit c8e847980161c47e3b41de43d40d757c1270e8ed