180 lines
6.3 KiB
Python
180 lines
6.3 KiB
Python
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),
|
|
}
|