spikard-cli 0.15.6-rc.3

Command-line interface for building and validating Spikard applications
Documentation
#![allow(
    clippy::doc_markdown,
    clippy::items_after_statements,
    reason = "Test file for CLI commands"
)]

use anyhow::Result;
use std::path::PathBuf;
use tempfile::TempDir;

fn repo_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .and_then(|p| p.parent())
        .expect("CARGO_MANIFEST_DIR should be crates/spikard-cli")
        .to_path_buf()
}

#[test]
fn cli_features_command_runs() -> Result<()> {
    spikard_cli::cli::run_from(["spikard", "features"])?;
    Ok(())
}

#[test]
fn cli_init_command_creates_python_project_by_default() -> Result<()> {
    let tmp = TempDir::new()?;
    let project_name = "cli_init_demo";

    spikard_cli::cli::run_from([
        "spikard",
        "init",
        project_name,
        "--dir",
        tmp.path().to_string_lossy().as_ref(),
    ])?;

    let project_dir = tmp.path().join(project_name);
    assert!(project_dir.exists());
    assert!(project_dir.join("pyproject.toml").exists());
    assert!(project_dir.join("src").join(project_name).join("__init__.py").exists());
    assert!(project_dir.join("src").join(project_name).join("app.py").exists());
    assert!(project_dir.join("tests").join("test_app.py").exists());
    Ok(())
}

#[test]
#[allow(clippy::too_many_lines)]
fn cli_init_command_creates_expected_project_structures_for_each_binding() -> Result<()> {
    let cases = [
        (
            "python",
            "py_demo",
            vec![
                "pyproject.toml",
                "README.md",
                ".gitignore",
                "src/py_demo/__init__.py",
                "src/py_demo/app.py",
                "tests/test_app.py",
            ],
        ),
        (
            "typescript",
            "ts-demo",
            vec![
                "package.json",
                "tsconfig.json",
                "vitest.config.ts",
                ".gitignore",
                "README.md",
                "src/app.ts",
                "src/server.ts",
                "tests/app.spec.ts",
            ],
        ),
        (
            "rust",
            "rust_demo",
            vec![
                "Cargo.toml",
                "README.md",
                ".gitignore",
                "src/main.rs",
                "src/lib.rs",
                "tests/integration_test.rs",
            ],
        ),
        (
            "ruby",
            "ruby_demo",
            vec![
                "Gemfile",
                ".gitignore",
                "README.md",
                "bin/server",
                "lib/ruby_demo.rb",
                "sig/ruby_demo.rbs",
                "spec/ruby_demo_spec.rb",
                "spec/spec_helper.rb",
                ".rspec",
                "Rakefile",
            ],
        ),
        (
            "php",
            "php_demo",
            vec![
                "composer.json",
                "phpstan.neon",
                "phpunit.xml",
                ".gitignore",
                "README.md",
                "src/AppController.php",
                "bin/server.php",
                "tests/AppTest.php",
            ],
        ),
        (
            "elixir",
            "elixir_demo",
            vec![
                "mix.exs",
                ".formatter.exs",
                ".gitignore",
                "lib/elixir_demo.ex",
                "lib/elixir_demo/router.ex",
                "run.exs",
                "test/elixir_demo_test.exs",
                "test/test_helper.exs",
            ],
        ),
    ];

    for (lang, project_name, expected_paths) in cases {
        let tmp = TempDir::new()?;

        spikard_cli::cli::run_from([
            "spikard",
            "init",
            project_name,
            "--lang",
            lang,
            "--dir",
            tmp.path().to_string_lossy().as_ref(),
        ])?;

        let project_dir = tmp.path().join(project_name);
        assert!(project_dir.exists(), "expected {lang} project root");

        for expected in expected_paths {
            assert!(
                project_dir.join(expected).exists(),
                "expected {lang} to create {expected}"
            );
        }
    }

    Ok(())
}

#[test]
fn cli_validate_asyncapi_command_runs() -> Result<()> {
    let schema = repo_root().join("testing_data/schemas/chat-service.asyncapi.yaml");
    spikard_cli::cli::run_from(["spikard", "validate-asyncapi", schema.to_string_lossy().as_ref()])?;
    Ok(())
}

#[test]
fn cli_testing_asyncapi_fixtures_generates_output() -> Result<()> {
    let schema = repo_root().join("testing_data/schemas/chat-service.asyncapi.yaml");
    let tmp = TempDir::new()?;
    spikard_cli::cli::run_from([
        "spikard",
        "testing",
        "asyncapi",
        "fixtures",
        schema.to_string_lossy().as_ref(),
        "-o",
        tmp.path().to_string_lossy().as_ref(),
    ])?;

    fn count_json_files(root: &std::path::Path) -> std::io::Result<usize> {
        let mut count = 0usize;
        for entry in std::fs::read_dir(root)? {
            let entry = entry?;
            let path = entry.path();
            if path.is_dir() {
                count += count_json_files(&path)?;
            } else if path.extension().and_then(|s| s.to_str()) == Some("json") {
                count += 1;
            }
        }
        Ok(count)
    }

    let json_files = count_json_files(tmp.path())?;

    assert!(json_files > 0, "expected fixture JSON files to be generated");
    Ok(())
}

