Files
Francisco Geiman Thiesen fc57c09cf0 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<fields...>(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<std::string> 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 4760ea7cd0.

* various minor changes

---------

Co-authored-by: Daniel Lemire <daniel@lemire.me>
2026-01-20 10:32:24 -05:00

160 lines
4.4 KiB
C++

#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 <class T>
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 <class T>
std::string fractured_json(T x, const fractured_json_options& options);
#if SIMDJSON_EXCEPTIONS
template <class T>
std::string fractured_json(simdjson_result<T> x);
template <class T>
std::string fractured_json(simdjson_result<T> 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