diff --git a/configforge/formatters/env_handler.py b/configforge/formatters/env_handler.py new file mode 100644 index 0000000..e146196 --- /dev/null +++ b/configforge/formatters/env_handler.py @@ -0,0 +1,114 @@ +"""ENV format handler for ConfigForge.""" + +import re +from typing import Any, Dict, List, Optional + +from configforge.exceptions import ConversionError + + +class ENVHandler: + """Handler for ENV (environment variable) configuration files.""" + + @staticmethod + def loads(content: str) -> Dict[str, Any]: + """Parse ENV file content.""" + result: Dict[str, Any] = {} + current_section: Optional[str] = None + + for line_num, line in enumerate(content.splitlines(), 1): + stripped = line.strip() + + if not stripped or stripped.startswith("#"): + continue + + if stripped.startswith("[") and stripped.endswith("]"): + current_section = stripped[1:-1].strip() + continue + + if "=" in stripped: + key, _, value = stripped.partition("=") + key = key.strip() + value = value.strip() + + if not key: + continue + + value = ENVHandler._parse_value(value) + + if current_section: + if current_section not in result: + result[current_section] = {} + result[current_section][key] = value + else: + result[key] = value + + return result + + @staticmethod + def _parse_value(value: str) -> Any: + """Parse a value from ENV format.""" + value = value.strip('"').strip("'") + + if value.lower() == "true": + return True + if value.lower() == "false": + return False + if value.lower() == "null": + return None + + try: + if "." in value: + return float(value) + return int(value) + except ValueError: + return value + + @staticmethod + def dumps(data: Dict[str, Any], section_name: Optional[str] = None) -> str: + """Serialize dictionary to ENV format.""" + lines = [] + + if section_name: + lines.append(f"[{section_name}]") + + for key, value in data.items(): + lines.append(f"{key}={ENVHandler._format_value(value)}") + + return "\n".join(lines) + + @staticmethod + def _format_value(value: Any) -> str: + """Format a value for ENV output.""" + if isinstance(value, bool): + return str(value).lower() + if value is None: + return "" + if isinstance(value, str): + if " " in value or '"' in value or "'" in value: + return f'"{value}"' + return value + return str(value) + + @staticmethod + def read(filepath: str) -> Dict[str, Any]: + """Read and parse ENV file.""" + try: + with open(filepath, "r", encoding="utf-8") as f: + content = f.read() + return ENVHandler.loads(content) + except FileNotFoundError: + raise ConversionError(f"File not found: {filepath}") + except PermissionError: + raise ConversionError(f"Permission denied: {filepath}") + + @staticmethod + def write(filepath: str, data: Dict[str, Any], section_name: Optional[str] = None) -> None: + """Write data to ENV file.""" + try: + content = ENVHandler.dumps(data, section_name) + with open(filepath, "w", encoding="utf-8") as f: + f.write(content) + except FileNotFoundError: + raise ConversionError(f"Directory not found for: {filepath}") + except PermissionError: + raise ConversionError(f"Permission denied to write: {filepath}")