import json import traceback from pathlib import Path from types import SimpleNamespace from typing import Any, Optional, Literal, Union from pydantic import BaseModel from actions.file.json_file_saver import JsonFileSaver class JsonFileMerger: """ Classe utilitária para unir um novo JSON com um existente em disco, aplicando merge profundo e salvando o resultado final. Mantém o mesmo padrão seguro de conversão da classe JsonFileSaver. """ def __init__(self, saver: Optional[JsonFileSaver] = None): self.saver = saver or JsonFileSaver() # ------------------------------------------------------------ # Carrega JSON existente (se houver) # ------------------------------------------------------------ def _load_existing(self, file_path: Path) -> Any: if not file_path.exists(): return {} try: with open(file_path, "r", encoding=self.saver.encoding) as f: return json.load(f) except Exception: # Em caso de erro, considera como vazio para não quebrar o fluxo return {} # ------------------------------------------------------------ # Merge de listas (não duplica itens) # ------------------------------------------------------------ def _merge_lists(self, base_list: list, new_list: list) -> list: combined = base_list[:] for item in new_list: if item not in combined: combined.append(item) return combined # ------------------------------------------------------------ # Merge profundo (deep merge) para dicionários # ------------------------------------------------------------ def _deep_merge(self, base: dict, new: dict) -> dict: for key, value in new.items(): if key in base: # dict + dict → merge recursivo if isinstance(base[key], dict) and isinstance(value, dict): base[key] = self._deep_merge(base[key], value) # list + list → merge de listas elif isinstance(base[key], list) and isinstance(value, list): base[key] = self._merge_lists(base[key], value) # tipos diferentes ou simples → substitui else: base[key] = value else: base[key] = value return base # ------------------------------------------------------------ # Estratégias de merge # ------------------------------------------------------------ def _apply_strategy(self, existing: Any, new: Any, strategy: str) -> Any: # Se ambos forem listas, usa merge de listas if isinstance(existing, list) and isinstance(new, list): return self._merge_lists(existing, new) # Se tipos forem diferentes, substitui completamente if type(existing) is not type(new): return new # replace → ignora o que existia if strategy == "replace": return new # update → comportamento similar a dict.update (apenas para dicts) if strategy == "update": if isinstance(existing, dict) and isinstance(new, dict): existing.update(new) return existing # para outros tipos, apenas substitui return new # default / "deep" → deep merge para dicts if strategy == "deep": if isinstance(existing, dict) and isinstance(new, dict): return self._deep_merge(existing, new) # se não forem dicts, substitui return new # fallback: se for estratégia desconhecida, substitui return new # ------------------------------------------------------------ # NOVO MÉTODO — merge sem salvar (apenas processa) # ------------------------------------------------------------ def merge_data( self, existing_json: Any, new_json: Union[dict, BaseModel, SimpleNamespace], strategy: Literal["replace", "update", "deep"] = "deep", ) -> Any: new_clean = self.saver._convert(new_json) return self._apply_strategy(existing_json, new_clean, strategy) # ------------------------------------------------------------ # MÉTODO PRINCIPAL — AGORA USANDO merge_data() # ------------------------------------------------------------ def merge_and_save( self, new_json: Union[dict, BaseModel, SimpleNamespace], file_path: str, strategy: Literal["replace", "update", "deep"] = "deep", add_timestamp: bool = False, ) -> dict: """ Faz merge entre existing.json ← new_json e salva o resultado final. Parâmetros: new_json: - Dado novo que será mesclado ao JSON existente. Pode ser dict, BaseModel ou SimpleNamespace. file_path: - Caminho do arquivo JSON em disco. strategy: - "replace" → sobrescreve tudo - "update" → comportamento semelhante a dict.update - "deep" → merge recursivo (deep merge) para dicts e merge de listas sem duplicação add_timestamp: - Se True, adiciona timestamp no nome do arquivo ao salvar. Retorno: dict com: { "success": bool, "path": str, "size": int, "error": Optional[str], } """ try: path = Path(file_path) path.parent.mkdir(parents=True, exist_ok=True) # Carrega JSON existente existing = self._load_existing(path) # Novo merge utilizando merge_data() merged = self.merge_data(existing, new_json, strategy) # Salva em disco return self.saver.save( data=merged, file_path=str(path), as_json=True, add_timestamp=add_timestamp, mode="overwrite", ) except Exception as e: traceback.print_exc() return { "success": False, "path": file_path, "size": 0, "error": str(e), }