diff --git a/json_to_openapi/cli.py b/json_to_openapi/cli.py new file mode 100644 index 0000000..76bedca --- /dev/null +++ b/json_to_openapi/cli.py @@ -0,0 +1,265 @@ +"""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)