#[test]
fn cli_generate_asyncapi_handlers_writes_file() -> Result<()> {
    let schema = repo_root().join("testing_data/schemas/chat-service.asyncapi.yaml");
    let tmp = TempDir::new()?;
    let output = tmp.path().join("handlers.py");

    spikard_cli::cli::run_from([
        "spikard",
        "generate",
        "asyncapi",
        schema.to_string_lossy().as_ref(),
        "-l",
        "python",
        "-o",
        output.to_string_lossy().as_ref(),
    ])?;

    assert!(output.exists());
    assert!(output.metadata()?.len() > 0);
    Ok(())
}

#[test]
fn cli_generate_openapi_writes_file() -> Result<()> {
    let schema = repo_root().join("testing_data/schemas/todo-api.openapi.yaml");
    let tmp = TempDir::new()?;
    let output = tmp.path().join("handlers.py");

    spikard_cli::cli::run_from([
        "spikard",
        "generate",
        "openapi",
        schema.to_string_lossy().as_ref(),
        "-l",
        "python",
        "-o",
        output.to_string_lossy().as_ref(),
    ])?;

    assert!(output.exists());
    assert!(output.metadata()?.len() > 0);
    Ok(())
}

#[test]
fn cli_generate_openapi_multi_language_outputs_non_empty_files() -> Result<()> {
    let schema = repo_root().join("testing_data/schemas/todo-api.openapi.yaml");
    let tmp = TempDir::new()?;

    let cases = [
        ("ruby", "handlers.rb"),
        ("typescript", "handlers.ts"),
        ("rust", "handlers.rs"),
        ("php", "handlers.php"),
    ];

    for (lang, filename) in cases {
        let output = tmp.path().join(filename);
        spikard_cli::cli::run_from([
            "spikard",
            "generate",
            "openapi",
            schema.to_string_lossy().as_ref(),
            "-l",
            lang,
            "-o",
            output.to_string_lossy().as_ref(),
        ])?;
        assert!(output.exists(), "expected {filename} to exist");
        assert!(output.metadata()?.len() > 0, "expected {filename} to be non-empty");
    }

    Ok(())
}

#[test]
fn cli_generate_jsonrpc_writes_file() -> Result<()> {
    let schema = repo_root().join("testing_data/schemas/user-api.openrpc.json");
    let tmp = TempDir::new()?;
    let output = tmp.path().join("handlers.py");

    spikard_cli::cli::run_from([
        "spikard",
        "generate",
        "jsonrpc",
        schema.to_string_lossy().as_ref(),
        "-l",
        "python",
        "-o",
        output.to_string_lossy().as_ref(),
    ])?;

    assert!(output.exists());
    assert!(output.metadata()?.len() > 0);
    Ok(())
}

#[test]
fn cli_generate_php_dto_writes_files() -> Result<()> {
    let tmp = TempDir::new()?;
    spikard_cli::cli::run_from([
        "spikard",
        "generate",
        "php-dto",
        "-o",
        tmp.path().to_string_lossy().as_ref(),
    ])?;

    let request = tmp.path().join("Request.php");
    let response = tmp.path().join("Response.php");
    assert!(request.exists());
    assert!(response.exists());
    Ok(())
}

#[test]
fn cli_generate_protobuf_resolves_imports_from_include_paths() -> Result<()> {
    let tmp = TempDir::new()?;
    let shared_dir = tmp.path().join("common");
    std::fs::create_dir_all(&shared_dir)?;

    let shared_proto = shared_dir.join("types.proto");
    std::fs::write(
        &shared_proto,
        r#"syntax = "proto3";

package common;

message SharedType {
  string id = 1;
}
"#,
    )?;

    let root_proto = tmp.path().join("service.proto");
    std::fs::write(
        &root_proto,
        r#"syntax = "proto3";

import "common/types.proto";

package example;

message UsesShared {
  SharedType shared = 1;
}
"#,
    )?;

    let output = tmp.path().join("generated.py");

    spikard_cli::cli::run_from([
        "spikard",
        "generate",
        "protobuf",
        root_proto.to_string_lossy().as_ref(),
        "-l",
        "python",
        "-o",
        output.to_string_lossy().as_ref(),
        "--include",
        tmp.path().to_string_lossy().as_ref(),
    ])?;

    let generated = std::fs::read_to_string(&output)?;
    assert!(generated.contains("class SharedType"));
    assert!(generated.contains("class UsesShared"));
    Ok(())
}