diff --git a/src/mockapi/cli/main.py b/src/mockapi/cli/main.py new file mode 100644 index 0000000..9272d80 --- /dev/null +++ b/src/mockapi/cli/main.py @@ -0,0 +1,265 @@ +"""MockAPI CLI - Main entry point.""" + +import sys +from typing import Optional + +import click + +from mockapi import __version__ +from mockapi.core.spec_loader import SpecLoader +from mockapi.core.validator import OpenAPIValidator +from mockapi.core.server_generator import MockServerGenerator +from mockapi.core.config import Config +from mockapi.core.hot_reload import HotReloader + + +@click.group() +@click.version_option(version=__version__) +@click.pass_context +def cli(ctx): + """MockAPI - OpenAPI Mock Server Generator. + + Generate functional mock API servers from OpenAPI 3.x specifications. + """ + ctx.ensure_object(dict) + + +@cli.command() +@click.argument("spec_file", type=click.Path(exists=True)) +@click.option( + "--format", + "fmt", + type=click.Choice(["yaml", "json"]), + default=None, + help="Force spec format (auto-detected if not specified)", +) +def validate(spec_file: str, fmt: Optional[str]): + """Validate an OpenAPI specification file. + + SPEC_FILE: Path to the OpenAPI spec file (YAML or JSON) + """ + try: + loader = SpecLoader(spec_file, fmt) + spec = loader.load() + + validator = OpenAPIValidator(spec) + errors = validator.validate() + + if errors: + click.echo("Validation failed:", err=True) + for error in errors: + click.echo(f" - {error}", err=True) + sys.exit(1) + else: + click.echo("✓ Specification is valid!") + click.echo(f" Paths: {len(spec.get('paths', {}))}") + click.echo(f" Schemas: {len(spec.get('components', {}).get('schemas', {}))}") + + except Exception as e: + click.echo(f"Error: {e}", err=True) + sys.exit(1) + + +@cli.command() +@click.argument("spec_file", type=click.Path(exists=True)) +@click.option( + "--port", + "-p", + type=int, + default=None, + help="Port to run the mock server on (default: from config or 8080)", +) +@click.option( + "--host", + "-h", + default=None, + help="Host to bind to (default: from config or 0.0.0.0)", +) +@click.option( + "--delay", + "-d", + type=int, + default=None, + help="Fixed response delay in milliseconds", +) +@click.option( + "--random-delay", + is_flag=True, + default=None, + help="Use random delays instead of fixed", +) +@click.option( + "--config", + "-c", + type=click.Path(exists=True), + default=None, + help="Path to mockapi.yaml configuration file", +) +@click.option( + "--watch", + "-w", + is_flag=True, + default=False, + help="Enable hot-reload on spec file changes", +) +@click.option( + "--verbose", + "-v", + is_flag=True, + default=False, + help="Enable verbose output", +) +def start( + spec_file: str, + port: Optional[int], + host: Optional[str], + delay: Optional[int], + random_delay: Optional[bool], + config: Optional[str], + watch: bool, + verbose: bool, +): + """Start a mock API server from an OpenAPI specification. + + SPEC_FILE: Path to the OpenAPI spec file (YAML or JSON) + """ + try: + cfg = Config.load(config_path=config) + + if port is not None: + cfg.port = port + if host is not None: + cfg.host = host + if delay is not None: + cfg.delay = delay + if random_delay is not None: + cfg.random_delay = random_delay + + loader = SpecLoader(spec_file) + spec = loader.load() + + validator = OpenAPIValidator(spec) + errors = validator.validate() + if errors: + click.echo("Specification validation failed:", err=True) + for error in errors: + click.echo(f" - {error}", err=True) + sys.exit(1) + + if verbose: + click.echo(f"Starting mock server on {cfg.host}:{cfg.port}") + click.echo(f"Spec file: {spec_file}") + + generator = MockServerGenerator(spec, cfg) + app = generator.generate() + + if watch: + reloader = HotReloader(spec_file, port=cfg.port, host=cfg.host) + click.echo(f"Watching {spec_file} for changes...") + reloader.start_watching() + else: + import uvicorn + uvicorn.run( + app, + host=cfg.host, + port=cfg.port, + log_level="info" if verbose else "warning", + ) + + except Exception as e: + click.echo(f"Error: {e}", err=True) + if verbose: + import traceback + traceback.print_exc() + sys.exit(1) + + +@cli.command() +@click.argument("spec_file", type=click.Path(exists=True)) +@click.option( + "--output", + "-o", + type=click.Path(), + default=None, + help="Output file path (default: stdout)", +) +def generate(spec_file: str, output: Optional[str]): + """Generate code/structure from an OpenAPI spec (dry-run mode). + + SPEC_FILE: Path to the OpenAPI spec file (YAML or JSON) + """ + try: + loader = SpecLoader(spec_file) + spec = loader.load() + + validator = OpenAPIValidator(spec) + errors = validator.validate() + + if errors: + click.echo("Specification validation failed:", err=True) + for error in errors: + click.echo(f" - {error}", err=True) + sys.exit(1) + + paths = spec.get("paths", {}) + schemas = spec.get("components", {}).get("schemas", {}) + + info = spec.get("info", {}) + title = info.get("title", "API") + version = info.get("version", "1.0.0") + + output_lines = [ + f"# OpenAPI Spec: {title} v{version}", + f"# Endpoints: {len(paths)}", + f"# Schemas: {len(schemas)}", + "", + "## Paths", + ] + + for path, path_item in paths.items(): + for method, operation in path_item.items(): + if method in ["get", "post", "put", "delete", "patch", "options", "head"]: + op_id = operation.get("operationId", f"{method}_{path}") + summary = operation.get("summary", "") + output_lines.append(f" {method.upper():7} {path} -> {op_id}") + if summary: + output_lines.append(f" {summary}") + + if output: + with open(output, "w") as f: + f.write("\n".join(output_lines)) + click.echo(f"Generated output written to {output}") + else: + click.echo("\n".join(output_lines)) + + except Exception as e: + click.echo(f"Error: {e}", err=True) + sys.exit(1) + + +@cli.command() +@click.option( + "--config", + "-c", + type=click.Path(exists=True), + default=None, + help="Path to mockapi.yaml configuration file", +) +def show_config(config: Optional[str]): + """Show the current configuration (from file and defaults).""" + try: + cfg = Config.load(config_path=config) + click.echo("Current MockAPI Configuration:") + click.echo(f" Port: {cfg.port}") + click.echo(f" Host: {cfg.host}") + click.echo(f" Delay: {cfg.delay}ms") + click.echo(f" Random Delay: {cfg.random_delay}") + click.echo(f" Seed: {cfg.seed}") + click.echo(f" Validate: {cfg.validate_requests}") + except Exception as e: + click.echo(f"Error: {e}", err=True) + sys.exit(1) + + +if __name__ == "__main__": + cli()