diff --git a/src/confdoc/main.py b/src/confdoc/main.py new file mode 100644 index 0000000..ed54d33 --- /dev/null +++ b/src/confdoc/main.py @@ -0,0 +1,235 @@ +import sys +from typing import Optional + +import typer +from rich import print as rprint + +from confdoc.parsers import ConfigParserFactory +from confdoc.validator import SchemaValidator +from confdoc.docs import DocGenerator +from confdoc.profiles import ProfileManager + +app = typer.Typer( + name="confdoc", + help="ConfDoc - Configuration Validation and Documentation Generator", + add_completion=False, +) + +@app.command() +def validate( + config_file: str = typer.Argument(..., help="Path to configuration file"), + schema_file: Optional[str] = typer.Option(None, "--schema", "-s", help="Path to schema file"), + format: str = typer.Option("auto", "--format", "-f", help="Config format: json, yaml, toml, auto"), + profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile to apply"), + output: str = typer.Option("text", "--output", "-o", help="Output format: text, json"), +) -> None: + """Validate a configuration file against a schema.""" + try: + if schema_file is None: + schema_file = "schema.json" + + parser_factory = ConfigParserFactory() + validator = SchemaValidator() + profile_manager = ProfileManager() + + config_format = format if format != "auto" else None + config_parser = parser_factory.get_parser(config_format) + + with open(config_file, 'r') as f: + config_content = f.read() + config = config_parser.parse(config_content) + + with open(schema_file, 'r') as f: + schema_content = f.read() + schema_parser = parser_factory.get_parser_for_file(schema_file) + schema = schema_parser.parse(schema_content) + + if profile: + profile = profile_manager.load_profile(profile) + if profile and "validation" in profile: + schema = profile_manager.apply_profile(schema, profile) + + is_valid, errors = validator.validate(config, schema) + + if output == "json": + result = {"valid": is_valid, "errors": errors} + import json + print(json.dumps(result, indent=2)) + else: + if is_valid: + rprint("[green]✓ Configuration is valid[/green]") + else: + rprint("[red]✗ Configuration validation failed[/red]") + for error in errors: + rprint(f"[red] - {error}[/red]") + + sys.exit(0 if is_valid else 1) + + except FileNotFoundError as e: + rprint(f"[red]Error: {e}[/red]") + sys.exit(1) + except Exception as e: + rprint(f"[red]Error: {e}[/red]") + sys.exit(1) + +@app.command() +def doc( + schema_file: str = typer.Argument(..., help="Path to schema file"), + output_file: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"), + title: Optional[str] = typer.Option(None, "--title", "-t", help="Document title"), + format: str = typer.Option("markdown", "--format", "-f", help="Output format: markdown"), +) -> None: + """Generate documentation from a schema.""" + try: + parser_factory = ConfigParserFactory() + doc_generator = DocGenerator() + + schema_parser = parser_factory.get_parser_for_file(schema_file) + + with open(schema_file, 'r') as f: + schema_content = f.read() + schema = schema_parser.parse(schema_content) + + doc_title = title or "Configuration Documentation" + documentation = doc_generator.generate(schema, doc_title) + + if output_file: + with open(output_file, 'w') as f: + f.write(documentation) + rprint(f"[green]Documentation written to {output_file}[/green]") + else: + print(documentation) + + except FileNotFoundError as e: + rprint(f"[red]Error: {e}[/red]") + sys.exit(1) + except Exception as e: + rprint(f"[red]Error: {e}[/red]") + sys.exit(1) + +@app.command() +def init( + output_file: str = typer.Argument("schema.json", help="Output schema file path"), + config_file: Optional[str] = typer.Option(None, "--config", "-c", help="Generate schema from existing config"), +) -> None: + """Initialize a new schema file.""" + try: + import json + + if config_file: + parser_factory = ConfigParserFactory() + schema_parser = parser_factory.get_parser_for_file(config_file) + + with open(config_file, 'r') as f: + config_content = f.read() + config = schema_parser.parse(config_content) + + schema = infer_schema_from_config(config) + else: + schema = get_starter_schema() + + with open(output_file, 'w') as f: + json.dump(schema, f, indent=2) + + rprint(f"[green]Schema template written to {output_file}[/green]") + + except Exception as e: + rprint(f"[red]Error: {e}[/red]") + sys.exit(1) + +def infer_schema_from_config(config: dict) -> dict: + """Infer a JSON schema from a configuration dictionary.""" + def infer_type(value): + if isinstance(value, bool): + return "boolean" + elif isinstance(value, int): + return "integer" + elif isinstance(value, float): + return "number" + elif isinstance(value, str): + return "string" + elif isinstance(value, list): + return "array" + elif isinstance(value, dict): + return "object" + return "string" + + def build_schema(obj, key=None): + if isinstance(obj, dict): + properties = {} + required = [] + for k, v in obj.items(): + prop_schema = build_schema(v, k) + properties[k] = prop_schema + if key: + required.append(k) + return { + "type": "object", + "properties": properties, + "required": required if required else [] + } + elif isinstance(obj, list): + if obj: + item_schema = build_schema(obj[0]) + return { + "type": "array", + "items": item_schema + } + return {"type": "array", "items": {}} + else: + return {"type": infer_type(obj)} + + schema = build_schema(config) + schema["$schema"] = "http://json-schema.org/draft-07/schema#" + return schema + +def get_starter_schema() -> dict: + """Get a starter schema template.""" + return { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Configuration Schema", + "description": "Auto-generated configuration schema", + "type": "object", + "properties": { + "app": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Application name" + }, + "version": { + "type": "string", + "description": "Application version" + }, + "debug": { + "type": "boolean", + "description": "Enable debug mode", + "default": False + } + }, + "required": ["name"] + }, + "database": { + "type": "object", + "properties": { + "host": { + "type": "string", + "description": "Database host" + }, + "port": { + "type": "integer", + "description": "Database port" + }, + "name": { + "type": "string", + "description": "Database name" + } + } + } + }, + "required": ["app"] + } + +def main() -> None: + app()