diff --git a/configforge/commands/convert.py b/configforge/commands/convert.py new file mode 100644 index 0000000..7a3f4d1 --- /dev/null +++ b/configforge/commands/convert.py @@ -0,0 +1,120 @@ +"""Convert command for ConfigForge.""" + +import os +from pathlib import Path +from typing import Optional + +import click + +from configforge.exceptions import ( + ConversionError, + FileOperationError, + InvalidConfigFormatError, +) +from configforge.formatters import JSONHandler, YAMLHandler, TOMLHandler, ENVHandler +from configforge.utils.file_parser import detect_format + + +OUTPUT_FORMAT_MAP = { + "json": ("json", JSONHandler), + "yaml": ("yaml", YAMLHandler), + "toml": ("toml", TOMLHandler), + "env": ("env", ENVHandler), +} + + +@click.command("convert") +@click.argument("input_file", type=click.Path(exists=True)) +@click.option( + "--output", + "-o", + type=click.Path(), + help="Output file path (default: stdout)", +) +@click.option( + "--format", + "-f", + type=click.Choice(["json", "yaml", "toml", "env"]), + help="Output format (default: auto-detect from extension)", +) +@click.option( + "--indent", + is_flag=True, + default=False, + help="Use indented output for JSON", +) +def convert( + input_file: str, + output: Optional[str], + format: Optional[str], + indent: bool, +) -> None: + """Convert a configuration file between formats.""" + try: + input_format = detect_format(input_file) + + if format: + output_format = format + elif output: + output_format = detect_format(output) + else: + raise ConversionError( + "Output format must be specified via --format or output file extension" + ) + + data = _read_input(input_file, input_format) + content = _format_output(data, output_format, indent) + + if output: + _write_output(output, content) + click.echo(f"Converted {input_file} ({input_format}) -> {output} ({output_format})") + else: + click.echo(content) + + except (InvalidConfigFormatError, FileOperationError, ConversionError) as e: + click.echo(f"Error: {e.message}", err=True) + click.get_current_context().exit(1) + + +def _read_input(filepath: str, format_type: str) -> dict: + """Read and parse input file.""" + handlers = { + "json": JSONHandler, + "yaml": YAMLHandler, + "toml": TOMLHandler, + "env": ENVHandler, + } + + handler = handlers.get(format_type) + if not handler: + raise InvalidConfigFormatError(f"Unsupported format: {format_type}") + + return handler.read(filepath) + + +def _format_output(data: dict, format_type: str, indent: bool = False) -> str: + """Format data to output string.""" + handlers = { + "json": lambda d: JSONHandler.dumps(d, indent=2 if indent else 0), + "yaml": lambda d: YAMLHandler.dumps(d), + "toml": lambda d: TOMLHandler.dumps(d), + "env": lambda d: ENVHandler.dumps(d), + } + + handler = handlers.get(format_type) + if not handler: + raise InvalidConfigFormatError(f"Unsupported output format: {format_type}") + + return handler(data) + + +def _write_output(filepath: str, content: str) -> None: + """Write content to output file.""" + try: + os.makedirs(os.path.dirname(filepath) if os.path.dirname(filepath) else ".", exist_ok=True) + with open(filepath, "w", encoding="utf-8") as f: + f.write(content) + except PermissionError: + raise FileOperationError(f"Permission denied: {filepath}", filepath=filepath) + except OSError as e: + raise FileOperationError(f"Failed to write {filepath}: {str(e)}", filepath=filepath)