""" Compare two JSON response bodies. CloudwalkResult: compare top-level `code` when both parse as objects; also deep-compare with optional key stripping. """ from __future__ import annotations import copy import json from typing import Any, List, Set # JSONPath-like: tuple of keys in order for nesting DEFAULT_STRIP_PATHS: Set[tuple] = { # ("data", "ts"), } def _strip(obj: Any, rules: Set[tuple], prefix: tuple) -> Any: if not isinstance(obj, (dict, list)): return obj if isinstance(obj, list): return [_strip(x, rules, prefix + (i,)) for i, x in enumerate(obj)] out = copy.deepcopy(obj) for k, v in list(obj.items()) if isinstance(obj, dict) else []: p = prefix + (k,) if p in rules: if k in out: del out[k] continue if isinstance(v, (dict, list)): out[k] = _strip(v, rules, p) return out def parse_json_loose(text: str) -> Any: if not (text or "").strip(): return None return json.loads(text) def normalize( data: Any, strip_paths: Set[tuple] | None = None, ) -> str: rules = strip_paths if strip_paths is not None else DEFAULT_STRIP_PATHS s = _strip(data, rules, tuple()) return json.dumps(s, ensure_ascii=False, sort_keys=True, default=str) def business_code(data: Any) -> str | None: if isinstance(data, dict) and "code" in data: c = data.get("code") return str(c) if c is not None else None return None def same_shape(old_text: str, new_text: str) -> bool: try: a = parse_json_loose(old_text) b = parse_json_loose(new_text) except json.JSONDecodeError: return False if type(a) != type(b): return False if isinstance(a, dict) and isinstance(b, dict) and a.keys() == b.keys(): return all(type(a[k]) is type(b[k]) for k in a) if isinstance(a, list) and isinstance(b, list): if len(a) == len(b) and len(a) == 0: return True return False def describe_diff( old_text: str, new_text: str, compare_norm: str = "deep" ) -> str: try: o = parse_json_loose(old_text) n = parse_json_loose(new_text) except json.JSONDecodeError as e: return f"json parse: {e}" if compare_norm == "code_only": c1, c2 = business_code(o), business_code(n) if c1 == c2: return "" return f"business code: old={c1!r} new={c2!r}" no = normalize(o) nn = normalize(n) if no == nn: return "" if business_code(o) is not None and business_code(n) is not None and business_code(o) != business_code( n ): return f"normalized JSON differs; first code: old={business_code(o)} new={business_code(n)}" return "normalized JSON differs (see full bodies in test log)"