Files
json-to-openapi/json_to_openapi/cli.py

266 lines
9.0 KiB
Python

"""CLI interface for JSON to OpenAPI Generator."""
import os
import sys
import glob
from typing import List, Optional
from dataclasses import dataclass
import click
from json_to_openapi.analyzer import parse_json_file
from json_to_openapi.schema_generator import OpenAPIGenerator, EndpointInfo
@dataclass
class CLIContext:
"""CLI context object."""
verbose: bool = False
quiet: bool = False
def echo(msg: str, ctx: CLIContext) -> None:
"""Print message if not in quiet mode."""
if not ctx.quiet:
click.echo(msg)
def debug(msg: str, ctx: CLIContext) -> None:
"""Print debug message if in verbose mode."""
if ctx.verbose:
click.echo(f"[DEBUG] {msg}")
@click.group()
@click.option("-v", "--verbose", is_flag=True, help="Enable verbose output")
@click.option("-q", "--quiet", is_flag=True, help="Suppress all output except errors")
@click.pass_context
def main(ctx: click.Context, verbose: bool, quiet: bool) -> None:
"""JSON to OpenAPI Generator - Convert JSON files to OpenAPI 3.0 specifications."""
ctx.ensure_object(CLIContext)
ctx.obj.verbose = verbose
ctx.obj.quiet = quiet
@main.command("convert")
@click.argument("input_file", type=click.Path(exists=True))
@click.option("-o", "--output", type=click.Path(), help="Output file path")
@click.option("-f", "--format", type=click.Choice(["yaml", "json"]), default="yaml",
help="Output format (default: yaml)")
@click.option("-t", "--title", default="My API", help="API title")
@click.option("-v", "--version", default="1.0.0", help="API version")
@click.option("-d", "--description", default="", help="API description")
@click.pass_context
def convert(
ctx: click.Context,
input_file: str,
output: Optional[str],
format: str,
title: str,
version: str,
description: str
) -> None:
"""Convert a single JSON file to OpenAPI specification."""
cli_ctx = ctx.obj
debug(f"Processing file: {input_file}", cli_ctx)
try:
data = parse_json_file(input_file)
debug("Parsed JSON successfully", cli_ctx)
generator = OpenAPIGenerator(title=title, version=version)
spec = generator.generate(data, description=description)
debug("Generated OpenAPI spec", cli_ctx)
if not output:
base_name = os.path.splitext(input_file)[0]
output = f"{base_name}.{format}"
generator.save_spec(spec, output, format)
echo(f"Generated: {output}", cli_ctx)
except Exception as e:
click.echo(f"Error: {e}", err=True)
sys.exit(1)
@main.command("interactive")
@click.option("-t", "--title", default="My API", help="API title")
@click.option("-v", "--version", default="1.0.0", help="API version")
@click.pass_context
def interactive(ctx: click.Context, title: str, version: str) -> None:
"""Interactive mode for customizing OpenAPI specification."""
cli_ctx = ctx.obj
echo("JSON to OpenAPI Generator - Interactive Mode", cli_ctx)
echo("=" * 50, cli_ctx)
input_file = click.prompt("Enter path to JSON file", type=click.Path(exists=True))
try:
data = parse_json_file(input_file)
endpoint_path = click.prompt("Endpoint path", default="/items", type=str)
endpoint_method = click.prompt("HTTP method", default="get",
type=click.Choice(["get", "post", "put", "delete", "patch"]))
endpoint_summary = click.prompt("Endpoint summary", default=f"Get {title} items", type=str)
endpoint_description = click.prompt("Endpoint description", default="", type=str)
add_tags = click.confirm("Add tags to endpoint?", default=False)
tags: List[str] = []
if add_tags:
tag_input = click.prompt("Enter tags (comma-separated)", type=str)
tags = [t.strip() for t in tag_input.split(",") if t.strip()]
output_format = click.prompt("Output format", default="yaml",
type=click.Choice(["yaml", "json"]))
output_path = click.prompt("Output file path", type=str)
endpoint = EndpointInfo(
path=endpoint_path,
method=endpoint_method,
summary=endpoint_summary,
description=endpoint_description,
tags=tags
)
generator = OpenAPIGenerator(title=title, version=version)
spec = generator.generate(data, endpoint=endpoint)
generator.save_spec(spec, output_path, output_format)
echo(f"\nGenerated: {output_path}", cli_ctx)
except Exception as e:
click.echo(f"Error: {e}", err=True)
sys.exit(1)
@main.command("batch")
@click.argument("input_pattern", type=str)
@click.option("-o", "--output-dir", type=click.Path(exists=False), help="Output directory")
@click.option("-f", "--format", type=click.Choice(["yaml", "json"]), default="yaml",
help="Output format (default: yaml)")
@click.option("-t", "--title", default="My API", help="API title")
@click.option("-v", "--version", default="1.0.0", help="API version")
@click.option("-c", "--combined", is_flag=True, help="Generate combined OpenAPI spec")
@click.option("-p", "--parallel", is_flag=True, help="Process files in parallel")
@click.pass_context
def batch(
ctx: click.Context,
input_pattern: str,
output_dir: Optional[str],
format: str,
title: str,
version: str,
combined: bool,
parallel: bool
) -> None:
"""Process multiple JSON files matching a pattern."""
cli_ctx = ctx.obj
files = glob.glob(input_pattern)
if not files:
echo(f"No files found matching: {input_pattern}", cli_ctx)
sys.exit(1)
echo(f"Found {len(files)} files to process", cli_ctx)
if output_dir and not os.path.exists(output_dir):
os.makedirs(output_dir)
debug(f"Created output directory: {output_dir}", cli_ctx)
try:
generator = OpenAPIGenerator(title=title, version=version)
if combined:
result = generator.generate_batch(files, output_dir, combined=True)
combined_spec = result["combined"]
output_path = os.path.join(output_dir or ".", f"combined.{format}")
generator.save_spec(combined_spec, output_path, format)
echo(f"Generated combined spec: {output_path}", cli_ctx)
for item in result["individual"]:
debug(f"Processed: {item['file']}", cli_ctx)
else:
for file_path in files:
data = parse_json_file(file_path)
spec = generator.generate(data)
base_name = os.path.splitext(os.path.basename(file_path))[0]
output_path = os.path.join(output_dir or ".", f"{base_name}.{format}")
generator.save_spec(spec, output_path, format)
echo(f"Generated: {output_path}", cli_ctx)
echo(f"Successfully processed {len(files)} files", cli_ctx)
except Exception as e:
click.echo(f"Error: {e}", err=True)
sys.exit(1)
@main.command("validate")
@click.argument("spec_file", type=click.Path(exists=True))
@click.pass_context
def validate(ctx: click.Context, spec_file: str) -> None:
"""Validate an OpenAPI specification file."""
cli_ctx = ctx.obj
try:
import yaml
with open(spec_file, 'r', encoding='utf-8') as f:
spec = yaml.safe_load(f)
errors: List[str] = []
if not isinstance(spec, dict):
errors.append("Spec must be a dictionary")
if "openapi" not in spec:
errors.append("Missing 'openapi' field")
elif spec["openapi"] != "3.0.3":
errors.append(f"Unsupported OpenAPI version: {spec.get('openapi')}")
if "info" not in spec:
errors.append("Missing 'info' field")
else:
info = spec["info"]
if "title" not in info:
errors.append("Missing 'info.title'")
if "version" not in info:
errors.append("Missing 'info.version'")
if "paths" not in spec:
errors.append("Missing 'paths' field")
if errors:
echo("Validation failed:", cli_ctx)
for error in errors:
echo(f" - {error}", cli_ctx)
sys.exit(1)
else:
echo("Validation passed! Spec is valid OpenAPI 3.0.3", cli_ctx)
except yaml.YAMLError as e:
click.echo(f"YAML error: {e}", err=True)
sys.exit(1)
except Exception as e:
click.echo(f"Error: {e}", err=True)
sys.exit(1)
@main.command("info")
@click.pass_context
def info(ctx: click.Context) -> None:
"""Display information about the tool."""
cli_ctx = ctx.obj
echo("JSON to OpenAPI Generator v1.0.0", cli_ctx)
echo("", cli_ctx)
echo("Commands:", cli_ctx)
echo(" convert Convert a JSON file to OpenAPI spec", cli_ctx)
echo(" interactive Interactive mode for customization", cli_ctx)
echo(" batch Process multiple JSON files", cli_ctx)
echo(" validate Validate an OpenAPI spec file", cli_ctx)
echo(" info Display this help information", cli_ctx)