Initial upload: ConfDoc v0.1.0 - Config validation and documentation generator
This commit is contained in:
235
src/confdoc/main.py
Normal file
235
src/confdoc/main.py
Normal 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()
|
||||
Reference in New Issue
Block a user