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