diff --git a/include/vix/json/dumps.hpp b/include/vix/json/dumps.hpp index 902e134..d314f2b 100644 --- a/include/vix/json/dumps.hpp +++ b/include/vix/json/dumps.hpp @@ -69,46 +69,60 @@ namespace vix::json namespace fs = std::filesystem; /** - * @brief Serialize JSON to a human-readable string (pretty printed). + * @brief Reusable JSON serialization settings for string and file output. * - * @param j JSON value (object/array/etc). - * @param indent Number of spaces per indentation level (default: 2). - * @param ensure_ascii If true, non-ASCII characters are escaped. - * @return Formatted JSON string. + * @details + * - `pretty=true` enables multi-line formatted output using `indent`. + * - `pretty=false` enables compact single-line output and ignores `indent`. + * - `indent` must be non-negative. + * - `ensure_ascii=true` escapes non-ASCII characters. + */ + struct SerializeOptions + { + bool pretty = true; + int indent = 2; + bool ensure_ascii = false; + }; + + /** + * @brief Validate serialization options before use. * - * @note - * Use this for logs, debugging, configuration files, and readable outputs. + * @throws std::invalid_argument If `indent` is negative. */ - inline std::string dumps(const Json &j, int indent = 2, bool ensure_ascii = false) + inline void validate_serialize_options(const SerializeOptions &options) { - return j.dump(indent, ' ', ensure_ascii); + if (options.indent < 0) + throw std::invalid_argument("JSON serialization indentation must be non-negative"); } /** - * @brief Serialize JSON to a compact single-line string. + * @brief Resolve the effective indent passed to nlohmann::json::dump(). * - * @param j JSON value (object/array/etc). - * @param ensure_ascii If true, non-ASCII characters are escaped. - * @return Compact JSON string. - * - * @note - * Use this for network payloads, compact storage, or performance-sensitive logs. + * @details + * Compact mode ignores `indent` and always produces a single-line string. */ - inline std::string dumps_compact(const Json &j, bool ensure_ascii = false) + inline int resolve_dump_indent(const SerializeOptions &options) { - return j.dump(-1, ' ', ensure_ascii); + return options.pretty ? options.indent : -1; } /** - * @brief Explicit alias for dumps() to emphasize readability. + * @brief Serialize JSON using explicit serialization options. + * + * @param j JSON value (object/array/etc). + * @param options Serialization settings shared with file output. + * @return Serialized JSON string. + * + * @throws std::invalid_argument If `options.indent` is negative. */ - inline std::string dumps_pretty(const Json &j, int indent = 2, bool ensure_ascii = false) + inline std::string dumps(const Json &j, const SerializeOptions &options) { - return dumps(j, indent, ensure_ascii); + validate_serialize_options(options); + return j.dump(resolve_dump_indent(options), ' ', options.ensure_ascii); } /** - * @brief Write JSON to a file using a temp file + rename strategy. + * @brief Write JSON to a file using explicit serialization options. * * @details * Steps: @@ -117,22 +131,19 @@ namespace vix::json * 3) Replace destination by renaming temp file to ``. * 4) If rename fails, fallback to copy + remove. * - * This strategy reduces the chance of ending with a partially-written file. - * * @param path Destination file path. * @param j JSON value to write. - * @param indent Pretty-print indentation (default: 2). - * @param ensure_ascii Escape non-ASCII characters if true. + * @param options Serialization settings shared with string output. * + * @throws std::invalid_argument + * If `options.indent` is negative. * @throws std::runtime_error * If the temp file cannot be written or the final replacement fails. - * - * @warning - * This function writes the entire JSON document at once. - * Do not use it for unbounded user uploads. */ - inline void dump_file(const fs::path &path, const Json &j, int indent = 2, bool ensure_ascii = false) + inline void dump_file(const fs::path &path, const Json &j, const SerializeOptions &options) { + validate_serialize_options(options); + // 1) Ensure directory exists (best-effort) if (path.has_parent_path()) { @@ -151,7 +162,7 @@ namespace vix::json try { - const auto s = j.dump(indent, ' ', ensure_ascii); + const auto s = dumps(j, options); ofs.write(s.data(), static_cast(s.size())); ofs.flush(); if (!ofs) @@ -182,18 +193,105 @@ namespace vix::json } } + /** + * @brief Serialize JSON to a human-readable string (pretty printed). + * + * @param j JSON value (object/array/etc). + * @param indent Number of spaces per indentation level (default: 2). + * @param ensure_ascii If true, non-ASCII characters are escaped. + * @return Formatted JSON string. + * + * @note + * Use this for logs, debugging, configuration files, and readable outputs. + */ + inline std::string dumps(const Json &j, int indent = 2, bool ensure_ascii = false) + { + if (indent < 0) + return dumps(j, SerializeOptions{false, 2, ensure_ascii}); + return dumps(j, SerializeOptions{true, indent, ensure_ascii}); + } + + /** + * @brief Serialize JSON to a compact single-line string. + * + * @param j JSON value (object/array/etc). + * @param ensure_ascii If true, non-ASCII characters are escaped. + * @return Compact JSON string. + * + * @note + * Use this for network payloads, compact storage, or performance-sensitive logs. + */ + inline std::string dumps_compact(const Json &j, bool ensure_ascii = false) + { + return dumps(j, SerializeOptions{false, 2, ensure_ascii}); + } + + /** + * @brief Explicit alias for dumps() to emphasize readability. + */ + inline std::string dumps_pretty(const Json &j, int indent = 2, bool ensure_ascii = false) + { + return dumps(j, indent, ensure_ascii); + } + + /** + * @brief Write JSON to a file using a temp file + rename strategy. + * + * @details + * Steps: + * 1) Ensure parent directory exists (best-effort). + * 2) Write JSON to `.tmp`. + * 3) Replace destination by renaming temp file to ``. + * 4) If rename fails, fallback to copy + remove. + * + * This strategy reduces the chance of ending with a partially-written file. + * + * @param path Destination file path. + * @param j JSON value to write. + * @param indent Pretty-print indentation (default: 2). + * @param ensure_ascii Escape non-ASCII characters if true. + * + * @throws std::runtime_error + * If the temp file cannot be written or the final replacement fails. + * + * @warning + * This function writes the entire JSON document at once. + * Do not use it for unbounded user uploads. + */ + inline void dump_file(const fs::path &path, const Json &j, int indent = 2, bool ensure_ascii = false) + { + if (indent < 0) + { + dump_file(path, j, SerializeOptions{false, 2, ensure_ascii}); + return; + } + dump_file(path, j, SerializeOptions{true, indent, ensure_ascii}); + } + /// @overload Convenience overload for C-string path. inline void dump_file(const char *path, const Json &j, int indent = 2, bool ensure_ascii = false) { dump_file(fs::path{path}, j, indent, ensure_ascii); } + /// @overload Convenience overload for C-string path with explicit options. + inline void dump_file(const char *path, const Json &j, const SerializeOptions &options) + { + dump_file(fs::path{path}, j, options); + } + /// @overload Convenience overload for std::string path. inline void dump_file(const std::string &path, const Json &j, int indent = 2, bool ensure_ascii = false) { dump_file(fs::path{path}, j, indent, ensure_ascii); } + /// @overload Convenience overload for std::string path with explicit options. + inline void dump_file(const std::string &path, const Json &j, const SerializeOptions &options) + { + dump_file(fs::path{path}, j, options); + } + } // namespace vix::json #endif // VIX_JSON_DUMPS_HPP diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b9bf660..a2b6812 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,6 +6,7 @@ set(VIX_JSON_TEST_TARGETS json_build_test json_convert_test json_dumps_test + json_dumps_options_test json_include_test json_jpath_test json_loads_test diff --git a/tests/json_dumps_options_test.cpp b/tests/json_dumps_options_test.cpp new file mode 100644 index 0000000..9bc01b9 --- /dev/null +++ b/tests/json_dumps_options_test.cpp @@ -0,0 +1,237 @@ +#include +#include + +#include +#include +#include +#include +#include + +int main() +{ + using namespace vix::json; + + namespace fs = std::filesystem; + + // SerializeOptions pretty=false produces compact single-line output. + { + Json j = { + {"name", "Vix.cpp"}, + {"active", true}, + {"count", 3}, + }; + + const std::string s = dumps(j, SerializeOptions{false, 8, false}); + + assert(!s.empty()); + assert(s.find('\n') == std::string::npos); + assert(s.find("\"name\"") != std::string::npos); + assert(s.find("\"active\"") != std::string::npos); + assert(s.find("\"count\"") != std::string::npos); + } + + // SerializeOptions pretty=true respects indentation width. + { + Json j = { + {"meta", + { + {"name", "Vix.cpp"}, + {"version", 1}, + }}, + }; + + const std::string s = dumps(j, SerializeOptions{true, 4, false}); + + assert(!s.empty()); + assert(s.find('\n') != std::string::npos); + assert(s.find("\n \"meta\"") != std::string::npos); + assert(s.find("\n \"name\"") != std::string::npos); + } + + // SerializeOptions ensure_ascii=false keeps non-ASCII text. + { + Json j = { + {"word", "café"}, + }; + + const std::string s = dumps(j, SerializeOptions{false, 2, false}); + + assert(s.find("café") != std::string::npos); + } + + // SerializeOptions ensure_ascii=true escapes non-ASCII text. + { + Json j = { + {"word", "café"}, + }; + + const std::string s = dumps(j, SerializeOptions{false, 2, true}); + + assert(s.find("café") == std::string::npos); + assert(s.find("\\u00e9") != std::string::npos); + } + + // Negative indentation is invalid for the options API. + { + Json j = { + {"ok", true}, + }; + + bool threw = false; + + try + { + (void)dumps(j, SerializeOptions{true, -1, false}); + } + catch (const std::invalid_argument &) + { + threw = true; + } + + assert(threw); + } + + // dump_file() with SerializeOptions uses the same compact semantics as dumps(). + { + const fs::path dir = fs::temp_directory_path() / "vix_json_dumps_options_test_compact"; + const fs::path path = dir / "config.json"; + + std::error_code ec; + fs::remove_all(dir, ec); + + Json j = { + {"message", "hello"}, + {"value", 7}, + }; + + const SerializeOptions options{false, 6, false}; + const std::string expected = dumps(j, options); + + dump_file(path, j, options); + + assert(fs::exists(path)); + + std::ifstream ifs(path, std::ios::binary); + assert(ifs); + + const std::string content((std::istreambuf_iterator(ifs)), + std::istreambuf_iterator()); + + assert(content == expected); + assert(content.find('\n') == std::string::npos); + + fs::remove_all(dir, ec); + } + + // dump_file() with SerializeOptions uses the same pretty semantics as dumps(). + { + const fs::path dir = fs::temp_directory_path() / "vix_json_dumps_options_test_pretty"; + const fs::path path = dir / "config.json"; + + std::error_code ec; + fs::remove_all(dir, ec); + + Json j = { + {"outer", + { + {"inner", "value"}, + }}, + }; + + const SerializeOptions options{true, 3, false}; + const std::string expected = dumps(j, options); + + dump_file(path, j, options); + + assert(fs::exists(path)); + + std::ifstream ifs(path, std::ios::binary); + assert(ifs); + + const std::string content((std::istreambuf_iterator(ifs)), + std::istreambuf_iterator()); + + assert(content == expected); + assert(content.find('\n') != std::string::npos); + assert(content.find("\n \"outer\"") != std::string::npos); + + fs::remove_all(dir, ec); + } + + // dump_file() rejects invalid SerializeOptions clearly. + { + const fs::path dir = fs::temp_directory_path() / "vix_json_dumps_options_test_invalid"; + const fs::path path = dir / "config.json"; + + std::error_code ec; + fs::remove_all(dir, ec); + + Json j = { + {"ok", true}, + }; + + bool threw = false; + + try + { + dump_file(path, j, SerializeOptions{true, -1, false}); + } + catch (const std::invalid_argument &) + { + threw = true; + } + + assert(threw); + assert(!fs::exists(path)); + + fs::remove_all(dir, ec); + } + + // Legacy positional dumps(j, -1) remains compact. + { + Json j = { + {"legacy", true}, + {"value", 9}, + }; + + const std::string s = dumps(j, -1); + + assert(!s.empty()); + assert(s.find('\n') == std::string::npos); + assert(s.find("\"legacy\"") != std::string::npos); + assert(s.find("\"value\"") != std::string::npos); + } + + // Legacy positional dump_file(path, j, -1) writes compact content. + { + const fs::path dir = fs::temp_directory_path() / "vix_json_dumps_options_test_legacy_compact"; + const fs::path path = dir / "config.json"; + + std::error_code ec; + fs::remove_all(dir, ec); + + Json j = { + {"legacy", true}, + {"value", 9}, + }; + + dump_file(path, j, -1); + + assert(fs::exists(path)); + + std::ifstream ifs(path, std::ios::binary); + assert(ifs); + + const std::string content((std::istreambuf_iterator(ifs)), + std::istreambuf_iterator()); + + assert(!content.empty()); + assert(content.find('\n') == std::string::npos); + assert(content.find("\"legacy\"") != std::string::npos); + assert(content.find("\"value\"") != std::string::npos); + + fs::remove_all(dir, ec); + } + + return 0; +}