Initial upload: ConfDoc v0.1.0 - Config validation and documentation generator

This commit is contained in:
2026-01-31 07:10:10 +00:00
parent 908c08f4f4
commit 26ee4c81e8

235
src/confdoc/main.py Normal file
View File

@@ -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()