from __future__ import annotations import json import re from pathlib import Path from typing import Any class Config: def __init__(self, file_path: str | Path | None = None) -> None: default_path = Path.home() / ".config" / "rs-cli" / "config.json" self.file_path = Path(file_path).expanduser() if file_path else default_path self.data: dict[str, Any] = {} def load(self) -> dict[str, Any]: self.file_path.parent.mkdir(parents=True, exist_ok=True) if not self.file_path.exists(): self.data = {} self.save() return self.data content = self.file_path.read_text(encoding="utf-8").strip() if not content: self.data = {} self.save() return self.data parsed = json.loads(content) if not isinstance(parsed, dict): raise ValueError("El archivo de configuracion debe contener un objeto JSON.") self.data = parsed return self.data def save(self) -> None: self.file_path.parent.mkdir(parents=True, exist_ok=True) self.file_path.write_text( json.dumps(self.data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8", ) def get(self, path: str, default: Any = None) -> Any: tokens = self._parse_path(path) current: Any = self.data for token in tokens: if isinstance(token, str): if not isinstance(current, dict) or token not in current: return default current = current[token] continue if not isinstance(current, list) or token >= len(current): return default current = current[token] return current def set(self, path: str, value: Any) -> None: tokens = self._parse_path(path) current: Any = self.data for idx, token in enumerate(tokens): is_last = idx == len(tokens) - 1 next_token = None if is_last else tokens[idx + 1] if isinstance(token, str): if not isinstance(current, dict): raise TypeError(f"No se puede usar clave '{token}' en un nodo no-dict.") if is_last: current[token] = value break expected_type = list if isinstance(next_token, int) else dict if token not in current or current[token] is None: current[token] = [] if expected_type is list else {} elif not isinstance(current[token], expected_type): current[token] = [] if expected_type is list else {} current = current[token] continue if not isinstance(current, list): raise TypeError(f"No se puede usar indice [{token}] en un nodo no-list.") while len(current) <= token: current.append(None) if is_last: current[token] = value break expected_type = list if isinstance(next_token, int) else dict if current[token] is None or not isinstance(current[token], expected_type): current[token] = [] if expected_type is list else {} current = current[token] self.save() def _parse_path(self, path: str) -> list[str | int]: if not path or not isinstance(path, str): raise ValueError("La ruta no puede estar vacia.") tokens: list[str | int] = [] for segment in path.split("."): if not segment: raise ValueError(f"Ruta invalida: '{path}'") index = 0 if segment[0] != "[": match = re.match(r"[^\[\]]+", segment) if not match: raise ValueError(f"Ruta invalida: '{path}'") key = match.group(0) tokens.append(key) index = len(key) while index < len(segment): if segment[index] != "[": raise ValueError(f"Ruta invalida: '{path}'") end = segment.find("]", index) if end == -1: raise ValueError(f"Ruta invalida: '{path}'") raw_index = segment[index + 1 : end] if not raw_index.isdigit(): raise ValueError(f"Indice invalido en ruta: '{path}'") tokens.append(int(raw_index)) index = end + 1 return tokens