// Glaze Library // For the license information refer to glaze.hpp #pragma once // RFC 6902 JSON Patch implementation // https://datatracker.ietf.org/doc/html/rfc6902 #include #include #include "glaze/json/generic.hpp" namespace glz { // Trait to detect any generic_json specialization template struct is_generic_json_t : std::false_type {}; template class MapType> struct is_generic_json_t> : std::true_type {}; template concept is_generic_json = is_generic_json_t>::value; // RFC 6902 operation types enum struct patch_op_type : uint8_t { add, remove, replace, move, copy, test }; // Single patch operation // Uses std::optional for fields that are only present for certain operations: // - value: required for add, replace, test // - from: required for move, copy template struct patch_op_t { patch_op_type op{}; std::string path{}; std::optional value{}; std::optional from{}; }; // Backward-compatible aliases using patch_op = patch_op_t<>; // A patch document is an array of operations template using patch_document_t = std::vector>; using patch_document = patch_document_t<>; // Options for diff generation struct diff_opts { // Generate move operations when a value is removed and added elsewhere // Default: false (only generates add/remove/replace like nlohmann) bool detect_moves = false; // Generate copy operations when identical values appear in target // Default: false bool detect_copies = false; // For arrays: use LCS (longest common subsequence) for smarter diffs // Default: false (simpler index-based comparison) bool array_lcs = false; }; // Options for patch application struct patch_opts { // If true, create intermediate objects/arrays for add operations // Default: false (RFC 6902 compliant - parent must exist) bool create_intermediate = false; // If true, rollback all changes on any operation failure // Default: true (atomic application) // Note: Requires O(n) space and time for backup copy of large documents bool atomic = true; }; } template <> struct glz::meta { using enum glz::patch_op_type; static constexpr auto value = enumerate(add, remove, replace, move, copy, test); }; namespace glz { // ============================================================================ // Helper Functions // ============================================================================ // Escape special characters in JSON Pointer tokens (RFC 6901) [[nodiscard]] inline std::string escape_json_ptr(std::string_view token) { std::string result; result.reserve(token.size()); for (char c : token) { if (c == '~') result += "~0"; else if (c == '/') result += "~1"; else result += c; } return result; } // Unescape JSON Pointer tokens (RFC 6901) // Returns error for malformed sequences (e.g., "~" at end, "~2") [[nodiscard]] inline expected unescape_json_ptr(std::string_view token) { std::string result; result.reserve(token.size()); for (size_t i = 0; i < token.size(); ++i) { if (token[i] == '~') { if (i + 1 >= token.size()) { return unexpected(error_ctx{0, error_code::invalid_json_pointer}); } if (token[i + 1] == '0') { result += '~'; ++i; } else if (token[i + 1] == '1') { result += '/'; ++i; } else { return unexpected(error_ctx{0, error_code::invalid_json_pointer}); } } else { result += token[i]; } } return result; } // Deep equality comparison for generic values template requires is_generic_json [[nodiscard]] bool equal(const GenericType& a, const GenericType& b) noexcept { if (a.data.index() != b.data.index()) { return false; } // After index check, we know both variants hold the same type return std::visit( [&b](const auto& val_a) -> bool { using T = std::decay_t; const auto& val_b = std::get(b.data); // Safe: index check guarantees same type if constexpr (std::same_as) { return true; } else if constexpr (std::same_as) { if (val_a.size() != val_b.size()) return false; for (size_t i = 0; i < val_a.size(); ++i) { if (!equal(val_a[i], val_b[i])) return false; } return true; } else if constexpr (std::same_as) { if (val_a.size() != val_b.size()) return false; for (const auto& [key, value] : val_a) { auto it = val_b.find(key); if (it == val_b.end()) return false; if (!equal(value, it->second)) return false; } return true; } else { return val_a == val_b; } }, a.data); } namespace detail { // Parse a JSON pointer path into parent path and final token // Returns {parent_path, token} or error [[nodiscard]] inline expected, error_ctx> split_path( std::string_view path) { if (path.empty()) { return std::pair{"", ""}; } if (path[0] != '/') { return unexpected(error_ctx{0, error_code::invalid_json_pointer}); } auto last_slash = path.rfind('/'); std::string_view parent = path.substr(0, last_slash); std::string_view token_escaped = path.substr(last_slash + 1); auto token = unescape_json_ptr(token_escaped); if (!token) { return unexpected(token.error()); } return std::pair{parent, std::move(*token)}; } // Parse array index from string, returns nullopt for "-" (append) or invalid [[nodiscard]] inline std::optional parse_array_index(std::string_view token) { if (token.empty()) return std::nullopt; // "-" means append to end if (token == "-") return std::nullopt; // Leading zeros are not allowed (except "0" itself) if (token.size() > 1 && token[0] == '0') return std::nullopt; size_t index = 0; auto [ptr, ec] = std::from_chars(token.data(), token.data() + token.size(), index); if (ec != std::errc{} || ptr != token.data() + token.size()) { return std::nullopt; } return index; } } // namespace detail // Navigate to parent of the target path and return the final key/index // Examples: // path="" → error (root has no parent) // path="/foo" → returns {&root, "foo"} (root is parent of top-level keys) // path="/a/b" → returns {&root["a"], "b"} template requires is_generic_json [[nodiscard]] expected, error_ctx> navigate_to_parent(GenericType& root, std::string_view path) { if (path.empty()) { // Empty path refers to root itself, which has no parent return unexpected(error_ctx{0, error_code::nonexistent_json_ptr}); } auto split = detail::split_path(path); if (!split) { return unexpected(split.error()); } auto [parent_path, token] = *split; // Determine the parent: // - If parent_path is empty (e.g., path="/foo"), the parent is root // - Otherwise, navigate to parent_path (e.g., path="/a/b" → navigate to "/a") GenericType* parent = nullptr; if (parent_path.empty()) { parent = &root; } else { parent = navigate_to(&root, parent_path); } if (!parent) { return unexpected(error_ctx{0, error_code::nonexistent_json_ptr}); } return std::pair{parent, std::move(token)}; } // Insert a value at a JSON Pointer path // If create_intermediate is true, creates intermediate objects as needed (like mkdir -p) template requires is_generic_json [[nodiscard]] error_ctx insert_at(GenericType& root, std::string_view path, GenericType value, bool create_intermediate = false) { // Empty path means replace root if (path.empty()) { root = std::move(value); return {}; } auto nav = navigate_to_parent(root, path); if (!nav) { if (!create_intermediate) { return nav.error(); } // Create intermediate objects along the path // Parse path into segments and create missing intermediate objects if (path.empty() || path[0] != '/') { return error_ctx{0, error_code::invalid_json_pointer}; } GenericType* current = &root; std::string_view remaining = path.substr(1); // Skip leading '/' while (!remaining.empty()) { // Find next '/' or end size_t slash_pos = remaining.find('/'); std::string_view segment_escaped = (slash_pos == std::string_view::npos) ? remaining : remaining.substr(0, slash_pos); auto segment = unescape_json_ptr(segment_escaped); if (!segment) { return segment.error(); } bool is_last_segment = (slash_pos == std::string_view::npos); if (is_last_segment) { // Final segment - insert the value if (current->is_null()) { current->data = typename GenericType::object_t{}; } if (current->is_object()) { current->get_object()[*segment] = std::move(value); return {}; } else if (current->is_array()) { auto& arr = current->get_array(); if (*segment == "-") { arr.push_back(std::move(value)); return {}; } auto index_opt = detail::parse_array_index(*segment); if (!index_opt || *index_opt > arr.size()) { return error_ctx{0, error_code::nonexistent_json_ptr}; } arr.insert(arr.begin() + static_cast(*index_opt), std::move(value)); return {}; } else { return error_ctx{0, error_code::nonexistent_json_ptr}; } } else { // Intermediate segment - navigate or create if (current->is_null()) { current->data = typename GenericType::object_t{}; } if (current->is_object()) { auto& obj = current->get_object(); auto it = obj.find(*segment); if (it == obj.end()) { // Create intermediate object // Note: Use emplace + assignment to avoid brace init creating an array auto [new_it, inserted] = obj.emplace(*segment, GenericType{}); new_it->second.data = typename GenericType::object_t{}; it = new_it; } current = &it->second; } else if (current->is_array()) { auto index_opt = detail::parse_array_index(*segment); if (!index_opt) { return error_ctx{0, error_code::nonexistent_json_ptr}; } auto& arr = current->get_array(); if (*index_opt >= arr.size()) { return error_ctx{0, error_code::nonexistent_json_ptr}; } current = &arr[*index_opt]; } else { return error_ctx{0, error_code::nonexistent_json_ptr}; } remaining = remaining.substr(slash_pos + 1); } } return {}; } auto [parent, token] = *nav; if (parent->is_object()) { auto& obj = parent->get_object(); obj[token] = std::move(value); return {}; } else if (parent->is_array()) { auto& arr = parent->get_array(); // "-" means append if (token == "-") { arr.push_back(std::move(value)); return {}; } auto index_opt = detail::parse_array_index(token); if (!index_opt) { return error_ctx{0, error_code::nonexistent_json_ptr}; } size_t index = *index_opt; if (index > arr.size()) { return error_ctx{0, error_code::nonexistent_json_ptr}; } arr.insert(arr.begin() + static_cast(index), std::move(value)); return {}; } else { return error_ctx{0, error_code::nonexistent_json_ptr}; } } // Remove a value at a JSON Pointer path template requires is_generic_json [[nodiscard]] expected remove_at(GenericType& root, std::string_view path) { if (path.empty()) { // Cannot remove root return unexpected(error_ctx{0, error_code::nonexistent_json_ptr}); } auto nav = navigate_to_parent(root, path); if (!nav) { return unexpected(nav.error()); } auto [parent, token] = *nav; if (parent->is_object()) { auto& obj = parent->get_object(); auto it = obj.find(token); if (it == obj.end()) { return unexpected(error_ctx{0, error_code::nonexistent_json_ptr}); } GenericType removed = std::move(it->second); obj.erase(it); return removed; } else if (parent->is_array()) { auto& arr = parent->get_array(); auto index_opt = detail::parse_array_index(token); if (!index_opt) { return unexpected(error_ctx{0, error_code::nonexistent_json_ptr}); } size_t index = *index_opt; if (index >= arr.size()) { return unexpected(error_ctx{0, error_code::nonexistent_json_ptr}); } GenericType removed = std::move(arr[index]); arr.erase(arr.begin() + static_cast(index)); return removed; } else { return unexpected(error_ctx{0, error_code::nonexistent_json_ptr}); } } // ============================================================================ // Patch Operations // ============================================================================ namespace detail { // Add operation template requires is_generic_json [[nodiscard]] error_ctx apply_add(GenericType& doc, std::string_view path, const GenericType& value, const patch_opts& opts) { return insert_at(doc, path, value, opts.create_intermediate); } // Remove operation template requires is_generic_json [[nodiscard]] error_ctx apply_remove(GenericType& doc, std::string_view path) { auto result = remove_at(doc, path); if (!result) { return result.error(); } return {}; } // Replace operation template requires is_generic_json [[nodiscard]] error_ctx apply_replace(GenericType& doc, std::string_view path, const GenericType& value) { if (path.empty()) { doc = value; return {}; } GenericType* target = navigate_to(&doc, path); if (!target) { return error_ctx{0, error_code::nonexistent_json_ptr}; } *target = value; return {}; } // Move operation template requires is_generic_json [[nodiscard]] error_ctx apply_move(GenericType& doc, std::string_view from, std::string_view path, const patch_opts& opts) { // Check for move into self (path cannot start with from) if (path.starts_with(from) && (path.size() == from.size() || path[from.size()] == '/')) { return error_ctx{0, error_code::syntax_error}; } // Remove from source auto removed = remove_at(doc, from); if (!removed) { return error_ctx{0, error_code::nonexistent_json_ptr}; } // Add to destination return insert_at(doc, path, std::move(*removed), opts.create_intermediate); } // Copy operation template requires is_generic_json [[nodiscard]] error_ctx apply_copy(GenericType& doc, std::string_view from, std::string_view path, const patch_opts& opts) { const GenericType* source = navigate_to(&doc, from); if (!source) { return error_ctx{0, error_code::nonexistent_json_ptr}; } return insert_at(doc, path, *source, opts.create_intermediate); } // Test operation template requires is_generic_json [[nodiscard]] error_ctx apply_test(const GenericType& doc, std::string_view path, const GenericType& expected) { if (path.empty()) { if (!equal(doc, expected)) { return error_ctx{0, error_code::patch_test_failed}; } return {}; } const GenericType* target = navigate_to(&doc, path); if (!target) { return error_ctx{0, error_code::nonexistent_json_ptr}; } if (!equal(*target, expected)) { return error_ctx{0, error_code::patch_test_failed}; } return {}; } // Apply a single patch operation template requires is_generic_json [[nodiscard]] error_ctx apply_operation(GenericType& doc, const patch_op_t& op, const patch_opts& opts) { switch (op.op) { case patch_op_type::add: if (!op.value) { return error_ctx{0, error_code::missing_key}; } return apply_add(doc, op.path, *op.value, opts); case patch_op_type::remove: return apply_remove(doc, op.path); case patch_op_type::replace: if (!op.value) { return error_ctx{0, error_code::missing_key}; } return apply_replace(doc, op.path, *op.value); case patch_op_type::move: if (!op.from) { return error_ctx{0, error_code::missing_key}; } return apply_move(doc, *op.from, op.path, opts); case patch_op_type::copy: if (!op.from) { return error_ctx{0, error_code::missing_key}; } return apply_copy(doc, *op.from, op.path, opts); case patch_op_type::test: if (!op.value) { return error_ctx{0, error_code::missing_key}; } return apply_test(doc, op.path, *op.value); default: return error_ctx{0, error_code::syntax_error}; } } } // namespace detail // ============================================================================ // Diff Algorithm // ============================================================================ namespace detail { // Note: diff_opts (detect_moves, detect_copies, array_lcs) are reserved for future implementation. // Currently only generates add/remove/replace operations (like nlohmann/json default behavior). template requires is_generic_json void diff_impl(const GenericType& source, const GenericType& target, const std::string& path, patch_document_t& ops, [[maybe_unused]] const diff_opts& opts) { // Different types -> replace if (source.data.index() != target.data.index()) { ops.push_back({patch_op_type::replace, path, target, std::nullopt}); return; } std::visit( [&](const auto& src_val) { using T = std::decay_t; if constexpr (std::same_as) { // Both null, nothing to do } else if constexpr (std::same_as) { const auto& src_obj = src_val; const auto& tgt_obj = std::get(target.data); // Keys removed from source for (const auto& [key, value] : src_obj) { if (tgt_obj.find(key) == tgt_obj.end()) { ops.push_back( {patch_op_type::remove, path + "/" + escape_json_ptr(key), std::nullopt, std::nullopt}); } } // Keys added or modified for (const auto& [key, tgt_value] : tgt_obj) { std::string child_path = path + "/" + escape_json_ptr(key); auto it = src_obj.find(key); if (it == src_obj.end()) { ops.push_back({patch_op_type::add, child_path, tgt_value, std::nullopt}); } else { diff_impl(it->second, tgt_value, child_path, ops, opts); } } } else if constexpr (std::same_as) { const auto& src_arr = src_val; const auto& tgt_arr = std::get(target.data); size_t min_len = std::min(src_arr.size(), tgt_arr.size()); // Compare common elements for (size_t i = 0; i < min_len; ++i) { std::string child_path = path + "/" + std::to_string(i); diff_impl(src_arr[i], tgt_arr[i], child_path, ops, opts); } // Target has more elements -> add if (tgt_arr.size() > src_arr.size()) { for (size_t i = min_len; i < tgt_arr.size(); ++i) { std::string child_path = path + "/" + std::to_string(i); ops.push_back({patch_op_type::add, child_path, tgt_arr[i], std::nullopt}); } } // Source has more elements -> remove from end to start // IMPORTANT: We iterate backward (highest index first) so that when // the patch is applied, removing element N doesn't shift the indices // of elements we still need to remove (N-1, N-2, etc.) else if (src_arr.size() > tgt_arr.size()) { for (size_t i = src_arr.size(); i > min_len; --i) { std::string child_path = path + "/" + std::to_string(i - 1); ops.push_back({patch_op_type::remove, child_path, std::nullopt, std::nullopt}); } } } else { // All scalar types: double, int64_t, uint64_t, string, bool const auto& tgt_val = std::get(target.data); if (src_val != tgt_val) { ops.push_back({patch_op_type::replace, path, target, std::nullopt}); } } }, source.data); } } // namespace detail // ============================================================================ // Main API Functions // ============================================================================ // Generate a patch document that transforms 'source' into 'target' template requires is_generic_json [[nodiscard]] expected, error_ctx> diff(const GenericType& source, const GenericType& target, diff_opts opts = {}) { patch_document_t ops; detail::diff_impl(source, target, "", ops, opts); return ops; } // Apply a patch document to a JSON value (in-place modification) template requires is_generic_json [[nodiscard]] error_ctx patch(GenericType& document, const patch_document_t& ops, patch_opts opts = {}) { if (opts.atomic) { // Deep copy for rollback GenericType backup = document; for (const auto& op : ops) { auto ec = detail::apply_operation(document, op, opts); if (ec) { document = std::move(backup); // Rollback return ec; } } } else { for (const auto& op : ops) { auto ec = detail::apply_operation(document, op, opts); if (ec) { return ec; } } } return {}; } // Apply a patch document, returning a new value (non-mutating) template requires is_generic_json [[nodiscard]] expected patched(const GenericType& document, const patch_document_t& ops, patch_opts opts = {}) { GenericType result = document; opts.atomic = false; // No need for atomic since we're working on a copy auto ec = patch(result, ops, opts); if (ec) { return unexpected(ec); } return result; } // Convenience overload for JSON string input template requires is_generic_json [[nodiscard]] expected, error_ctx> diff(std::string_view source_json, std::string_view target_json, diff_opts opts = {}) { auto source = read_json(source_json); if (!source) { return unexpected(source.error()); } auto target = read_json(target_json); if (!target) { return unexpected(target.error()); } return diff(*source, *target, opts); } // Convenience overload for JSON string patch application template requires is_generic_json [[nodiscard]] expected patch_json(std::string_view document_json, std::string_view patch_json_str, patch_opts opts = {}) { auto document = read_json(document_json); if (!document) { return unexpected(document.error()); } auto ops = read_json>(patch_json_str); if (!ops) { return unexpected(ops.error()); } auto ec = patch(*document, *ops, opts); if (ec) { return unexpected(ec); } return write_json(*document); } // ============================================================================ // RFC 7386 JSON Merge Patch // https://datatracker.ietf.org/doc/html/rfc7386 // ============================================================================ namespace detail { // Recursive merge patch implementation // Modifies target in-place according to RFC 7386 algorithm template requires is_generic_json [[nodiscard]] error_ctx apply_merge_patch_impl(GenericType& target, const GenericType& patch, uint32_t depth = 0) { if (depth >= max_recursive_depth_limit) [[unlikely]] { return error_ctx{0, error_code::exceeded_max_recursive_depth}; } if (patch.is_object()) { // If target is not an object, replace with empty object if (!target.is_object()) { target.data = typename GenericType::object_t{}; } auto& target_obj = target.get_object(); const auto& patch_obj = patch.get_object(); for (const auto& [key, value] : patch_obj) { if (value.is_null()) { // Null means remove target_obj.erase(key); } else if (value.is_object()) { // Recursively merge objects // Use try_emplace to avoid double lookup auto [it, inserted] = target_obj.try_emplace(key, GenericType{}); auto ec = apply_merge_patch_impl(it->second, value, depth + 1); if (ec) { return ec; } } else { // Non-object value - direct assignment (no recursion needed) target_obj[key] = value; } } } else { // Non-object patch replaces target entirely target = patch; } return {}; } // Merge diff implementation - generates a merge patch from source to target template requires is_generic_json void merge_diff_impl(const GenericType& source, const GenericType& target, GenericType& patch) { // If types differ or source is not an object, return target as patch if (!source.is_object() || !target.is_object()) { patch = target; return; } // Both are objects - compute diff patch.data = typename GenericType::object_t{}; auto& patch_obj = patch.get_object(); const auto& source_obj = source.get_object(); const auto& target_obj = target.get_object(); // Check for removed keys (in source but not in target) for (const auto& [key, value] : source_obj) { if (target_obj.find(key) == target_obj.end()) { // Key was removed - emit null patch_obj.emplace(key, nullptr); } } // Check for added or modified keys for (const auto& [key, target_value] : target_obj) { auto source_it = source_obj.find(key); if (source_it == source_obj.end()) { // Key was added patch_obj.emplace(key, target_value); } else if (!equal(source_it->second, target_value)) { // Key was modified if (source_it->second.is_object() && target_value.is_object()) { // Both objects - recurse GenericType child_patch; merge_diff_impl(source_it->second, target_value, child_patch); patch_obj.emplace(key, std::move(child_patch)); } else { // Different types or non-objects - replace patch_obj.emplace(key, target_value); } } // If equal, no patch entry needed } } } // namespace detail // ============================================================================ // Merge Patch API Functions // ============================================================================ // Apply a merge patch to a JSON value (in-place modification) // Note: Uses template parameter to prevent ambiguity with string_view overload // (string literals would otherwise match both const generic& and string_view) template requires(is_generic_json && std::same_as, GenericType>) [[nodiscard]] inline error_ctx merge_patch(GenericType& target, G&& patch) { return detail::apply_merge_patch_impl(target, patch); } // Apply a merge patch, returning a new value (non-mutating) template requires(is_generic_json && std::same_as, GenericType>) [[nodiscard]] inline expected merge_patched(const GenericType& target, G&& patch) { GenericType result = target; auto ec = merge_patch(result, std::forward(patch)); if (ec) { return unexpected(ec); } return result; } // Convenience overload: apply merge patch from JSON string template requires is_generic_json [[nodiscard]] inline error_ctx merge_patch(GenericType& target, std::string_view patch_json) { auto patch = read_json(patch_json); if (!patch) { return patch.error(); } return merge_patch(target, *patch); } // Convenience overload: apply merge patch from JSON strings, return JSON string template requires is_generic_json [[nodiscard]] inline expected merge_patch_json(std::string_view target_json, std::string_view patch_json) { auto target = read_json(target_json); if (!target) { return unexpected(target.error()); } auto patch = read_json(patch_json); if (!patch) { return unexpected(patch.error()); } auto ec = merge_patch(*target, *patch); if (ec) { return unexpected(ec); } return write_json(*target); } // Convenience overload: apply merge patch from JSON strings, return generic template requires is_generic_json [[nodiscard]] inline expected merge_patched(std::string_view target_json, std::string_view patch_json) { auto target = read_json(target_json); if (!target) { return unexpected(target.error()); } auto patch = read_json(patch_json); if (!patch) { return unexpected(patch.error()); } auto ec = merge_patch(*target, *patch); if (ec) { return unexpected(ec); } return *target; } // Generate a merge patch that transforms 'source' into 'target' // Note: Due to null semantics, this cannot perfectly round-trip if // the target contains explicit null values (they would be interpreted as removals) template requires is_generic_json [[nodiscard]] inline expected merge_diff(const GenericType& source, const GenericType& target) { GenericType patch; detail::merge_diff_impl(source, target, patch); return patch; } // Convenience overload: generate merge patch from JSON strings, return JSON string template requires is_generic_json [[nodiscard]] inline expected merge_diff_json(std::string_view source_json, std::string_view target_json) { auto source = read_json(source_json); if (!source) { return unexpected(source.error()); } auto target = read_json(target_json); if (!target) { return unexpected(target.error()); } auto patch = merge_diff(*source, *target); if (!patch) { return unexpected(patch.error()); } return write_json(*patch); } // ============================================================================ // Struct/Type-based Merge Patch API // ============================================================================ // Concept to check if a type is not a generic_json specialization (to avoid ambiguous overloads) template concept not_generic = !is_generic_json>; // Concept to check if a type is a struct/class suitable for merge patching // (excludes generic and string-like types to avoid ambiguous overloads) template concept merge_patch_struct = not_generic && !std::convertible_to && !std::is_array_v>; // Apply a merge patch to a C++ struct/type (in-place modification) // Glaze's read_json already has merge-patch semantics: it only updates fields // present in the source and leaves other fields unchanged. // Note: Uses template parameter G constrained to generic_json to prevent implicit conversion // from string literals (which would cause ambiguity with the string_view overload) template requires(merge_patch_struct && is_generic_json>) [[nodiscard]] inline error_ctx merge_patch(T& target, G&& patch) { // Glaze can read directly from generic into structs, updating only fields present return read_json(target, patch); } // Apply a merge patch from JSON string to a C++ struct/type // Glaze's read_json already has merge-patch semantics: it only updates fields // present in the JSON and leaves other fields unchanged. template requires(merge_patch_struct) [[nodiscard]] inline error_ctx merge_patch(T& target, std::string_view patch_json) { // Direct read - Glaze only updates fields present in the JSON return read_json(target, patch_json); } // Apply a merge patch, returning a new struct (non-mutating) // Note: Uses template parameter G constrained to generic_json to prevent implicit conversion // from string literals (which would cause ambiguity with the string_view overload) template requires(merge_patch_struct && std::is_default_constructible_v && is_generic_json>) [[nodiscard]] inline expected merge_patched(const T& target, G&& patch) { T result = target; auto ec = merge_patch(result, std::forward(patch)); if (ec) { return unexpected(ec); } return result; } // Apply a merge patch from JSON string, returning a new struct (non-mutating) template requires(merge_patch_struct && std::is_default_constructible_v) [[nodiscard]] inline expected merge_patched(const T& target, std::string_view patch_json) { T result = target; auto ec = read_json(result, patch_json); if (ec) { return unexpected(ec); } return result; } // Generate a merge patch that transforms one struct into another template requires(merge_patch_struct && is_generic_json) [[nodiscard]] inline expected merge_diff(const T& source, const T& target) { // Serialize both to JSON then to generic auto source_json = write_json(source); if (!source_json) { return unexpected(source_json.error()); } auto source_generic = read_json(*source_json); if (!source_generic) { return unexpected(source_generic.error()); } auto target_json = write_json(target); if (!target_json) { return unexpected(target_json.error()); } auto target_generic = read_json(*target_json); if (!target_generic) { return unexpected(target_generic.error()); } return merge_diff(*source_generic, *target_generic); } // Generate a merge patch between two structs, return as JSON string template requires(merge_patch_struct) [[nodiscard]] inline expected merge_diff_json(const T& source, const T& target) { auto patch = merge_diff(source, target); if (!patch) { return unexpected(patch.error()); } return write_json(*patch); } }