"""Core conversion logic for config-converter-cli.""" import io import json from typing import Any, Dict, Optional import tomlkit from ruamel.yaml import YAML from configconverter.exceptions import ( InvalidFormatError, ParseError, UnsupportedConversionError, ) def _toml_to_dict(obj: Any) -> Any: """Convert TOMLDocument to dict recursively.""" if hasattr(obj, 'unwrap'): return _toml_to_dict(obj.unwrap()) elif isinstance(obj, tomlkit.items.Array): return [_toml_to_dict(item) for item in obj] elif isinstance(obj, dict): return {key: _toml_to_dict(value) for key, value in obj.items()} return obj class Converter: """Bidirectional converter between JSON, YAML, and TOML formats.""" SUPPORTED_FORMATS = ("json", "yaml", "toml") def __init__(self): self.yaml_parser = YAML() self.yaml_parser.preserve_quotes = True self.yaml_parser.allow_duplicate_keys = False def detect_format(self, content: str) -> str: """Detect the format of the input content.""" content = content.strip() if not content: raise InvalidFormatError("Empty content") first_char = content[0] if first_char in ("{", "["): if self._is_valid_json(content): return "json" try: parsed = tomlkit.parse(content) if parsed: return "toml" except Exception: pass try: self.yaml_parser.load(content) return "yaml" except Exception: pass raise InvalidFormatError("Could not determine input format") def _is_valid_json(self, content: str) -> bool: """Check if content is valid JSON.""" try: json.loads(content) return True except json.JSONDecodeError: return False def convert(self, content: str, from_format: str, to_format: str) -> str: """Convert content from one format to another.""" from_format = from_format.lower() if from_format == "auto": from_format = self.detect_format(content) to_format = to_format.lower() if from_format not in self.SUPPORTED_FORMATS: raise UnsupportedConversionError(f"Unsupported source format: {from_format}") if to_format not in self.SUPPORTED_FORMATS: raise UnsupportedConversionError(f"Unsupported target format: {to_format}") data = self._parse(content, from_format) return self._serialize(data, to_format) def _parse(self, content: str, format: str) -> Any: """Parse content based on format.""" if format == "json": return self._parse_json(content) elif format == "yaml": return self._parse_yaml(content) elif format == "toml": return self._parse_toml(content) else: raise UnsupportedConversionError(f"Unsupported format: {format}") def _parse_json(self, content: str) -> Any: """Parse JSON content.""" try: return json.loads(content) except json.JSONDecodeError as e: raise ParseError( f"Invalid JSON syntax: {e.msg}", line_number=e.lineno, column=e.colno, context=e.doc, ) def _parse_yaml(self, content: str) -> Any: """Parse YAML content.""" try: return self.yaml_parser.load(content) except Exception as e: error_msg = str(e) line_info = "" if hasattr(e, "problem_mark") and e.problem_mark: line_info = f" (line {e.problem_mark.line + 1}, column {e.problem_mark.column + 1})" raise ParseError(f"Invalid YAML syntax: {error_msg}{line_info}") def _parse_toml(self, content: str) -> Any: """Parse TOML content.""" try: parsed = tomlkit.parse(content) return _toml_to_dict(parsed) except tomlkit.exceptions.ParseError as e: line_info = "" if e.line is not None: line_info = f" (line {e.line})" raise ParseError(f"Invalid TOML syntax: {e}{line_info}") def _serialize(self, data: Any, format: str) -> str: """Serialize data to the specified format.""" if format == "json": return self._to_json(data) elif format == "yaml": return self._to_yaml(data) elif format == "toml": return self._to_toml(data) else: raise UnsupportedConversionError(f"Unsupported format: {format}") def _to_json(self, data: Any, indent: int = 2) -> str: """Convert data to JSON string.""" return json.dumps(data, indent=indent, ensure_ascii=False) def _to_yaml(self, data: Any) -> str: """Convert data to YAML string.""" stream = io.StringIO() self.yaml_parser.dump(data, stream) return stream.getvalue() def _to_toml(self, data: Any) -> str: """Convert data to TOML string.""" return tomlkit.dumps(data) def serialize_with_indent( self, data: Any, format: str, indent: int = 2, compact: bool = False ) -> str: """Serialize data with custom indentation.""" if format == "json": if compact: return json.dumps(data, separators=(',', ':'), ensure_ascii=False) return json.dumps(data, indent=indent, ensure_ascii=False) elif format == "yaml": stream = io.StringIO() self.yaml_parser.indent(mapping=indent, sequence=indent, offset=2) self.yaml_parser.dump(data, stream) return stream.getvalue() elif format == "toml": return tomlkit.dumps(data) else: raise UnsupportedConversionError(f"Unsupported format: {format}") converter = Converter()