Newer
Older
navi-1 / navi / overlay / merge.py
"""RFC 7386 JSON merge patch — the one merge rule the overlay uses.

Why merge patch rather than a bespoke merger:

  - an absent field means "no opinion" — the local value stays (the whole
    point of the overlay);
  - ``null`` means "drop the override", so a field can be handed back to the
    local config without deleting anything from the local files;
  - objects merge recursively, which is what per-profile and per-MCP-server
    overlays need;
  - arrays are replaced wholesale — "add one tool" spells out the list, and
    nothing silently reorders a toolset.

The function never mutates its arguments: callers keep the local base intact
and can re-merge at any time (a refresh must not stack overlay on overlay).
"""

from __future__ import annotations

import copy


def merge_patch(target: object, patch: object) -> object:
    """Return *patch* applied over *target*, per RFC 7386."""
    if not isinstance(patch, dict):
        return copy.deepcopy(patch)
    result: dict = copy.deepcopy(target) if isinstance(target, dict) else {}
    for key, value in patch.items():
        if value is None:
            result.pop(key, None)
        else:
            result[key] = merge_patch(result.get(key), value)
    return result