From fc57c09cf0751fd4eea94e76ce22fea906e27c40 Mon Sep 17 00:00:00 2001 From: Francisco Geiman Thiesen Date: Tue, 20 Jan 2026 07:32:24 -0800 Subject: [PATCH] Add FracturedJson formatting support for DOM serialization (#2580) * Add FracturedJson formatting support for DOM serialization Implements FracturedJson formatting as requested in issue #2576. FracturedJson produces human-readable yet compact JSON output by intelligently choosing between different layout strategies based on content complexity, length, and structure similarity. Key features: - Four layout modes: inline, compact multiline, table, and expanded - Structure analysis pass to compute metrics before formatting - Table formatting for arrays of similar objects with column alignment - Configurable options for line length, indentation, padding, etc. New files: - fractured_json.h: Public API with fractured_json_options struct - fractured_json-inl.h: Implementation (~1000 lines) - json_structure_analyzer.h: Structure analysis for layout decisions - fractured_formatter.h: Formatter class using CRTP pattern Usage: dom::parser parser; element doc = parser.parse(json_string); std::cout << fractured_json(doc) << std::endl; // Or with custom options: fractured_json_options opts; opts.indent_spaces = 2; std::cout << fractured_json(doc, opts) << std::endl; // Or format any JSON string (useful with reflection API): auto formatted = fractured_json_string(minified_json); Resolves #2576 * Add comprehensive tests for FracturedJson formatter Adds 27 test cases covering all aspects of the FracturedJson formatter: Core functionality tests (13): - Roundtrip parsing verification - Inline formatting for simple arrays and objects - Expanded formatting for complex nested structures - Compact multiline arrays with configurable items per line - Table formatting for uniform arrays of objects - Empty container handling - All scalar types (string, int, uint, double, bool, null) - String escaping (quotes, backslashes, control characters) - Custom indentation options - Deep nesting (10+ levels) - Mixed type arrays Edge case tests (11): - Unicode strings (Chinese, emoji, Arabic, Russian, accented chars) - Boundary numbers (INT64_MIN/MAX, UINT64_MAX, DBL_MIN/MAX) - Nested arrays (arrays of arrays) - Empty string values - Keys with special characters (spaces, quotes, colons, etc.) - Non-uniform arrays (should not trigger table mode) - Very long strings (500+ chars) - Large arrays (100 elements) - Reflection API workflow simulation - Control characters (tab, newline, CR, null) - Single element containers Option tests (3): - Disable compact multiline mode - Disable table format mode - Disable all padding options * Add FracturedJson integration with builder/reflection API Extends FracturedJson to work seamlessly with the builder API, enabling formatted output directly from C++ structs using static reflection. New functions: - to_fractured_json_string(obj, opts) - serialize struct to formatted JSON - to_fractured_json(obj, output, opts) - same with output parameter - extract_fractured_json(obj, opts) - format only specific fields These functions combine the builder's reflection-based serialization with FracturedJson formatting in a single convenient call: struct User { int id; std::string name; bool active; }; User user{1, "Alice", true}; // Minified output (existing): auto minified = to_json_string(user); // {"id":1,"name":"Alice","active":true} // Formatted output (new): auto formatted = to_fractured_json_string(user); // { "id": 1, "name": "Alice", "active": true } // Partial extraction with formatting: auto partial = extract_fractured_json<"id", "name">(user); // { "id": 1, "name": "Alice" } New files: - generic/builder/fractured_json_builder.h - builder integration - tests/builder/static_reflection_fractured_json_tests.cpp - 7 tests * Fix INT64_MIN overflow and implement table_similarity_threshold - Fix undefined behavior when negating INT64_MIN in estimate_number_length() and measure_value_length() by returning 20 (the exact length of the string representation) directly - Actually use table_similarity_threshold in check_array_uniformity() by calling compute_object_similarity() to compare objects against the first object in the array * Fix -Werror=effc++ member initialization warnings Initialize all member variables in member initialization lists to satisfy GCC's -Werror=effc++ flag: - element_metrics::common_keys - add {} default initializer - structure_analyzer - add default constructor with member init list - fractured_formatter - add column_widths_{} to constructor - fractured_string_builder - add analyzer_{} to constructor * Add Rule of Five to structure_analyzer class The class has a pointer member (current_opts_) which triggers -Werror=effc++ requiring explicit copy/move operations. Delete copy operations (class shouldn't be copied due to cache) and default move operations. * Fix Windows build: wrap std::max to avoid macro conflict Windows.h defines max/min macros that interfere with std::max/std::min. Wrapping in parentheses as (std::max)(...) prevents macro expansion. * Fix GCC 15 false positive -Wfree-nonheap-object warning GCC 15 on MINGW64 gives a false positive warning in parser_moving_parser() when the std::vector goes out of scope. Suppress this specific warning with a pragma for GCC builds. * Fix metrics cache key bug by passing metrics through recursion The cache was using element addresses as keys, but dom::element objects are lightweight wrappers that get copied during iteration, causing different addresses between analysis and formatting phases. This resulted in cache misses and fallback to empty metrics. Solution: Store child metrics in the element_metrics struct and pass them through recursive calls, eliminating the need for address-based caching entirely. Changes: - Add children vector to element_metrics for hierarchical metrics - Remove metrics_cache_ and related get_metrics/has_metrics methods - Update all format functions to accept and pass child metrics - Add public analyze_array/analyze_object overloads for standalone use * Add ignore patterns for Node.js, Rust, and generated files Add entries for node_modules, package-lock.json, Rust target directories, local ablation artifacts, and generated documentation files. * Refactor: extract analyze_scalar helper to reduce code duplication Extract common scalar type handling (STRING, INT64, UINT64, DOUBLE, BOOL, NULL_VALUE) into a dedicated analyze_scalar method. Each scalar type shares the same initialization pattern for complexity, child_count, can_inline, and recommended_layout. Also simplify boolean formatting in format_scalar to use ternary operator. * Fix formatting and duplicate error message in amalgamate.py Reformat cramped is_amalgamator condition to multi-line for readability. Fix duplicate error message text in _included_filename_root and use correct variable name (relative_root instead of root). * Refactor: add count_newlines helper in fractured_json tests Extract repeated newline counting loop into a reusable static helper function, used by inline_array_test, inline_object_test, and expanded_test. * Revert "Add ignore patterns for Node.js, Rust, and generated files" This reverts commit 4760ea7cd04991bb621421b4919dbfa66bc50c70. * various minor changes --------- Co-authored-by: Daniel Lemire --- doc/builder.md | 47 + include/simdjson/dom.h | 2 + include/simdjson/dom/fractured_json-inl.h | 1077 +++++++++++++++++ include/simdjson/dom/fractured_json.h | 159 +++ .../simdjson/generic/builder/amalgamated.h | 1 + .../simdjson/generic/builder/dependencies.h | 1 + .../generic/builder/fractured_json_builder.h | 117 ++ .../simdjson/internal/fractured_formatter.h | 161 +++ .../internal/json_structure_analyzer.h | 169 +++ singleheader/amalgamate.py | 8 +- tests/CMakeLists.txt | 1 + tests/builder/CMakeLists.txt | 1 + ...static_reflection_fractured_json_tests.cpp | 236 ++++ tests/dom/basictests.cpp | 8 + tests/fractured_json_tests.cpp | 1025 ++++++++++++++++ 15 files changed, 3011 insertions(+), 2 deletions(-) create mode 100644 include/simdjson/dom/fractured_json-inl.h create mode 100644 include/simdjson/dom/fractured_json.h create mode 100644 include/simdjson/generic/builder/fractured_json_builder.h create mode 100644 include/simdjson/internal/fractured_formatter.h create mode 100644 include/simdjson/internal/json_structure_analyzer.h create mode 100644 tests/builder/static_reflection_fractured_json_tests.cpp create mode 100644 tests/fractured_json_tests.cpp diff --git a/doc/builder.md b/doc/builder.md index 8bbfb141b..ec170c662 100644 --- a/doc/builder.md +++ b/doc/builder.md @@ -14,6 +14,7 @@ speed and high convenience. * [C++26 static reflection](#c--26-static-reflection) + [Without `string_buffer` instance](#without--string-buffer--instance) + [Without `string_buffer` instance but with explicit error handling](#without--string-buffer--instance-but-with-explicit-error-handling) + + [Pretty formatted (fractured JSON)](#pretty-formatted-fractured-json) Overview: string_builder --------------------------- @@ -364,3 +365,49 @@ void tag_invoke(serialize_tag, builder_type &builder, const Car& car) { } // namespace simdjson ``` + +### Pretty formatted (fractured JSON) + +In some instances, you may want your JSON to be more readable. For this pupose, we also +support the Fractured JSON standard. + +```Cpp + TableTestData data{ + {{1, "Alice", true}, {2, "Bob", false}, {3, "Carol", true}, {4, "Dave", false}} + }; + + fractured_json_options opts; + opts.enable_table_format = true; + opts.min_table_rows = 3; + + std::string formatted = simdjson::to_fractured_json_string(data, opts); +``` + +The result might be as follows. + +```json +{ + "records": [ + { "active": true , "id": 1, "name": "Alice" }, + { "active": false, "id": 2, "name": "Bob" }, + { "active": true , "id": 3, "name": "Carol" }, + { "active": false, "id": 4, "name": "Dave" } + ] +} +``` + +The `fractured_json_options` struct allows you to customize the formatting behavior. It includes the following options: + +- `max_total_line_length` (default: 120): Maximum total characters per line. Content exceeding this will be expanded to multiple lines. +- `max_inline_length` (default: 80): Maximum length for inlined elements. Simple arrays/objects shorter than this may be rendered inline. +- `max_inline_complexity` (default: 2): Maximum nesting depth for inline rendering. Elements with complexity exceeding this will be expanded. Complexity 0 = scalar, 1 = flat array/object, 2 = one level of nesting. +- `max_compact_array_complexity` (default: 1): Maximum complexity for compact array formatting. Arrays with elements of this complexity or less may have multiple items per line. +- `indent_spaces` (default: 4): Number of spaces per indentation level. +- `enable_table_format` (default: true): Enable tabular formatting for arrays of similar objects. When enabled, arrays of objects with identical keys are formatted as aligned tables. +- `min_table_rows` (default: 3): Minimum number of rows to trigger table mode. +- `table_similarity_threshold` (default: 0.8): Similarity threshold for table detection. Objects must share at least this fraction of keys to be formatted as a table. +- `enable_compact_multiline` (default: true): Enable compact multiline arrays. When enabled, arrays of simple elements may have multiple items per line. +- `max_items_per_line` (default: 10): Maximum array items per line in compact mode. +- `simple_bracket_padding` (default: true): Add space inside brackets for simple containers. When true: `{ "key": "value" }`, when false: `{"key": "value"}`. +- `colon_padding` (default: true): Add space after colons. When true: `"key": "value"`, when false: `"key":"value"`. +- `comma_padding` (default: true): Add space after commas in inline content. When true: `[1, 2, 3]`, when false: `[1,2,3]`. \ No newline at end of file diff --git a/include/simdjson/dom.h b/include/simdjson/dom.h index bcf22ac22..cfdd1009f 100644 --- a/include/simdjson/dom.h +++ b/include/simdjson/dom.h @@ -9,6 +9,7 @@ #include "simdjson/dom/object.h" #include "simdjson/dom/parser.h" #include "simdjson/dom/serialization.h" +#include "simdjson/dom/fractured_json.h" // Inline functions #include "simdjson/dom/array-inl.h" @@ -19,5 +20,6 @@ #include "simdjson/dom/parser-inl.h" #include "simdjson/internal/tape_ref-inl.h" #include "simdjson/dom/serialization-inl.h" +#include "simdjson/dom/fractured_json-inl.h" #endif // SIMDJSON_DOM_H diff --git a/include/simdjson/dom/fractured_json-inl.h b/include/simdjson/dom/fractured_json-inl.h new file mode 100644 index 000000000..2ab9d36d8 --- /dev/null +++ b/include/simdjson/dom/fractured_json-inl.h @@ -0,0 +1,1077 @@ +#ifndef SIMDJSON_DOM_FRACTURED_JSON_INL_H +#define SIMDJSON_DOM_FRACTURED_JSON_INL_H + +#include "simdjson/dom/fractured_json.h" +#include "simdjson/dom/serialization.h" +#include "simdjson/dom/element-inl.h" +#include "simdjson/dom/array-inl.h" +#include "simdjson/dom/object-inl.h" +#include "simdjson/dom/parser-inl.h" +#include "simdjson/padded_string.h" +#include "simdjson/internal/json_structure_analyzer.h" +#include "simdjson/internal/fractured_formatter.h" + +#include +#include +#include + +namespace simdjson { +namespace internal { + +// +// Structure Analyzer Implementation +// + +inline element_metrics structure_analyzer::analyze(const dom::element& elem, + const fractured_json_options& opts) { + current_opts_ = &opts; + return analyze_element(elem, 0); +} + +inline void structure_analyzer::clear() { + current_opts_ = nullptr; +} + +inline element_metrics structure_analyzer::analyze_array(const dom::array& arr, + const fractured_json_options& opts) { + current_opts_ = &opts; + return analyze_array(arr, 0); +} + +inline element_metrics structure_analyzer::analyze_object(const dom::object& obj, + const fractured_json_options& opts) { + current_opts_ = &opts; + return analyze_object(obj, 0); +} + +inline element_metrics structure_analyzer::analyze_element(const dom::element& elem, size_t depth) { + switch (elem.type()) { + case dom::element_type::ARRAY: { + dom::array arr; + if (elem.get_array().get(arr) == SUCCESS) { + return analyze_array(arr, depth); + } + break; + } + case dom::element_type::OBJECT: { + dom::object obj; + if (elem.get_object().get(obj) == SUCCESS) { + return analyze_object(obj, depth); + } + break; + } + default: + // Handle all scalar types with a helper + return analyze_scalar(elem); + } + return element_metrics{}; +} + +inline element_metrics structure_analyzer::analyze_scalar(const dom::element& elem) { + element_metrics metrics; + metrics.complexity = 0; + metrics.child_count = 0; + metrics.can_inline = true; + metrics.recommended_layout = layout_mode::INLINE; + + switch (elem.type()) { + case dom::element_type::STRING: { + std::string_view str; + if (elem.get_string().get(str) == SUCCESS) { + metrics.estimated_inline_len = estimate_string_length(str); + } + break; + } + case dom::element_type::INT64: { + int64_t val; + if (elem.get_int64().get(val) == SUCCESS) { + metrics.estimated_inline_len = estimate_number_length(val); + } + break; + } + case dom::element_type::UINT64: { + uint64_t val; + if (elem.get_uint64().get(val) == SUCCESS) { + metrics.estimated_inline_len = estimate_number_length(val); + } + break; + } + case dom::element_type::DOUBLE: { + double val; + if (elem.get_double().get(val) == SUCCESS) { + metrics.estimated_inline_len = estimate_number_length(val); + } + break; + } + case dom::element_type::BOOL: { + bool val; + if (elem.get_bool().get(val) == SUCCESS) { + metrics.estimated_inline_len = val ? 4 : 5; // "true" or "false" + } + break; + } + case dom::element_type::NULL_VALUE: + metrics.estimated_inline_len = 4; // "null" + break; + default: + break; + } + + return metrics; +} + +inline element_metrics structure_analyzer::analyze_array(const dom::array& arr, + size_t depth) { + element_metrics metrics; + metrics.complexity = 1; // At least 1 for being an array + metrics.estimated_inline_len = 2; // "[]" + metrics.child_count = 0; + + size_t max_child_complexity = 0; + bool first = true; + + for (dom::element child : arr) { + if (!first) { + metrics.estimated_inline_len += 2; // ", " + } + first = false; + + element_metrics child_metrics = analyze_element(child, depth + 1); + metrics.estimated_inline_len += child_metrics.estimated_inline_len; + max_child_complexity = (std::max)(max_child_complexity, child_metrics.complexity); + metrics.child_count++; + metrics.children.push_back(std::move(child_metrics)); + } + + // Complexity is 1 + max child complexity + metrics.complexity = 1 + max_child_complexity; + + // Check if can inline + metrics.can_inline = (metrics.complexity <= current_opts_->max_inline_complexity) && + (metrics.estimated_inline_len <= current_opts_->max_inline_length); + + // Check for uniform array (table formatting) + if (current_opts_->enable_table_format && + metrics.child_count >= current_opts_->min_table_rows) { + metrics.is_uniform_array = check_array_uniformity(arr, metrics.common_keys); + } + + // Decide layout + if (metrics.child_count == 0) { + metrics.recommended_layout = layout_mode::INLINE; + } else if (metrics.can_inline) { + metrics.recommended_layout = layout_mode::INLINE; + } else if (metrics.is_uniform_array && !metrics.common_keys.empty()) { + metrics.recommended_layout = layout_mode::TABLE; + } else if (current_opts_->enable_compact_multiline && + max_child_complexity <= current_opts_->max_compact_array_complexity) { + metrics.recommended_layout = layout_mode::COMPACT_MULTILINE; + } else { + metrics.recommended_layout = layout_mode::EXPANDED; + } + + return metrics; +} + +inline element_metrics structure_analyzer::analyze_object(const dom::object& obj, + size_t depth) { + element_metrics metrics; + metrics.complexity = 1; + metrics.estimated_inline_len = 2; // "{}" + metrics.child_count = 0; + + size_t max_child_complexity = 0; + bool first = true; + + for (dom::key_value_pair field : obj) { + if (!first) { + metrics.estimated_inline_len += 2; // ", " + } + first = false; + + // Key length: quotes + key + colon + space + metrics.estimated_inline_len += estimate_string_length(field.key) + 2; + + element_metrics child_metrics = analyze_element(field.value, depth + 1); + metrics.estimated_inline_len += child_metrics.estimated_inline_len; + max_child_complexity = (std::max)(max_child_complexity, child_metrics.complexity); + metrics.child_count++; + metrics.children.push_back(std::move(child_metrics)); + } + + metrics.complexity = 1 + max_child_complexity; + + metrics.can_inline = (metrics.complexity <= current_opts_->max_inline_complexity) && + (metrics.estimated_inline_len <= current_opts_->max_inline_length); + + // Objects use inline or expanded (no table/compact for objects) + if (metrics.child_count == 0 || metrics.can_inline) { + metrics.recommended_layout = layout_mode::INLINE; + } else { + metrics.recommended_layout = layout_mode::EXPANDED; + } + + return metrics; +} + +inline size_t structure_analyzer::estimate_string_length(std::string_view s) const { + size_t len = 2; // quotes + for (char c : s) { + if (c == '"' || c == '\\' || static_cast(c) < 32) { + len += 2; // escape sequence (at least) + } else { + len += 1; + } + } + return len; +} + +inline size_t structure_analyzer::estimate_number_length(double d) const { + if (std::isnan(d) || std::isinf(d)) { + return 4; // "null" for invalid numbers + } + // Rough estimate: up to 17 significant digits + sign + decimal point + exponent + char buf[32]; + int len = snprintf(buf, sizeof(buf), "%.17g", d); + return len > 0 ? static_cast(len) : 20; +} + +inline size_t structure_analyzer::estimate_number_length(int64_t i) const { + if (i == 0) return 1; + // Handle INT64_MIN specially to avoid overflow when negating + if (i == INT64_MIN) return 20; // "-9223372036854775808" is 20 characters + size_t len = (i < 0) ? 1 : 0; // negative sign + int64_t abs_val = (i < 0) ? -i : i; + while (abs_val > 0) { + len++; + abs_val /= 10; + } + return len; +} + +inline size_t structure_analyzer::estimate_number_length(uint64_t u) const { + if (u == 0) return 1; + size_t len = 0; + while (u > 0) { + len++; + u /= 10; + } + return len; +} + +inline bool structure_analyzer::check_array_uniformity(const dom::array& arr, + std::vector& common_keys) const { + common_keys.clear(); + + std::set shared_keys; + dom::object first_obj; + bool have_first = false; + size_t object_count = 0; + + for (dom::element elem : arr) { + if (elem.type() != dom::element_type::OBJECT) { + return false; // Not all elements are objects + } + + dom::object obj; + if (elem.get_object().get(obj) != SUCCESS) { + return false; + } + + std::set current_keys; + for (dom::key_value_pair field : obj) { + current_keys.insert(std::string(field.key)); + } + + if (!have_first) { + shared_keys = current_keys; + first_obj = obj; + have_first = true; + } else { + // Check similarity threshold against the first object + double similarity = compute_object_similarity(first_obj, obj); + if (similarity < current_opts_->table_similarity_threshold) { + return false; // Objects are too dissimilar for table format + } + + // Intersect with current keys + std::set intersection; + std::set_intersection(shared_keys.begin(), shared_keys.end(), + current_keys.begin(), current_keys.end(), + std::inserter(intersection, intersection.begin())); + shared_keys = intersection; + } + + object_count++; + } + + if (object_count < current_opts_->min_table_rows) { + return false; + } + + // Require at least one common key for table formatting + if (shared_keys.empty()) { + return false; + } + + common_keys.assign(shared_keys.begin(), shared_keys.end()); + return true; +} + +inline double structure_analyzer::compute_object_similarity(const dom::object& a, + const dom::object& b) const { + std::set keys_a, keys_b; + for (dom::key_value_pair field : a) { + keys_a.insert(std::string(field.key)); + } + for (dom::key_value_pair field : b) { + keys_b.insert(std::string(field.key)); + } + + std::set intersection; + std::set_intersection(keys_a.begin(), keys_a.end(), + keys_b.begin(), keys_b.end(), + std::inserter(intersection, intersection.begin())); + + std::set union_set; + std::set_union(keys_a.begin(), keys_a.end(), + keys_b.begin(), keys_b.end(), + std::inserter(union_set, union_set.begin())); + + if (union_set.empty()) return 1.0; + return static_cast(intersection.size()) / static_cast(union_set.size()); +} + +inline layout_mode structure_analyzer::decide_layout(const element_metrics& metrics, + size_t depth, + size_t available_width) const { + if (metrics.child_count == 0) { + return layout_mode::INLINE; + } + + // Check inline feasibility + size_t indent_width = depth * current_opts_->indent_spaces; + if (metrics.can_inline && + metrics.estimated_inline_len + indent_width <= available_width) { + return layout_mode::INLINE; + } + + // Check table mode + if (metrics.is_uniform_array && !metrics.common_keys.empty()) { + return layout_mode::TABLE; + } + + // Check compact multiline + if (current_opts_->enable_compact_multiline && + metrics.complexity <= current_opts_->max_compact_array_complexity + 1) { + return layout_mode::COMPACT_MULTILINE; + } + + return layout_mode::EXPANDED; +} + +// +// Fractured Formatter Implementation +// + +inline fractured_formatter::fractured_formatter(const fractured_json_options& opts) + : options_(opts), column_widths_{} {} + +simdjson_inline void fractured_formatter::print_newline() { + if (current_layout_ == layout_mode::INLINE) { + return; // No newlines in inline mode + } + one_char('\n'); + current_line_length_ = 0; +} + +simdjson_inline void fractured_formatter::print_indents(size_t depth) { + if (current_layout_ == layout_mode::INLINE) { + return; // No indentation in inline mode + } + for (size_t i = 0; i < depth * options_.indent_spaces; i++) { + one_char(' '); + current_line_length_++; + } +} + +simdjson_inline void fractured_formatter::print_space() { + one_char(' '); + current_line_length_++; +} + +inline void fractured_formatter::set_layout_mode(layout_mode mode) { + current_layout_ = mode; +} + +inline layout_mode fractured_formatter::get_layout_mode() const { + return current_layout_; +} + +inline void fractured_formatter::set_depth(size_t depth) { + current_depth_ = depth; +} + +inline size_t fractured_formatter::get_depth() const { + return current_depth_; +} + +inline void fractured_formatter::track_line_length(size_t chars) { + current_line_length_ += chars; +} + +inline void fractured_formatter::reset_line_length() { + current_line_length_ = 0; +} + +inline size_t fractured_formatter::get_line_length() const { + return current_line_length_; +} + +inline bool fractured_formatter::should_break_line(size_t upcoming_length) const { + return (current_line_length_ + upcoming_length) > options_.max_total_line_length; +} + +inline const fractured_json_options& fractured_formatter::options() const { + return options_; +} + +inline void fractured_formatter::begin_table_row() { + in_table_mode_ = true; + current_column_ = 0; +} + +inline void fractured_formatter::end_table_row() { + in_table_mode_ = false; + current_column_ = 0; +} + +inline void fractured_formatter::set_column_widths(const std::vector& widths) { + column_widths_ = widths; +} + +inline size_t fractured_formatter::get_column_index() const { + return current_column_; +} + +inline void fractured_formatter::next_column() { + current_column_++; +} + +inline void fractured_formatter::align_to_column_width(size_t actual_width) { + if (current_column_ < column_widths_.size()) { + size_t target_width = column_widths_[current_column_]; + while (actual_width < target_width) { + one_char(' '); + actual_width++; + current_line_length_++; + } + } +} + +// +// Fractured String Builder Implementation +// + +inline fractured_string_builder::fractured_string_builder(const fractured_json_options& opts) + : format_(opts), analyzer_{}, options_(opts) {} + +inline void fractured_string_builder::append(const dom::element& value) { + // Phase 1: Analyze structure (metrics tree is built recursively) + element_metrics root_metrics = analyzer_.analyze(value, options_); + + // Phase 2: Format using metrics tree (passed through recursion) + format_element(value, root_metrics, 0); +} + +inline void fractured_string_builder::append(const dom::array& value) { + // Analyze the array to get proper metrics with children + element_metrics metrics = analyzer_.analyze_array(value, options_); + format_array(value, metrics, 0); +} + +inline void fractured_string_builder::append(const dom::object& value) { + // Analyze the object to get proper metrics with children + element_metrics metrics = analyzer_.analyze_object(value, options_); + format_object(value, metrics, 0); +} + +simdjson_inline void fractured_string_builder::clear() { + format_.clear(); + analyzer_.clear(); +} + +simdjson_inline std::string_view fractured_string_builder::str() const { + return format_.str(); +} + +inline void fractured_string_builder::format_element(const dom::element& elem, + const element_metrics& metrics, + size_t depth) { + switch (elem.type()) { + case dom::element_type::ARRAY: { + dom::array arr; + if (elem.get_array().get(arr) == SUCCESS) { + format_array(arr, metrics, depth); + } + break; + } + case dom::element_type::OBJECT: { + dom::object obj; + if (elem.get_object().get(obj) == SUCCESS) { + format_object(obj, metrics, depth); + } + break; + } + default: + format_scalar(elem); + break; + } +} + +inline void fractured_string_builder::format_array(const dom::array& arr, + const element_metrics& metrics, + size_t depth) { + switch (metrics.recommended_layout) { + case layout_mode::INLINE: + format_array_inline(arr, metrics); + break; + case layout_mode::COMPACT_MULTILINE: + format_array_compact_multiline(arr, metrics, depth); + break; + case layout_mode::TABLE: + format_array_as_table(arr, metrics, depth); + break; + case layout_mode::EXPANDED: + default: + format_array_expanded(arr, metrics, depth); + break; + } +} + +inline void fractured_string_builder::format_array_inline(const dom::array& arr, + const element_metrics& metrics) { + layout_mode prev_layout = format_.get_layout_mode(); + format_.set_layout_mode(layout_mode::INLINE); + + format_.start_array(); + + bool first = true; + bool empty = true; + size_t child_idx = 0; + for (dom::element elem : arr) { + empty = false; + if (!first) { + format_.comma(); + if (options_.comma_padding) { + format_.print_space(); + } + } else if (options_.simple_bracket_padding) { + format_.print_space(); + } + first = false; + const element_metrics& child_metrics = (child_idx < metrics.children.size()) + ? metrics.children[child_idx] : element_metrics{}; + format_element(elem, child_metrics, 0); + child_idx++; + } + + if (options_.simple_bracket_padding && !empty) { + format_.print_space(); + } + format_.end_array(); + + format_.set_layout_mode(prev_layout); +} + +inline void fractured_string_builder::format_array_compact_multiline(const dom::array& arr, + const element_metrics& metrics, + size_t depth) { + format_.start_array(); + format_.print_newline(); + format_.print_indents(depth + 1); + + size_t items_on_line = 0; + bool first = true; + size_t child_idx = 0; + + for (dom::element elem : arr) { + if (!first) { + format_.comma(); + + // Check if we should break to new line + if (items_on_line >= options_.max_items_per_line || + format_.should_break_line(20)) { // 20 is rough estimate for next item + format_.print_newline(); + format_.print_indents(depth + 1); + items_on_line = 0; + } else if (options_.comma_padding) { + format_.print_space(); + } + } + first = false; + + // Format element inline + layout_mode prev_layout = format_.get_layout_mode(); + format_.set_layout_mode(layout_mode::INLINE); + const element_metrics& child_metrics = (child_idx < metrics.children.size()) + ? metrics.children[child_idx] : element_metrics{}; + format_element(elem, child_metrics, depth + 1); + format_.set_layout_mode(prev_layout); + + items_on_line++; + child_idx++; + } + + format_.print_newline(); + format_.print_indents(depth); + format_.end_array(); +} + +inline void fractured_string_builder::format_array_as_table(const dom::array& arr, + const element_metrics& metrics, + size_t depth) { + const std::vector& columns = metrics.common_keys; + if (columns.empty()) { + format_array_expanded(arr, metrics, depth); + return; + } + + // Calculate column widths for alignment + std::vector col_widths = calculate_column_widths(arr, columns); + format_.set_column_widths(col_widths); + + format_.start_array(); + format_.print_newline(); + + bool first_row = true; + size_t child_idx = 0; + for (dom::element elem : arr) { + if (!first_row) { + format_.comma(); + format_.print_newline(); + } + first_row = false; + + format_.print_indents(depth + 1); + format_.begin_table_row(); + + // Format object as inline with aligned columns + dom::object obj; + if (elem.get_object().get(obj) != SUCCESS) { + child_idx++; + continue; + } + + // Get child metrics for this row (object) + const element_metrics& row_metrics = (child_idx < metrics.children.size()) + ? metrics.children[child_idx] : element_metrics{}; + + format_.start_object(); + if (options_.simple_bracket_padding) { + format_.print_space(); + } + + bool first_col = true; + const size_t num_columns = columns.size(); + + for (size_t col_idx = 0; col_idx < num_columns; col_idx++) { + const std::string& key = columns[col_idx]; + const bool is_last_col = (col_idx == num_columns - 1); + + if (!first_col) { + format_.comma(); + if (options_.comma_padding) { + format_.print_space(); + } + } + first_col = false; + + // Write key + format_.key(key); + if (options_.colon_padding) { + format_.print_space(); + } + + // Find the value for this key and its metrics + dom::element value; + bool found = false; + size_t field_idx = 0; + for (dom::key_value_pair field : obj) { + if (field.key == key) { + value = field.value; + found = true; + break; + } + field_idx++; + } + + // Write value + if (found) { + layout_mode prev_layout = format_.get_layout_mode(); + format_.set_layout_mode(layout_mode::INLINE); + const element_metrics& value_metrics = (field_idx < row_metrics.children.size()) + ? row_metrics.children[field_idx] : element_metrics{}; + format_element(value, value_metrics, depth + 1); + format_.set_layout_mode(prev_layout); + } else { + format_.null_atom(); + } + + // Only pad non-last columns to align values across rows + if (!is_last_col) { + size_t actual_len = found ? measure_value_length(value) : 4; // 4 for "null" + size_t target_width = col_widths[col_idx]; + while (actual_len < target_width) { + format_.one_char(' '); + actual_len++; + } + } + + format_.next_column(); + } + + if (options_.simple_bracket_padding) { + format_.print_space(); + } + format_.end_object(); + format_.end_table_row(); + child_idx++; + } + + format_.print_newline(); + format_.print_indents(depth); + format_.end_array(); +} + +inline void fractured_string_builder::format_array_expanded(const dom::array& arr, + const element_metrics& metrics, + size_t depth) { + format_.start_array(); + + bool empty = true; + bool first = true; + size_t child_idx = 0; + + for (dom::element elem : arr) { + empty = false; + if (!first) { + format_.comma(); + } + first = false; + + format_.print_newline(); + format_.print_indents(depth + 1); + const element_metrics& child_metrics = (child_idx < metrics.children.size()) + ? metrics.children[child_idx] : element_metrics{}; + format_element(elem, child_metrics, depth + 1); + child_idx++; + } + + if (!empty) { + format_.print_newline(); + format_.print_indents(depth); + } + format_.end_array(); +} + +inline void fractured_string_builder::format_object(const dom::object& obj, + const element_metrics& metrics, + size_t depth) { + if (metrics.recommended_layout == layout_mode::INLINE || metrics.can_inline) { + format_object_inline(obj, metrics); + } else { + format_object_expanded(obj, metrics, depth); + } +} + +inline void fractured_string_builder::format_object_inline(const dom::object& obj, + const element_metrics& metrics) { + layout_mode prev_layout = format_.get_layout_mode(); + format_.set_layout_mode(layout_mode::INLINE); + + format_.start_object(); + + bool empty = true; + bool first = true; + size_t child_idx = 0; + + for (dom::key_value_pair field : obj) { + empty = false; + if (!first) { + format_.comma(); + if (options_.comma_padding) { + format_.print_space(); + } + } else if (options_.simple_bracket_padding) { + format_.print_space(); + } + first = false; + + format_.key(field.key); + if (options_.colon_padding) { + format_.print_space(); + } + const element_metrics& child_metrics = (child_idx < metrics.children.size()) + ? metrics.children[child_idx] : element_metrics{}; + format_element(field.value, child_metrics, 0); + child_idx++; + } + + if (options_.simple_bracket_padding && !empty) { + format_.print_space(); + } + format_.end_object(); + + format_.set_layout_mode(prev_layout); +} + +inline void fractured_string_builder::format_object_expanded(const dom::object& obj, + const element_metrics& metrics, + size_t depth) { + format_.start_object(); + + bool empty = true; + bool first = true; + size_t child_idx = 0; + + for (dom::key_value_pair field : obj) { + empty = false; + if (!first) { + format_.comma(); + } + first = false; + + format_.print_newline(); + format_.print_indents(depth + 1); + format_.key(field.key); + if (options_.colon_padding) { + format_.print_space(); + } + const element_metrics& child_metrics = (child_idx < metrics.children.size()) + ? metrics.children[child_idx] : element_metrics{}; + format_element(field.value, child_metrics, depth + 1); + child_idx++; + } + + if (!empty) { + format_.print_newline(); + format_.print_indents(depth); + } + format_.end_object(); +} + +inline void fractured_string_builder::format_scalar(const dom::element& elem) { + switch (elem.type()) { + case dom::element_type::STRING: { + std::string_view str; + if (elem.get_string().get(str) == SUCCESS) { + format_.string(str); + } + break; + } + case dom::element_type::INT64: { + int64_t val; + if (elem.get_int64().get(val) == SUCCESS) { + format_.number(val); + } + break; + } + case dom::element_type::UINT64: { + uint64_t val; + if (elem.get_uint64().get(val) == SUCCESS) { + format_.number(val); + } + break; + } + case dom::element_type::DOUBLE: { + double val; + if (elem.get_double().get(val) == SUCCESS) { + format_.number(val); + } + break; + } + case dom::element_type::BOOL: { + bool val; + if (elem.get_bool().get(val) == SUCCESS) { + val ? format_.true_atom() : format_.false_atom(); + } + break; + } + case dom::element_type::NULL_VALUE: + format_.null_atom(); + break; + default: + break; + } +} + +inline size_t fractured_string_builder::measure_value_length(const dom::element& elem) const { + switch (elem.type()) { + case dom::element_type::STRING: { + std::string_view str; + if (elem.get_string().get(str) == SUCCESS) { + // Count actual escaped length + size_t len = 2; // quotes + for (char c : str) { + if (c == '"' || c == '\\' || static_cast(c) < 32) { + len += 2; // escape sequence + } else { + len += 1; + } + } + return len; + } + return 2; + } + case dom::element_type::INT64: { + int64_t val; + if (elem.get_int64().get(val) == SUCCESS) { + if (val == 0) return 1; + // Handle INT64_MIN specially to avoid overflow when negating + if (val == INT64_MIN) return 20; // "-9223372036854775808" is 20 characters + size_t len = (val < 0) ? 1 : 0; + int64_t abs_val = (val < 0) ? -val : val; + while (abs_val > 0) { len++; abs_val /= 10; } + return len; + } + return 1; + } + case dom::element_type::UINT64: { + uint64_t val; + if (elem.get_uint64().get(val) == SUCCESS) { + if (val == 0) return 1; + size_t len = 0; + while (val > 0) { len++; val /= 10; } + return len; + } + return 1; + } + case dom::element_type::DOUBLE: { + double val; + if (elem.get_double().get(val) == SUCCESS) { + char buf[32]; + int len = snprintf(buf, sizeof(buf), "%.17g", val); + return len > 0 ? static_cast(len) : 1; + } + return 1; + } + case dom::element_type::BOOL: { + bool val; + if (elem.get_bool().get(val) == SUCCESS) { + return val ? 4 : 5; // "true" or "false" + } + return 5; + } + case dom::element_type::NULL_VALUE: + return 4; // "null" + default: + return 4; + } +} + +inline std::vector fractured_string_builder::calculate_column_widths( + const dom::array& arr, + const std::vector& columns) const { + + std::vector widths(columns.size(), 0); + + for (dom::element elem : arr) { + dom::object obj; + if (elem.get_object().get(obj) != SUCCESS) { + continue; + } + + for (size_t col_idx = 0; col_idx < columns.size(); col_idx++) { + const std::string& key = columns[col_idx]; + + for (dom::key_value_pair field : obj) { + if (field.key == key) { + // Measure actual value length + size_t len = measure_value_length(field.value); + widths[col_idx] = (std::max)(widths[col_idx], len); + break; + } + } + } + } + + return widths; +} + +} // namespace internal + +// +// Public API Implementation +// + +template +std::string fractured_json(T x) { + return fractured_json(x, fractured_json_options{}); +} + +template +std::string fractured_json(T x, const fractured_json_options& options) { + internal::fractured_string_builder sb(options); + sb.append(x); + std::string_view result = sb.str(); + return std::string(result.data(), result.size()); +} + +#if SIMDJSON_EXCEPTIONS +template +std::string fractured_json(simdjson_result x) { + if (x.error()) { + throw simdjson_error(x.error()); + } + return fractured_json(x.value()); +} + +template +std::string fractured_json(simdjson_result x, const fractured_json_options& options) { + if (x.error()) { + throw simdjson_error(x.error()); + } + return fractured_json(x.value(), options); +} +#endif + +// Explicit template instantiations for common types +template std::string fractured_json(dom::element x); +template std::string fractured_json(dom::element x, const fractured_json_options& options); +template std::string fractured_json(dom::array x); +template std::string fractured_json(dom::array x, const fractured_json_options& options); +template std::string fractured_json(dom::object x); +template std::string fractured_json(dom::object x, const fractured_json_options& options); + +#if SIMDJSON_EXCEPTIONS +template std::string fractured_json(simdjson_result x); +template std::string fractured_json(simdjson_result x, const fractured_json_options& options); +#endif + +// +// String-based API for formatting any JSON string +// + +inline std::string fractured_json_string(std::string_view json_str) { + return fractured_json_string(json_str, fractured_json_options{}); +} + +inline std::string fractured_json_string(std::string_view json_str, + const fractured_json_options& options) { + // Parse the JSON string + dom::parser parser; + dom::element doc; + // Need to pad the string for simdjson + auto padded = padded_string(json_str); + auto error = parser.parse(padded).get(doc); + if (error) { + // If parsing fails, return the original string + return std::string(json_str); + } + return fractured_json(doc, options); +} + +} // namespace simdjson + +#endif // SIMDJSON_DOM_FRACTURED_JSON_INL_H diff --git a/include/simdjson/dom/fractured_json.h b/include/simdjson/dom/fractured_json.h new file mode 100644 index 000000000..7651c81fe --- /dev/null +++ b/include/simdjson/dom/fractured_json.h @@ -0,0 +1,159 @@ +#ifndef SIMDJSON_DOM_FRACTURED_JSON_H +#define SIMDJSON_DOM_FRACTURED_JSON_H + +#include "simdjson/dom/base.h" +#include "simdjson/dom/element.h" + +namespace simdjson { + +/** + * Configuration options for FracturedJson formatting. + * + * FracturedJson intelligently chooses between different layout strategies + * (inline, compact multiline, table, expanded) based on content complexity, + * length, and structure similarity. + */ +struct fractured_json_options { + /** + * Maximum total characters per line (default: 120). + * Content exceeding this will be expanded to multiple lines. + */ + size_t max_total_line_length = 120; + + /** + * Maximum length for inlined elements (default: 80). + * Simple arrays/objects shorter than this may be rendered inline. + */ + size_t max_inline_length = 80; + + /** + * Maximum nesting depth for inline rendering (default: 2). + * Elements with complexity exceeding this will be expanded. + * Complexity 0 = scalar, 1 = flat array/object, 2 = one level of nesting. + */ + size_t max_inline_complexity = 2; + + /** + * Maximum complexity for compact array formatting (default: 1). + * Arrays with elements of this complexity or less may have multiple + * items per line. + */ + size_t max_compact_array_complexity = 1; + + /** + * Number of spaces per indentation level (default: 4). + */ + size_t indent_spaces = 4; + + /** + * Enable tabular formatting for arrays of similar objects (default: true). + * When enabled, arrays of objects with identical keys are formatted + * as aligned tables. + */ + bool enable_table_format = true; + + /** + * Minimum number of rows to trigger table mode (default: 3). + */ + size_t min_table_rows = 3; + + /** + * Similarity threshold for table detection (default: 0.8). + * Objects must share at least this fraction of keys to be formatted + * as a table. + */ + double table_similarity_threshold = 0.8; + + /** + * Enable compact multiline arrays (default: true). + * When enabled, arrays of simple elements may have multiple items + * per line. + */ + bool enable_compact_multiline = true; + + /** + * Maximum array items per line in compact mode (default: 10). + */ + size_t max_items_per_line = 10; + + /** + * Add space inside brackets for simple containers (default: true). + * When true: { "key": "value" } + * When false: {"key": "value"} + */ + bool simple_bracket_padding = true; + + /** + * Add space after colons (default: true). + * When true: "key": "value" + * When false: "key":"value" + */ + bool colon_padding = true; + + /** + * Add space after commas in inline content (default: true). + * When true: [1, 2, 3] + * When false: [1,2,3] + */ + bool comma_padding = true; +}; + +/** + * Format JSON using FracturedJson formatting with default options. + * + * FracturedJson produces human-readable yet compact output by intelligently + * choosing between inline, compact multiline, table, and expanded layouts. + * + * dom::parser parser; + * element doc = parser.parse(json_string); + * cout << fractured_json(doc) << endl; + */ +template +std::string fractured_json(T x); + +/** + * Format JSON using FracturedJson formatting with custom options. + * + * dom::parser parser; + * element doc = parser.parse(json_string); + * fractured_json_options opts; + * opts.max_total_line_length = 80; + * cout << fractured_json(doc, opts) << endl; + */ +template +std::string fractured_json(T x, const fractured_json_options& options); + +#if SIMDJSON_EXCEPTIONS +template +std::string fractured_json(simdjson_result x); + +template +std::string fractured_json(simdjson_result x, const fractured_json_options& options); +#endif + +/** + * Format a JSON string using FracturedJson formatting. + * + * This is useful for formatting output from the builder/static reflection API + * or any valid JSON string. + * + * // With static reflection + * MyStruct data = {...}; + * auto minified = simdjson::to_json_string(data); + * auto formatted = simdjson::fractured_json_string(minified.value()); + * + * // Or with any JSON string + * std::string json = R"({"key":"value"})"; + * auto formatted = simdjson::fractured_json_string(json); + */ +inline std::string fractured_json_string(std::string_view json_str); + +/** + * Format a JSON string using FracturedJson formatting with custom options. + */ +inline std::string fractured_json_string(std::string_view json_str, + const fractured_json_options& options); + +} // namespace simdjson + +#endif // SIMDJSON_DOM_FRACTURED_JSON_H diff --git a/include/simdjson/generic/builder/amalgamated.h b/include/simdjson/generic/builder/amalgamated.h index d09f83c4d..f61b1d421 100644 --- a/include/simdjson/generic/builder/amalgamated.h +++ b/include/simdjson/generic/builder/amalgamated.h @@ -4,6 +4,7 @@ #include "simdjson/generic/builder/json_string_builder.h" #include "simdjson/generic/builder/json_builder.h" +#include "simdjson/generic/builder/fractured_json_builder.h" diff --git a/include/simdjson/generic/builder/dependencies.h b/include/simdjson/generic/builder/dependencies.h index fcab068f1..a5a26abf6 100644 --- a/include/simdjson/generic/builder/dependencies.h +++ b/include/simdjson/generic/builder/dependencies.h @@ -9,5 +9,6 @@ // All includes not under simdjson/generic/builder must be here! // Otherwise, amalgamation will fail. #include "simdjson/concepts.h" +#include "simdjson/dom/fractured_json.h" #endif // SIMDJSON_GENERIC_BUILDER_DEPENDENCIES_H \ No newline at end of file diff --git a/include/simdjson/generic/builder/fractured_json_builder.h b/include/simdjson/generic/builder/fractured_json_builder.h new file mode 100644 index 000000000..d5dda49fc --- /dev/null +++ b/include/simdjson/generic/builder/fractured_json_builder.h @@ -0,0 +1,117 @@ +#ifndef SIMDJSON_GENERIC_FRACTURED_JSON_BUILDER_H +#define SIMDJSON_GENERIC_FRACTURED_JSON_BUILDER_H + +#ifndef SIMDJSON_CONDITIONAL_INCLUDE +#include "simdjson/generic/builder/json_builder.h" +#include "simdjson/dom/fractured_json.h" +#endif // SIMDJSON_CONDITIONAL_INCLUDE + +#if SIMDJSON_STATIC_REFLECTION + +namespace simdjson { +namespace SIMDJSON_IMPLEMENTATION { +namespace builder { + +/** + * Serialize an object to a FracturedJson-formatted string. + * + * FracturedJson produces human-readable yet compact JSON output by intelligently + * choosing between different layout strategies (inline, compact multiline, table, + * expanded) based on content complexity, length, and structure similarity. + * + * This function combines the builder's serialization with FracturedJson formatting: + * 1. Serializes the object to minified JSON using reflection + * 2. Parses and reformats using FracturedJson + * + * Example: + * struct User { int id; std::string name; bool active; }; + * User user{1, "Alice", true}; + * auto result = to_fractured_json_string(user); + * // result.value() == "{ \"id\": 1, \"name\": \"Alice\", \"active\": true }" + * + * @param obj The object to serialize (must be a reflectable type) + * @param opts FracturedJson formatting options + * @param initial_capacity Initial buffer capacity for serialization + * @return The formatted JSON string, or an error + */ +template +simdjson_warn_unused simdjson_result to_fractured_json_string( + const T& obj, + const fractured_json_options& opts = {}, + size_t initial_capacity = string_builder::DEFAULT_INITIAL_CAPACITY) { + // Step 1: Serialize to minified JSON + std::string formatted; + auto error = to_json_string(obj, initial_capacity).get(formatted); + if (error) { + return error; + } + + // Step 2: Reformat with FracturedJson + return fractured_json_string(formatted, opts); +} + +/** + * Extract specific fields from an object and format with FracturedJson. + * + * Example: + * struct User { int id; std::string name; std::string email; bool active; }; + * User user{1, "Alice", "alice@example.com", true}; + * auto result = extract_fractured_json<"id", "name">(user); + * // result.value() == "{ \"id\": 1, \"name\": \"Alice\" }" + * + * @param obj The object to serialize + * @param opts FracturedJson formatting options + * @param initial_capacity Initial buffer capacity for serialization + * @return The formatted JSON string containing only the specified fields + */ +template + requires(std::is_class_v && (sizeof...(FieldNames) > 0)) +simdjson_warn_unused simdjson_result extract_fractured_json( + const T& obj, + const fractured_json_options& opts = {}, + size_t initial_capacity = string_builder::DEFAULT_INITIAL_CAPACITY) { + // Step 1: Extract fields to minified JSON + std::string formatted; + auto error = extract_from(obj, initial_capacity).get(formatted); + if (error) { + return error; + } + + // Step 2: Reformat with FracturedJson + return fractured_json_string(formatted, opts); +} + +} // namespace builder +} // namespace SIMDJSON_IMPLEMENTATION + +// Global namespace convenience functions + +/** + * Serialize an object to a FracturedJson-formatted string. + * Global namespace version for convenience. + */ +template +simdjson_warn_unused simdjson_result to_fractured_json_string( + const T& obj, + const fractured_json_options& opts = {}, + size_t initial_capacity = SIMDJSON_IMPLEMENTATION::builder::string_builder::DEFAULT_INITIAL_CAPACITY) { + return SIMDJSON_IMPLEMENTATION::builder::to_fractured_json_string(obj, opts, initial_capacity); +} +/** + * Extract specific fields from an object and format with FracturedJson. + * Global namespace version for convenience. + */ +template + requires(std::is_class_v && (sizeof...(FieldNames) > 0)) +simdjson_warn_unused simdjson_result extract_fractured_json( + const T& obj, + const fractured_json_options& opts = {}, + size_t initial_capacity = SIMDJSON_IMPLEMENTATION::builder::string_builder::DEFAULT_INITIAL_CAPACITY) { + return SIMDJSON_IMPLEMENTATION::builder::extract_fractured_json(obj, opts, initial_capacity); +} + +} // namespace simdjson + +#endif // SIMDJSON_STATIC_REFLECTION + +#endif // SIMDJSON_GENERIC_FRACTURED_JSON_BUILDER_H diff --git a/include/simdjson/internal/fractured_formatter.h b/include/simdjson/internal/fractured_formatter.h new file mode 100644 index 000000000..d2d2c092f --- /dev/null +++ b/include/simdjson/internal/fractured_formatter.h @@ -0,0 +1,161 @@ +#ifndef SIMDJSON_INTERNAL_FRACTURED_FORMATTER_H +#define SIMDJSON_INTERNAL_FRACTURED_FORMATTER_H + +#include "simdjson/dom/serialization.h" +#include "simdjson/dom/fractured_json.h" +#include "simdjson/internal/json_structure_analyzer.h" + +namespace simdjson { +namespace internal { + +/** + * Fractured JSON formatter using CRTP pattern. + * + * This formatter intelligently chooses between different layout modes + * (inline, compact multiline, table, expanded) based on pre-computed + * structure metrics. + */ +class fractured_formatter : public base_formatter { +public: + explicit fractured_formatter(const fractured_json_options& opts = {}); + + /** CRTP hook: print newline (context-aware) */ + simdjson_inline void print_newline(); + + /** CRTP hook: print indentation */ + simdjson_inline void print_indents(size_t depth); + + /** CRTP hook: print space (context-aware) */ + simdjson_inline void print_space(); + + /** Set the current layout mode */ + void set_layout_mode(layout_mode mode); + + /** Get the current layout mode */ + layout_mode get_layout_mode() const; + + /** Set current depth for formatting decisions */ + void set_depth(size_t depth); + + /** Get current depth */ + size_t get_depth() const; + + /** Track current line length for compact multiline decisions */ + void track_line_length(size_t chars); + + /** Reset line length (after newline) */ + void reset_line_length(); + + /** Get current line length */ + size_t get_line_length() const; + + /** Check if we should break to a new line in compact mode */ + bool should_break_line(size_t upcoming_length) const; + + /** Get the options */ + const fractured_json_options& options() const; + + // Table formatting support + /** Begin a table row */ + void begin_table_row(); + + /** End a table row */ + void end_table_row(); + + /** Set column widths for table alignment */ + void set_column_widths(const std::vector& widths); + + /** Get current column index in table mode */ + size_t get_column_index() const; + + /** Advance to next column */ + void next_column(); + + /** Add padding to align with column width */ + void align_to_column_width(size_t actual_width); + +private: + fractured_json_options options_; + layout_mode current_layout_ = layout_mode::EXPANDED; + size_t current_depth_ = 0; + size_t current_line_length_ = 0; + + // Table state + bool in_table_mode_ = false; + std::vector column_widths_; + size_t current_column_ = 0; +}; + +/** + * Specialized string builder for fractured JSON formatting. + * + * This builder performs two passes: + * 1. Analyze the structure to compute metrics + * 2. Format using the metrics to make layout decisions + */ +class fractured_string_builder { +public: + fractured_string_builder(const fractured_json_options& opts = {}); + + /** Append a DOM element with fractured formatting */ + void append(const dom::element& value); + + /** Append a DOM array with fractured formatting */ + void append(const dom::array& value); + + /** Append a DOM object with fractured formatting */ + void append(const dom::object& value); + + /** Clear the builder */ + simdjson_inline void clear(); + + /** Get the formatted string */ + simdjson_inline std::string_view str() const; + +private: + fractured_formatter format_; + structure_analyzer analyzer_; + fractured_json_options options_; + + /** Format an element using pre-computed metrics */ + void format_element(const dom::element& elem, const element_metrics& metrics, size_t depth); + + /** Format an array with the appropriate layout */ + void format_array(const dom::array& arr, const element_metrics& metrics, size_t depth); + + /** Format an array inline: [1, 2, 3] */ + void format_array_inline(const dom::array& arr, const element_metrics& metrics); + + /** Format an array with compact multiline: multiple items per line */ + void format_array_compact_multiline(const dom::array& arr, const element_metrics& metrics, size_t depth); + + /** Format an array as a table */ + void format_array_as_table(const dom::array& arr, const element_metrics& metrics, size_t depth); + + /** Format an array expanded: one item per line */ + void format_array_expanded(const dom::array& arr, const element_metrics& metrics, size_t depth); + + /** Format an object with the appropriate layout */ + void format_object(const dom::object& obj, const element_metrics& metrics, size_t depth); + + /** Format an object inline: {"a": 1, "b": 2} */ + void format_object_inline(const dom::object& obj, const element_metrics& metrics); + + /** Format an object expanded: one key per line */ + void format_object_expanded(const dom::object& obj, const element_metrics& metrics, size_t depth); + + /** Format a scalar value */ + void format_scalar(const dom::element& elem); + + /** Calculate column widths for table formatting */ + std::vector calculate_column_widths(const dom::array& arr, + const std::vector& columns) const; + + /** Measure the actual formatted length of a value (for alignment) */ + size_t measure_value_length(const dom::element& elem) const; +}; + +} // namespace internal +} // namespace simdjson + +#endif // SIMDJSON_INTERNAL_FRACTURED_FORMATTER_H diff --git a/include/simdjson/internal/json_structure_analyzer.h b/include/simdjson/internal/json_structure_analyzer.h new file mode 100644 index 000000000..cda34b1e3 --- /dev/null +++ b/include/simdjson/internal/json_structure_analyzer.h @@ -0,0 +1,169 @@ +#ifndef SIMDJSON_INTERNAL_JSON_STRUCTURE_ANALYZER_H +#define SIMDJSON_INTERNAL_JSON_STRUCTURE_ANALYZER_H + +#include "simdjson/dom/base.h" +#include "simdjson/dom/element.h" +#include "simdjson/dom/array.h" +#include "simdjson/dom/object.h" +#include "simdjson/dom/fractured_json.h" +#include "simdjson/internal/tape_type.h" + +#include +#include +#include +#include + +namespace simdjson { +namespace internal { + +/** + * Layout mode for fractured JSON formatting. + */ +enum class layout_mode { + INLINE, // Single line: [1, 2, 3] or {"a": 1} + COMPACT_MULTILINE, // Multiple items per line with breaks + TABLE, // Tabular format for arrays of similar objects + EXPANDED // Traditional multi-line with indentation +}; + +/** + * Metrics computed for a JSON element during structure analysis. + * These metrics drive layout decisions and contain child metrics for recursive formatting. + */ +struct element_metrics { + /** Nesting depth score (0 = scalar, 1 = flat container, etc.) */ + size_t complexity = 0; + + /** Estimated character length if rendered inline (minified + spaces) */ + size_t estimated_inline_len = 0; + + /** Number of direct children (0 for scalars) */ + size_t child_count = 0; + + /** Pre-computed: can this element be rendered inline? */ + bool can_inline = false; + + /** Is this an array where all elements have similar structure? */ + bool is_uniform_array = false; + + /** For uniform arrays of objects: the common keys */ + std::vector common_keys{}; + + /** Recommended layout mode based on analysis */ + layout_mode recommended_layout = layout_mode::EXPANDED; + + /** Child metrics for arrays and objects (in order of iteration) */ + std::vector children{}; +}; + +/** + * Analyzes JSON structure to compute metrics for formatting decisions. + * + * The analyzer performs a single pass over the DOM to compute: + * - Complexity (nesting depth) + * - Estimated inline length + * - Array uniformity for table detection + * + * Metrics are stored hierarchically with child metrics embedded in parent metrics, + * enabling efficient lookup during formatting without address-based caching. + */ +class structure_analyzer { +public: + /** Default constructor */ + structure_analyzer() : current_opts_(nullptr) {} + + /** Copy constructor - deleted since class has pointer member */ + structure_analyzer(const structure_analyzer&) = delete; + + /** Copy assignment - deleted since class has pointer member */ + structure_analyzer& operator=(const structure_analyzer&) = delete; + + /** Move constructor */ + structure_analyzer(structure_analyzer&&) = default; + + /** Move assignment */ + structure_analyzer& operator=(structure_analyzer&&) = default; + + /** + * Analyze a DOM element and compute metrics. + * @param elem The element to analyze + * @param opts Formatting options that affect metric computation + * @return Metrics for the root element (with child metrics embedded) + */ + element_metrics analyze(const dom::element& elem, + const fractured_json_options& opts); + + /** + * Clear state. + */ + void clear(); + + /** + * Analyze an array element directly (for standalone array formatting). + * @param arr The array to analyze + * @param opts Formatting options + * @return Metrics for the array + */ + element_metrics analyze_array(const dom::array& arr, + const fractured_json_options& opts); + + /** + * Analyze an object element directly (for standalone object formatting). + * @param obj The object to analyze + * @param opts Formatting options + * @return Metrics for the object + */ + element_metrics analyze_object(const dom::object& obj, + const fractured_json_options& opts); + +private: + const fractured_json_options* current_opts_ = nullptr; + + /** Recursive analysis implementation */ + element_metrics analyze_element(const dom::element& elem, size_t depth); + + /** Analyze scalar values (strings, numbers, booleans, null) */ + element_metrics analyze_scalar(const dom::element& elem); + + /** Analyze an array element */ + element_metrics analyze_array(const dom::array& arr, size_t depth); + + /** Analyze an object element */ + element_metrics analyze_object(const dom::object& obj, size_t depth); + + /** Estimate inline length for a string (including quotes and escaping) */ + size_t estimate_string_length(std::string_view s) const; + + /** Estimate inline length for a number */ + size_t estimate_number_length(double d) const; + size_t estimate_number_length(int64_t i) const; + size_t estimate_number_length(uint64_t u) const; + + /** + * Check if an array contains uniform objects suitable for table formatting. + * @param arr The array to check + * @param common_keys Output: keys common to all objects + * @return true if the array is suitable for table formatting + */ + bool check_array_uniformity(const dom::array& arr, + std::vector& common_keys) const; + + /** + * Compute similarity between two objects. + * @return Fraction of keys that are common (0.0 to 1.0) + */ + double compute_object_similarity(const dom::object& a, + const dom::object& b) const; + + /** + * Decide the recommended layout mode based on metrics and options. + */ + layout_mode decide_layout(const element_metrics& metrics, + size_t depth, + size_t available_width) const; +}; + +} // namespace internal +} // namespace simdjson + +#endif // SIMDJSON_INTERNAL_JSON_STRUCTURE_ANALYZER_H diff --git a/singleheader/amalgamate.py b/singleheader/amalgamate.py index 71cd85a28..978f01be4 100755 --- a/singleheader/amalgamate.py +++ b/singleheader/amalgamate.py @@ -231,7 +231,11 @@ class SimdjsonFile: @property def is_amalgamator(self): if self.implementation: - return self.root == 'src' or self.include_dir == 'simdjson' or self.filename == 'ondemand.h'or self.filename == 'builder.h' or self.filename == 'implementation.h' + return (self.root == 'src' or + self.include_dir == 'simdjson' or + self.filename == 'ondemand.h' or + self.filename == 'builder.h' or + self.filename == 'implementation.h') else: return self.filename == 'amalgamated.h' @@ -319,7 +323,7 @@ class SimdjsonRepository: result = None for relative_root in self.relative_roots: if os.path.exists(os.path.join(self.project_path, relative_root, filename)): - assert result is None, f"Error: File '{filename}' exists in both '{result}' and '{root}' directories. Files must be unique across roots to avoid ambiguity. Rename or move the duplicate file.{result}' and '{root}' directories. Files must be unique across roots to avoid ambiguity. Rename or move the duplicate file." + assert result is None, f"Error: File '{filename}' exists in both '{result}' and '{relative_root}' directories. Files must be unique across roots to avoid ambiguity. Rename or move the duplicate file." result = relative_root return result diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fa31baf78..9cffb049e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,6 +12,7 @@ add_cpp_test(unicode_tests LABELS dom acceptance per_implementation) add_cpp_test(minify_tests LABELS other acceptance per_implementation) add_cpp_test(padded_string_tests LABELS other acceptance ) add_cpp_test(prettify_tests LABELS other acceptance per_implementation) +add_cpp_test(fractured_json_tests LABELS other acceptance per_implementation) if(WIN32 AND BUILD_SHARED_LIBS) # Copy the simdjson dll into the tests directory diff --git a/tests/builder/CMakeLists.txt b/tests/builder/CMakeLists.txt index db31586fb..df8501cb9 100644 --- a/tests/builder/CMakeLists.txt +++ b/tests/builder/CMakeLists.txt @@ -7,6 +7,7 @@ if(SIMDJSON_STATIC_REFLECTION) add_cpp_test(static_reflection_comprehensive_tests LABELS ondemand acceptance per_implementation) add_cpp_test(static_reflection_edge_cases_tests LABELS ondemand acceptance per_implementation) add_cpp_test(static_reflection_enum_tests LABELS ondemand acceptance per_implementation) + add_cpp_test(static_reflection_fractured_json_tests LABELS ondemand acceptance per_implementation) endif(SIMDJSON_STATIC_REFLECTION) # Copy the simdjson dll into the tests directory if(MSVC AND BUILD_SHARED_LIBS) diff --git a/tests/builder/static_reflection_fractured_json_tests.cpp b/tests/builder/static_reflection_fractured_json_tests.cpp new file mode 100644 index 000000000..fb3469a54 --- /dev/null +++ b/tests/builder/static_reflection_fractured_json_tests.cpp @@ -0,0 +1,236 @@ +#include "simdjson.h" +#include "test_builder.h" +#include +#include +#include + +using namespace simdjson; + +// Test structures for FracturedJson builder integration +struct SimpleUser { + int id; + std::string name; + bool active; +}; + +struct UserWithEmail { + int id; + std::string name; + std::string email; + bool active; +}; + +struct NestedData { + std::string title; + std::vector users; + int count; +}; + +struct TableTestData { + std::vector records; +}; + +namespace fractured_json_builder_tests { + +// Test basic to_fractured_json_string functionality +bool basic_fractured_json_test() { + TEST_START(); +#if SIMDJSON_STATIC_REFLECTION + SimpleUser user{1, "Alice", true}; + + std::string formatted; + ASSERT_SUCCESS(simdjson::to_fractured_json_string(user).get(formatted)); + + // Verify it produces valid JSON by re-parsing + simdjson::dom::parser parser; + simdjson::dom::element doc; + ASSERT_SUCCESS(parser.parse(formatted).get(doc)); + + // Verify values are preserved + int64_t id; + ASSERT_SUCCESS(doc["id"].get_int64().get(id)); + ASSERT_EQUAL(id, 1); + + std::string_view name; + ASSERT_SUCCESS(doc["name"].get_string().get(name)); + ASSERT_EQUAL(name, "Alice"); + + bool active; + ASSERT_SUCCESS(doc["active"].get_bool().get(active)); + ASSERT_EQUAL(active, true); + + // Output should be inline since it's simple + std::cout << "Basic FracturedJson output: " << formatted << std::endl; +#endif + TEST_SUCCEED(); +} + +// Test with custom formatting options +bool custom_options_test() { + TEST_START(); +#if SIMDJSON_STATIC_REFLECTION + NestedData data{"Test", {{1, "Alice", true}, {2, "Bob", false}}, 2}; + + fractured_json_options opts; + opts.indent_spaces = 2; + opts.simple_bracket_padding = false; + + std::string formatted; + ASSERT_SUCCESS(simdjson::to_fractured_json_string(data, opts).get(formatted)); + + // Verify it produces valid JSON + simdjson::dom::parser parser; + simdjson::dom::element doc; + ASSERT_SUCCESS(parser.parse(formatted).get(doc)); + + std::cout << "Custom options output:\n" << formatted << std::endl; +#endif + TEST_SUCCEED(); +} + +// Test to_fractured_json with output parameter +bool output_param_test() { + TEST_START(); +#if SIMDJSON_STATIC_REFLECTION + SimpleUser user{42, "Charlie", false}; + + std::string output; + ASSERT_SUCCESS(simdjson::to_fractured_json_string(user).get(output)); + + // Verify it produces valid JSON + simdjson::dom::parser parser; + simdjson::dom::element doc; + ASSERT_SUCCESS(parser.parse(output).get(doc)); + + std::cout << "Output param result: " << output << std::endl; +#endif + TEST_SUCCEED(); +} + +// Test extract_fractured_json for partial serialization +bool extract_fields_test() { + TEST_START(); +#if SIMDJSON_STATIC_REFLECTION + UserWithEmail user{1, "Alice", "alice@example.com", true}; + + // Extract only id and name fields + std::string formatted; + ASSERT_SUCCESS(simdjson::extract_fractured_json<"id", "name">(user).get(formatted)); + + // Verify it produces valid JSON + simdjson::dom::parser parser; + simdjson::dom::element doc; + ASSERT_SUCCESS(parser.parse(formatted).get(doc)); + + // Should have id and name but NOT email and active + int64_t id; + ASSERT_SUCCESS(doc["id"].get_int64().get(id)); + ASSERT_EQUAL(id, 1); + + std::string_view name; + ASSERT_SUCCESS(doc["name"].get_string().get(name)); + ASSERT_EQUAL(name, "Alice"); + + // email should not be present + simdjson::dom::element email_elem; + auto err = doc["email"].get(email_elem); + ASSERT_TRUE(err != SUCCESS); // Should fail - field not present + + std::cout << "Extract fields output: " << formatted << std::endl; +#endif + TEST_SUCCEED(); +} + +// Test table formatting with array of similar objects +bool table_format_test() { + TEST_START(); +#if SIMDJSON_STATIC_REFLECTION + TableTestData data{ + {{1, "Alice", true}, {2, "Bob", false}, {3, "Carol", true}, {4, "Dave", false}} + }; + + fractured_json_options opts; + opts.enable_table_format = true; + opts.min_table_rows = 3; + + std::string formatted; + ASSERT_SUCCESS(simdjson::to_fractured_json_string(data, opts).get(formatted)); + + // Verify it produces valid JSON + simdjson::dom::parser parser; + simdjson::dom::element doc; + ASSERT_SUCCESS(parser.parse(formatted).get(doc)); + + std::cout << "Table format output:\n" << formatted << std::endl; +#endif + TEST_SUCCEED(); +} + +// Test roundtrip: serialize with reflection, parse back +bool roundtrip_test() { + TEST_START(); +#if SIMDJSON_STATIC_REFLECTION + SimpleUser original{123, "TestUser", true}; + + // Serialize with FracturedJson + std::string formatted; + ASSERT_SUCCESS(simdjson::to_fractured_json_string(original).get(formatted)); + + // Parse back using ondemand + simdjson::ondemand::parser parser; + simdjson::ondemand::document doc; + ASSERT_SUCCESS(parser.iterate(simdjson::pad(formatted)).get(doc)); + + // Deserialize back to struct + SimpleUser parsed; + ASSERT_SUCCESS(doc.get().get(parsed)); + + // Verify values match + ASSERT_EQUAL(parsed.id, original.id); + ASSERT_EQUAL(parsed.name, original.name); + ASSERT_EQUAL(parsed.active, original.active); + + std::cout << "Roundtrip test passed with: " << formatted << std::endl; +#endif + TEST_SUCCEED(); +} + +// Test global namespace functions +bool global_namespace_test() { + TEST_START(); +#if SIMDJSON_STATIC_REFLECTION + SimpleUser user{99, "GlobalTest", false}; + + // Test global to_fractured_json_string + std::string formatted; + ASSERT_SUCCESS(simdjson::to_fractured_json_string(user).get(formatted)); + // Verify output + simdjson::dom::parser parser; + simdjson::dom::element doc; + ASSERT_SUCCESS(parser.parse(simdjson::pad(formatted)).get(doc)); + std::cout << "Global namespace output: " << formatted << std::endl; +#endif + TEST_SUCCEED(); +} + +bool run() { + return basic_fractured_json_test() && + custom_options_test() && + output_param_test() && + extract_fields_test() && + table_format_test() && + roundtrip_test() && + global_namespace_test(); +} + +} // namespace fractured_json_builder_tests + +int main(int argc, char *argv[]) { + std::cout << "Running FracturedJson builder integration tests..." << std::endl; +#if SIMDJSON_STATIC_REFLECTION + std::cout << "Static reflection is ENABLED" << std::endl; +#else + std::cout << "Static reflection is DISABLED - tests will be skipped" << std::endl; +#endif + return test_main(argc, argv, fractured_json_builder_tests::run); +} diff --git a/tests/dom/basictests.cpp b/tests/dom/basictests.cpp index a49db253a..51e452558 100644 --- a/tests/dom/basictests.cpp +++ b/tests/dom/basictests.cpp @@ -462,6 +462,11 @@ namespace parse_api_tests { const padded_string BASIC_JSON = "[1,2,3]"_padded; const padded_string BASIC_NDJSON = "[1,2,3]\n[4,5,6]"_padded; const padded_string EMPTY_NDJSON = ""_padded; + // GCC 15 gives false positive -Wfree-nonheap-object warning here +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wfree-nonheap-object" +#endif bool parser_moving_parser() { std::cout << "Running " << __func__ << std::endl; typedef std::tuple,element> simdjson_tuple; @@ -479,6 +484,9 @@ namespace parse_api_tests { } return true; } +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic pop +#endif #if SIMDJSON_EXCEPTIONS bool issue_2375() { diff --git a/tests/fractured_json_tests.cpp b/tests/fractured_json_tests.cpp new file mode 100644 index 000000000..1ab2f56c2 --- /dev/null +++ b/tests/fractured_json_tests.cpp @@ -0,0 +1,1025 @@ +#include +#include +#include +#include +#include + +#include "simdjson.h" +#include "test_macros.h" + +// Helper function to count newlines in a string +static size_t count_newlines(const std::string& s) { + size_t count = 0; + for (char c : s) { + if (c == '\n') count++; + } + return count; +} + +// Test that fractured_json produces valid JSON that can be re-parsed +bool roundtrip_test() { + std::cout << "Running " << __func__ << std::endl; + + const char* json = R"({"users":[{"id":1,"name":"Alice","active":true},{"id":2,"name":"Bob","active":false}],"count":2})"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json, strlen(json)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + // Format with fractured_json + auto formatted = simdjson::fractured_json(doc); + std::cout << "Formatted output:\n" << formatted << std::endl; + + // Re-parse the formatted output + error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + // Format again and compare + auto formatted2 = simdjson::fractured_json(doc); + if (formatted != formatted2) { + std::cerr << "Formatted outputs differ!" << std::endl; + return false; + } + + std::cout << "Roundtrip test passed." << std::endl; + return true; +} + +// Test inline formatting for simple arrays +bool inline_array_test() { + std::cout << "Running " << __func__ << std::endl; + + const char* json = R"([1, 2, 3, 4, 5])"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json, strlen(json)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + simdjson::fractured_json_options opts; + opts.max_inline_length = 100; + opts.max_inline_complexity = 2; + + auto formatted = simdjson::fractured_json(doc, opts); + std::cout << "Formatted: " << formatted << std::endl; + + // Should be inline (single line, no newlines except possibly at end) + size_t newline_count = count_newlines(formatted); + if (newline_count > 1) { + std::cerr << "Simple array should be inline but has " << newline_count << " newlines" << std::endl; + return false; + } + + std::cout << "Inline array test passed." << std::endl; + return true; +} + +// Test inline formatting for simple objects +bool inline_object_test() { + std::cout << "Running " << __func__ << std::endl; + + const char* json = R"({"a": 1, "b": 2})"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json, strlen(json)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + simdjson::fractured_json_options opts; + opts.max_inline_length = 100; + + auto formatted = simdjson::fractured_json(doc, opts); + std::cout << "Formatted: " << formatted << std::endl; + + // Should be inline (single line) + size_t newline_count = count_newlines(formatted); + if (newline_count > 1) { + std::cerr << "Simple object should be inline but has " << newline_count << " newlines" << std::endl; + return false; + } + + std::cout << "Inline object test passed." << std::endl; + return true; +} + +// Test expanded formatting for complex nested structures +bool expanded_test() { + std::cout << "Running " << __func__ << std::endl; + + const char* json = R"({"level1":{"level2":{"level3":{"value":42}}}})"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json, strlen(json)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + simdjson::fractured_json_options opts; + opts.max_inline_complexity = 1; // Force expansion + + auto formatted = simdjson::fractured_json(doc, opts); + std::cout << "Formatted:\n" << formatted << std::endl; + + // Should have multiple lines (at least 3 newlines for nested structure) + size_t newline_count = count_newlines(formatted); + if (newline_count < 3) { + std::cerr << "Complex nested object should be expanded" << std::endl; + return false; + } + + std::cout << "Expanded test passed." << std::endl; + return true; +} + +// Test compact multiline arrays +bool compact_multiline_test() { + std::cout << "Running " << __func__ << std::endl; + + // Array with many simple elements + const char* json = R"([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20])"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json, strlen(json)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + simdjson::fractured_json_options opts; + opts.max_inline_length = 30; // Force compact multiline + opts.max_total_line_length = 60; + opts.enable_compact_multiline = true; + opts.max_items_per_line = 5; + + auto formatted = simdjson::fractured_json(doc, opts); + std::cout << "Formatted:\n" << formatted << std::endl; + + // Re-parse to verify validity + error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + std::cout << "Compact multiline test passed." << std::endl; + return true; +} + +// Test table formatting for uniform arrays +bool table_format_test() { + std::cout << "Running " << __func__ << std::endl; + + const char* json = R"([ + {"id": 1, "name": "Alice", "score": 95}, + {"id": 2, "name": "Bob", "score": 87}, + {"id": 3, "name": "Carol", "score": 92}, + {"id": 4, "name": "Dave", "score": 78} + ])"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json, strlen(json)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + simdjson::fractured_json_options opts; + opts.enable_table_format = true; + opts.min_table_rows = 3; + opts.max_inline_length = 80; + + auto formatted = simdjson::fractured_json(doc, opts); + std::cout << "Formatted (table):\n" << formatted << std::endl; + + // Re-parse to verify validity + error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + std::cout << "Table format test passed." << std::endl; + return true; +} + +// Test empty containers +bool empty_containers_test() { + std::cout << "Running " << __func__ << std::endl; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + + // Empty array + const char* empty_array = "[]"; + auto error = parser.parse(empty_array, strlen(empty_array)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + auto formatted = simdjson::fractured_json(doc); + std::cout << "Empty array: " << formatted << std::endl; + + if (formatted.find("[]") == std::string::npos) { + std::cerr << "Empty array should format as []" << std::endl; + return false; + } + + // Empty object + const char* empty_object = "{}"; + error = parser.parse(empty_object, strlen(empty_object)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + formatted = simdjson::fractured_json(doc); + std::cout << "Empty object: " << formatted << std::endl; + + if (formatted.find("{}") == std::string::npos) { + std::cerr << "Empty object should format as {}" << std::endl; + return false; + } + + std::cout << "Empty containers test passed." << std::endl; + return true; +} + +// Test all scalar types +bool scalar_types_test() { + std::cout << "Running " << __func__ << std::endl; + + const char* json = R"({ + "string": "hello world", + "int": 42, + "negative": -123, + "uint": 18446744073709551615, + "float": 3.14159, + "bool_true": true, + "bool_false": false, + "null_val": null + })"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json, strlen(json)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + auto formatted = simdjson::fractured_json(doc); + std::cout << "Formatted:\n" << formatted << std::endl; + + // Re-parse and verify + error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + std::cout << "Scalar types test passed." << std::endl; + return true; +} + +// Test string escaping +bool string_escaping_test() { + std::cout << "Running " << __func__ << std::endl; + + const char* json = R"({"escaped": "line1\nline2\ttab\"quote\\backslash"})"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json, strlen(json)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + auto formatted = simdjson::fractured_json(doc); + std::cout << "Formatted: " << formatted << std::endl; + + // Re-parse and verify + error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + // Verify the value is preserved + std::string_view val; + error = doc["escaped"].get_string().get(val); + if (error) { + std::cerr << "Get string error: " << error << std::endl; + return false; + } + + if (val != "line1\nline2\ttab\"quote\\backslash") { + std::cerr << "String value not preserved!" << std::endl; + return false; + } + + std::cout << "String escaping test passed." << std::endl; + return true; +} + +// Test custom options +bool custom_options_test() { + std::cout << "Running " << __func__ << std::endl; + + const char* json = R"({"a": [1, 2, 3], "b": {"x": 1}})"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json, strlen(json)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + // Test with different indent sizes + simdjson::fractured_json_options opts; + opts.indent_spaces = 2; + + auto formatted = simdjson::fractured_json(doc, opts); + std::cout << "With 2-space indent:\n" << formatted << std::endl; + + opts.indent_spaces = 8; + formatted = simdjson::fractured_json(doc, opts); + std::cout << "With 8-space indent:\n" << formatted << std::endl; + + // Re-parse to verify validity + error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + std::cout << "Custom options test passed." << std::endl; + return true; +} + +// Test deeply nested structures +bool deep_nesting_test() { + std::cout << "Running " << __func__ << std::endl; + + // Build deeply nested JSON + std::string json = "{"; + for (int i = 0; i < 10; i++) { + json += "\"level" + std::to_string(i) + "\":{"; + } + json += "\"value\":42"; + for (int i = 0; i < 10; i++) { + json += "}"; + } + json += "}"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + auto formatted = simdjson::fractured_json(doc); + std::cout << "Deeply nested (first 500 chars):\n" << formatted.substr(0, 500) << "..." << std::endl; + + // Re-parse to verify validity + error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + std::cout << "Deep nesting test passed." << std::endl; + return true; +} + +// Test mixed array types +bool mixed_array_test() { + std::cout << "Running " << __func__ << std::endl; + + const char* json = R"([1, "two", true, null, {"nested": "object"}, [1, 2, 3]])"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json, strlen(json)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + auto formatted = simdjson::fractured_json(doc); + std::cout << "Mixed array:\n" << formatted << std::endl; + + // Re-parse to verify validity + error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + std::cout << "Mixed array test passed." << std::endl; + return true; +} + +// Test fractured_json_string for formatting JSON strings +bool string_api_test() { + std::cout << "Running " << __func__ << std::endl; + + // Test with a minified JSON string (simulating output from to_json_string) + std::string minified = R"({"users":[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}],"count":2})"; + + auto formatted = simdjson::fractured_json_string(minified); + std::cout << "From string:\n" << formatted << std::endl; + + // Verify it's valid JSON + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + // Test with custom options + simdjson::fractured_json_options opts; + opts.indent_spaces = 2; + formatted = simdjson::fractured_json_string(minified, opts); + std::cout << "With 2-space indent:\n" << formatted << std::endl; + + std::cout << "String API test passed." << std::endl; + return true; +} + +// Test Unicode strings +bool unicode_test() { + std::cout << "Running " << __func__ << std::endl; + + const char* json = R"({ + "greeting": "Hello, 世界!", + "emoji": "🎉🚀✨", + "arabic": "مرحبا", + "russian": "Привет", + "mixed": "café résumé naïve" + })"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json, strlen(json)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + auto formatted = simdjson::fractured_json(doc); + std::cout << "Unicode formatted:\n" << formatted << std::endl; + + // Re-parse to verify validity + error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + std::cout << "Unicode test passed." << std::endl; + return true; +} + +// Test boundary number values +bool boundary_numbers_test() { + std::cout << "Running " << __func__ << std::endl; + + // Using string literals for extreme values to ensure valid JSON + const char* json = R"({ + "max_int64": 9223372036854775807, + "min_int64": -9223372036854775808, + "max_uint64": 18446744073709551615, + "zero": 0, + "neg_zero": -0, + "small_double": 2.2250738585072014e-308, + "large_double": 1.7976931348623157e+308, + "pi": 3.141592653589793 + })"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json, strlen(json)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + auto formatted = simdjson::fractured_json(doc); + std::cout << "Boundary numbers formatted:\n" << formatted << std::endl; + + // Re-parse to verify validity + error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + std::cout << "Boundary numbers test passed." << std::endl; + return true; +} + +// Test arrays of arrays (nested arrays) +bool nested_arrays_test() { + std::cout << "Running " << __func__ << std::endl; + + const char* json = R"([ + [[1, 2], [3, 4]], + [[5, 6], [7, 8]], + [[9, 10], [11, 12]] + ])"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json, strlen(json)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + auto formatted = simdjson::fractured_json(doc); + std::cout << "Nested arrays formatted:\n" << formatted << std::endl; + + // Re-parse to verify validity + error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + std::cout << "Nested arrays test passed." << std::endl; + return true; +} + +// Test empty strings +bool empty_string_test() { + std::cout << "Running " << __func__ << std::endl; + + const char* json = R"({"empty": "", "not_empty": "value", "also_empty": ""})"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json, strlen(json)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + auto formatted = simdjson::fractured_json(doc); + std::cout << "Empty string formatted: " << formatted << std::endl; + + // Re-parse to verify validity + error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + // Verify empty strings preserved + std::string_view val; + error = doc["empty"].get_string().get(val); + if (error || !val.empty()) { + std::cerr << "Empty string not preserved!" << std::endl; + return false; + } + + std::cout << "Empty string test passed." << std::endl; + return true; +} + +// Test keys with special characters +bool special_key_test() { + std::cout << "Running " << __func__ << std::endl; + + const char* json = R"({ + "normal": 1, + "with space": 2, + "with-dash": 3, + "with.dot": 4, + "with:colon": 5, + "with\"quote": 6, + "with\\backslash": 7 + })"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json, strlen(json)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + auto formatted = simdjson::fractured_json(doc); + std::cout << "Special keys formatted:\n" << formatted << std::endl; + + // Re-parse to verify validity + error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + std::cout << "Special key test passed." << std::endl; + return true; +} + +// Test non-uniform array (shouldn't trigger table mode) +bool non_uniform_array_test() { + std::cout << "Running " << __func__ << std::endl; + + const char* json = R"([ + {"id": 1, "name": "Alice"}, + {"id": 2, "email": "bob@example.com"}, + {"id": 3, "name": "Carol", "age": 30}, + {"different": "structure", "completely": true} + ])"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json, strlen(json)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + simdjson::fractured_json_options opts; + opts.enable_table_format = true; + opts.min_table_rows = 3; + + auto formatted = simdjson::fractured_json(doc, opts); + std::cout << "Non-uniform array formatted:\n" << formatted << std::endl; + + // Re-parse to verify validity + error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + std::cout << "Non-uniform array test passed." << std::endl; + return true; +} + +// Test very long string +bool long_string_test() { + std::cout << "Running " << __func__ << std::endl; + + // Create JSON with a long string value + std::string long_value(500, 'x'); + std::string json = R"({"short": "abc", "long": ")" + long_value + R"("})"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + auto formatted = simdjson::fractured_json(doc); + std::cout << "Long string formatted (first 200 chars):\n" << formatted.substr(0, 200) << "..." << std::endl; + + // Re-parse to verify validity + error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + // Verify long string preserved + std::string_view val; + error = doc["long"].get_string().get(val); + if (error || val.length() != 500) { + std::cerr << "Long string not preserved! Length: " << val.length() << std::endl; + return false; + } + + std::cout << "Long string test passed." << std::endl; + return true; +} + +// Test large array with many elements (triggers compact multiline) +bool large_array_test() { + std::cout << "Running " << __func__ << std::endl; + + // Create array with 100 elements + std::string json = "["; + for (int i = 0; i < 100; i++) { + if (i > 0) json += ","; + json += std::to_string(i); + } + json += "]"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + simdjson::fractured_json_options opts; + opts.max_inline_length = 50; + opts.max_total_line_length = 80; + opts.enable_compact_multiline = true; + opts.max_items_per_line = 10; + + auto formatted = simdjson::fractured_json(doc, opts); + std::cout << "Large array formatted (first 300 chars):\n" << formatted.substr(0, 300) << "..." << std::endl; + + // Re-parse to verify validity + error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + // Verify array length + simdjson::dom::array arr; + error = doc.get_array().get(arr); + if (error) { + std::cerr << "Get array error: " << error << std::endl; + return false; + } + + size_t count = 0; + for (auto elem : arr) { + (void)elem; + count++; + } + + if (count != 100) { + std::cerr << "Array count mismatch: " << count << " != 100" << std::endl; + return false; + } + + std::cout << "Large array test passed." << std::endl; + return true; +} + +// Test reflection API workflow (simulated) +bool reflection_workflow_test() { + std::cout << "Running " << __func__ << std::endl; + + // Simulate what to_json_string would produce from a struct + // struct User { int id; std::string name; bool active; }; + // std::vector users = {{1, "Alice", true}, {2, "Bob", false}}; + std::string minified = R"({"users":[{"id":1,"name":"Alice","active":true},{"id":2,"name":"Bob","active":false}],"total":2})"; + + // This is the typical workflow with reflection API: + // auto minified = simdjson::to_json_string(my_struct); + // auto formatted = simdjson::fractured_json_string(minified.value()); + + auto formatted = simdjson::fractured_json_string(minified); + std::cout << "Reflection workflow output:\n" << formatted << std::endl; + + // Verify it produces valid, readable JSON + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + // Test with custom options + simdjson::fractured_json_options opts; + opts.indent_spaces = 2; + opts.simple_bracket_padding = false; + + formatted = simdjson::fractured_json_string(minified, opts); + std::cout << "With custom options:\n" << formatted << std::endl; + + std::cout << "Reflection workflow test passed." << std::endl; + return true; +} + +// Test control characters +bool control_characters_test() { + std::cout << "Running " << __func__ << std::endl; + + // JSON with various control characters (escaped in the source) + const char* json = R"({"tab": "a\tb", "newline": "a\nb", "cr": "a\rb", "null_char": "a\u0000b"})"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json, strlen(json)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + auto formatted = simdjson::fractured_json(doc); + std::cout << "Control characters formatted: " << formatted << std::endl; + + // Re-parse to verify validity + error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + std::cout << "Control characters test passed." << std::endl; + return true; +} + +// Test single element containers +bool single_element_test() { + std::cout << "Running " << __func__ << std::endl; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + + // Single element array + const char* single_array = "[42]"; + auto error = parser.parse(single_array, strlen(single_array)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + auto formatted = simdjson::fractured_json(doc); + std::cout << "Single element array: " << formatted << std::endl; + + // Single key object + const char* single_object = R"({"only_key": "only_value"})"; + error = parser.parse(single_object, strlen(single_object)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + formatted = simdjson::fractured_json(doc); + std::cout << "Single key object: " << formatted << std::endl; + + std::cout << "Single element test passed." << std::endl; + return true; +} + +// Test option: disable compact multiline +bool disable_compact_test() { + std::cout << "Running " << __func__ << std::endl; + + const char* json = R"([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json, strlen(json)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + simdjson::fractured_json_options opts; + opts.max_inline_length = 20; + opts.enable_compact_multiline = false; // Force expanded mode + + auto formatted = simdjson::fractured_json(doc, opts); + std::cout << "Compact disabled (expanded):\n" << formatted << std::endl; + + // Re-parse to verify validity + error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + std::cout << "Disable compact test passed." << std::endl; + return true; +} + +// Test option: disable table format +bool disable_table_test() { + std::cout << "Running " << __func__ << std::endl; + + const char* json = R"([ + {"id": 1, "name": "Alice"}, + {"id": 2, "name": "Bob"}, + {"id": 3, "name": "Carol"} + ])"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json, strlen(json)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + simdjson::fractured_json_options opts; + opts.enable_table_format = false; // Disable table mode + + auto formatted = simdjson::fractured_json(doc, opts); + std::cout << "Table disabled:\n" << formatted << std::endl; + + // Re-parse to verify validity + error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + std::cout << "Disable table test passed." << std::endl; + return true; +} + +// Test option: no padding +bool no_padding_test() { + std::cout << "Running " << __func__ << std::endl; + + const char* json = R"({"a": [1, 2, 3], "b": {"x": 1}})"; + + simdjson::dom::parser parser; + simdjson::dom::element doc; + auto error = parser.parse(json, strlen(json)).get(doc); + if (error) { + std::cerr << "Parse error: " << error << std::endl; + return false; + } + + simdjson::fractured_json_options opts; + opts.simple_bracket_padding = false; + opts.colon_padding = false; + opts.comma_padding = false; + + auto formatted = simdjson::fractured_json(doc, opts); + std::cout << "No padding: " << formatted << std::endl; + + // Re-parse to verify validity + error = parser.parse(formatted).get(doc); + if (error) { + std::cerr << "Re-parse error: " << error << std::endl; + return false; + } + + std::cout << "No padding test passed." << std::endl; + return true; +} + +int main() { + bool success = true; + + // Original tests + success = roundtrip_test() && success; + success = inline_array_test() && success; + success = inline_object_test() && success; + success = expanded_test() && success; + success = compact_multiline_test() && success; + success = table_format_test() && success; + success = empty_containers_test() && success; + success = scalar_types_test() && success; + success = string_escaping_test() && success; + success = custom_options_test() && success; + success = deep_nesting_test() && success; + success = mixed_array_test() && success; + success = string_api_test() && success; + + // Edge case tests + success = unicode_test() && success; + success = boundary_numbers_test() && success; + success = nested_arrays_test() && success; + success = empty_string_test() && success; + success = special_key_test() && success; + success = non_uniform_array_test() && success; + success = long_string_test() && success; + success = large_array_test() && success; + success = reflection_workflow_test() && success; + success = control_characters_test() && success; + success = single_element_test() && success; + + // Option tests + success = disable_compact_test() && success; + success = disable_table_test() && success; + success = no_padding_test() && success; + + if (success) { + std::cout << "\nAll fractured_json tests passed! (" << 27 << " tests)" << std::endl; + return EXIT_SUCCESS; + } else { + std::cerr << "\nSome tests failed!" << std::endl; + return EXIT_FAILURE; + } +}