Add commands and formatters modules
Some checks failed
CI / test (3.10) (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.8) (push) Has been cancelled
CI / test (3.9) (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / typecheck (push) Has been cancelled
CI / build-package (push) Has been cancelled

This commit is contained in:
2026-01-29 10:49:03 +00:00
parent 15607b4310
commit 0bd3793525

View File

@@ -0,0 +1,259 @@
"""Interactive schema generator command for ConfigForge."""
import json
from pathlib import Path
from typing import Any, Dict, List, Optional
import click
from configforge.exceptions import (
FileOperationError,
InvalidConfigFormatError,
SchemaGenerationError,
)
from configforge.formatters import JSONHandler, YAMLHandler
from configforge.utils.file_parser import detect_format
@click.command("schema")
@click.option(
"--output",
"-o",
type=click.Path(),
help="Output file path for the generated schema",
)
@click.option(
"--format",
"-f",
type=click.Choice(["json", "yaml"]),
default="json",
help="Output format for the generated schema",
)
@click.option(
"--from-config",
type=click.Path(exists=True),
help="Generate schema from an existing config file",
)
def schema(
output: Optional[str],
format: str,
from_config: Optional[str],
) -> None:
"""Interactively generate a JSON Schema from configuration."""
try:
if from_config:
schema_dict = _generate_from_config(from_config)
else:
schema_dict = _run_interactive_wizard()
schema_output = _format_schema(schema_dict, format)
if output:
_write_schema(output, schema_output, format)
click.echo(f"Generated schema -> {output}")
else:
click.echo(schema_output)
except (InvalidConfigFormatError, FileOperationError, SchemaGenerationError) as e:
click.echo(f"Error: {e.message}", err=True)
click.get_current_context().exit(1)
def _generate_from_config(config_file: str) -> Dict[str, Any]:
"""Generate a schema from an existing configuration file."""
format_type = detect_format(config_file)
if format_type == "json":
data = JSONHandler.read(config_file)
elif format_type in ("yaml", "yml"):
data = YAMLHandler.read(config_file)
else:
raise InvalidConfigFormatError(
f"Unsupported format: {format_type}. Use JSON or YAML."
)
return _data_to_schema(data)
def _data_to_schema(data: Any, name: str = "") -> Dict[str, Any]:
"""Convert sample data to JSON Schema."""
if data is None:
return {"type": "null", "description": "Null value"}
if isinstance(data, bool):
return {"type": "boolean", "description": "Boolean value"}
if isinstance(data, (int, float)):
schema: Dict[str, Any] = {"type": "number", "description": "Number value"}
if isinstance(data, int):
schema["type"] = "integer"
return schema
if isinstance(data, str):
return {"type": "string", "description": "String value"}
if isinstance(data, list):
if not data:
return {"type": "array", "items": {"type": "any"}, "description": "Array of values"}
all_schemas = [_data_to_schema(item) for item in data]
merged = _merge_schemas(all_schemas)
return {"type": "array", "items": merged, "description": "Array of values"}
if isinstance(data, dict):
properties: Dict[str, Any] = {}
required: List[str] = []
for key, value in data.items():
properties[key] = _data_to_schema(value, key)
required.append(key)
schema: Dict[str, Any] = {
"type": "object",
"properties": properties,
"required": required,
"additionalProperties": False,
"description": "Configuration object"
}
return schema
return {"type": "any", "description": "Any value"}
def _merge_schemas(schemas: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Merge multiple schemas into one."""
if not schemas:
return {"type": "any"}
if len(schemas) == 1:
return schemas[0]
types = set()
for schema in schemas:
schema_type = schema.get("type")
if schema_type:
types.add(schema_type)
if len(types) > 1:
return {"type": " | ".join(sorted(types)), "description": "Union of multiple types"}
common_type = types.pop() if types else "any"
if common_type == "object":
all_props: Dict[str, Any] = {}
all_required = set()
for schema in schemas:
props = schema.get("properties", {})
all_props.update(props)
required = schema.get("required", [])
all_required.update(required)
return {
"type": "object",
"properties": all_props,
"required": list(all_required),
"additionalProperties": False
}
return {"type": common_type}
def _run_interactive_wizard() -> Dict[str, Any]:
"""Run the interactive schema wizard."""
click.echo("ConfigForge Schema Wizard")
click.echo("=" * 40)
schema_name = click.prompt("Schema name", default="Config")
schema_description = click.prompt("Schema description", default="")
properties: Dict[str, Any] = {}
required: List[str] = []
while True:
click.echo("")
add_property = click.confirm("Add a property?")
if not add_property:
break
prop_name = click.prompt("Property name")
prop_type = click.prompt(
"Property type",
type=click.Choice(["string", "integer", "number", "boolean", "array", "object"]),
default="string"
)
is_required = click.confirm("Is this property required?")
if is_required:
required.append(prop_name)
prop_description = click.prompt("Property description", default="")
prop_schema: Dict[str, Any] = {
"type": prop_type,
"description": prop_description
}
if prop_type == "string":
if click.confirm("Add string constraints?"):
min_length = click.prompt(" Min length (0 for none)", type=int, default=0)
if min_length > 0:
prop_schema["minLength"] = min_length
max_length = click.prompt(" Max length (0 for none)", type=int, default=0)
if max_length > 0:
prop_schema["maxLength"] = max_length
elif prop_type in ("integer", "number"):
if click.confirm("Add numeric constraints?"):
minimum = click.prompt(" Minimum value", type=float, default=None)
if minimum is not None:
prop_schema["minimum"] = minimum
maximum = click.prompt(" Maximum value", type=float, default=None)
if maximum is not None:
prop_schema["maximum"] = maximum
elif prop_type == "array":
if click.confirm("Add array constraints?"):
min_items = click.prompt(" Min items", type=int, default=0)
if min_items > 0:
prop_schema["minItems"] = min_items
max_items = click.prompt(" Max items", type=int, default=0)
if max_items > 0:
prop_schema["maxItems"] = max_items
properties[prop_name] = prop_schema
schema: Dict[str, Any] = {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": schema_name,
"type": "object",
"properties": properties,
"required": required,
"additionalProperties": False,
}
if schema_description:
schema["description"] = schema_description
return schema
def _format_schema(schema: Dict[str, Any], format: str) -> str:
"""Format schema to output string."""
if format == "json":
return JSONHandler.dumps(schema, indent=2)
else:
return YAMLHandler.dumps(schema)
def _write_schema(filepath: str, content: str, format: str) -> None:
"""Write schema to file."""
try:
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)