cratestack-cli 0.7.7

Rust-native schema-first framework for typed HTTP APIs, generated clients, and backend services.
mod build_runner;
mod cli_handlers;
mod cli_support;
mod cli_types;
mod drift;
mod generate_proto;
mod migrate;
mod schema_diff;

use anyhow::Result;
use clap::Parser;

use crate::cli_handlers::run;
use crate::cli_types::Cli;

fn main() -> Result<()> {
    let cli = Cli::parse();
    run(cli)
}

#[cfg(test)]
mod tests {
    use std::path::{Path, PathBuf};

    use clap::Parser;

    use crate::Cli;
    use crate::cli_support::{json_check_failure, json_check_success};
    use crate::cli_types::{Command, DartPresetArg, StudioCmd, TypeScriptPresetArg};

    #[test]
    fn json_success_payload_has_empty_diagnostics() {
        let payload = json_check_success(Path::new("schema.cstack"));
        assert_eq!(payload["ok"], true);
        assert_eq!(payload["schema"], "schema.cstack");
        assert_eq!(payload["diagnostics"], serde_json::json!([]));
    }

    #[test]
    fn json_failure_payload_exposes_structured_diagnostic_fields() {
        let error = cratestack_parser::parse_schema("model User {\n  email String\n}\n")
            .expect_err("schema should fail validation");
        let payload = json_check_failure(Path::new("schema.cstack"), &error);
        let diagnostic = &payload["diagnostics"][0];

        assert_eq!(payload["ok"], false);
        assert_eq!(diagnostic["line"], 1);
        assert!(diagnostic["start"].as_u64().is_some());
        assert!(diagnostic["end"].as_u64().is_some());
        assert!(
            diagnostic["message"]
                .as_str()
                .expect("message should be a string")
                .contains("missing an @id field")
        );
    }

    #[test]
    fn generate_dart_clap_defaults() {
        let cli = Cli::parse_from([
            "cratestack",
            "generate-dart",
            "--schema",
            "schema.cstack",
            "--out",
            "out",
        ]);

        match cli.command {
            Command::GenerateDart {
                schema,
                out,
                library_name,
                base_path,
                template_dir,
                check,
                preset,
                run_build_runner,
            } => {
                assert_eq!(schema, PathBuf::from("schema.cstack"));
                assert_eq!(out, PathBuf::from("out"));
                assert_eq!(library_name, "cratestack_client");
                assert_eq!(base_path, "/api");
                assert_eq!(template_dir, None);
                assert!(!check);
                assert_eq!(preset, DartPresetArg::Default);
                assert!(
                    !run_build_runner,
                    "--run-build-runner must default to off (issue #303: opt-in, not default)"
                );
            }
            _ => panic!("expected generate-dart command"),
        }
    }

    #[test]
    fn generate_dart_clap_accepts_riverpod_preset() {
        let cli = Cli::parse_from([
            "cratestack",
            "generate-dart",
            "--schema",
            "schema.cstack",
            "--out",
            "out",
            "--preset",
            "riverpod",
        ]);

        match cli.command {
            Command::GenerateDart { preset, .. } => {
                assert_eq!(preset, DartPresetArg::Riverpod);
            }
            _ => panic!("expected generate-dart command"),
        }
    }

    #[test]
    fn generate_dart_clap_accepts_run_build_runner_flag() {
        let cli = Cli::parse_from([
            "cratestack",
            "generate-dart",
            "--schema",
            "schema.cstack",
            "--out",
            "out",
            "--preset",
            "riverpod",
            "--run-build-runner",
        ]);

        match cli.command {
            Command::GenerateDart {
                preset,
                run_build_runner,
                ..
            } => {
                assert_eq!(preset, DartPresetArg::Riverpod);
                assert!(run_build_runner);
            }
            _ => panic!("expected generate-dart command"),
        }
    }

    #[test]
    fn generate_typescript_clap_defaults() {
        let cli = Cli::parse_from([
            "cratestack",
            "generate-typescript",
            "--schema",
            "schema.cstack",
            "--out",
            "out",
        ]);

        match cli.command {
            Command::GenerateTypeScript {
                schema,
                out,
                package_name,
                base_path,
                template_dir,
                check,
                full_selection,
                preset,
            } => {
                assert_eq!(schema, PathBuf::from("schema.cstack"));
                assert_eq!(out, PathBuf::from("out"));
                assert_eq!(package_name, "cratestack-client");
                assert_eq!(base_path, "/api");
                assert_eq!(template_dir, None);
                assert!(!check);
                assert!(!full_selection);
                assert_eq!(preset, TypeScriptPresetArg::Default);
            }
            _ => panic!("expected generate-typescript command"),
        }
    }

    #[test]
    fn generate_typescript_clap_accepts_swr_preset_flag() {
        // Issue #304: `--preset swr` selects the new file-per-model
        // layout; the value must clap-parse to the `Swr` variant (kebab
        // renamed to the literal string `swr`).
        let cli = Cli::parse_from([
            "cratestack",
            "generate-typescript",
            "--schema",
            "schema.cstack",
            "--out",
            "out",
            "--preset",
            "swr",
        ]);

        match cli.command {
            Command::GenerateTypeScript { preset, .. } => {
                assert_eq!(preset, TypeScriptPresetArg::Swr);
            }
            _ => panic!("expected generate-typescript command"),
        }
    }

    #[test]
    fn generate_typescript_clap_accepts_full_selection_flag() {
        let cli = Cli::parse_from([
            "cratestack",
            "generate-typescript",
            "--schema",
            "schema.cstack",
            "--out",
            "out",
            "--full-selection",
        ]);

        match cli.command {
            Command::GenerateTypeScript { full_selection, .. } => {
                assert!(full_selection);
            }
            _ => panic!("expected generate-typescript command"),
        }
    }

    #[test]
    fn generate_proto_clap_defaults() {
        let cli = Cli::parse_from([
            "cratestack",
            "generate-proto",
            "--schema",
            "schema.cstack",
            "--out",
            "api.proto",
        ]);

        match cli.command {
            Command::GenerateProto {
                schema,
                out,
                package,
                check,
            } => {
                assert_eq!(schema, PathBuf::from("schema.cstack"));
                assert_eq!(out, PathBuf::from("api.proto"));
                assert_eq!(package, None);
                assert!(!check);
            }
            _ => panic!("expected generate-proto command"),
        }
    }

    #[test]
    fn generate_proto_clap_accepts_package_and_check() {
        let cli = Cli::parse_from([
            "cratestack",
            "generate-proto",
            "--schema",
            "schema.cstack",
            "--out",
            "api.proto",
            "--package",
            "shop_api",
            "--check",
        ]);

        match cli.command {
            Command::GenerateProto { package, check, .. } => {
                assert_eq!(package, Some("shop_api".to_owned()));
                assert!(check);
            }
            _ => panic!("expected generate-proto command"),
        }
    }

    #[test]
    fn generate_wiremock_clap_defaults() {
        let cli = Cli::parse_from([
            "cratestack",
            "generate-wiremock",
            "--schema",
            "schema.cstack",
            "--out",
            "out",
        ]);

        match cli.command {
            Command::GenerateWiremock {
                schema,
                out,
                base_path,
                check,
            } => {
                assert_eq!(schema, PathBuf::from("schema.cstack"));
                assert_eq!(out, PathBuf::from("out"));
                assert_eq!(base_path, "/api");
                assert!(!check);
            }
            _ => panic!("expected generate-wiremock command"),
        }
    }

    #[test]
    fn studio_run_clap_defaults() {
        let cli = Cli::parse_from(["cratestack", "studio", "run"]);
        match cli.command {
            Command::Studio {
                cmd: StudioCmd::Run { config, bind },
            } => {
                assert_eq!(config, PathBuf::from("studio.toml"));
                assert!(bind.is_none());
            }
            _ => panic!("expected studio run command"),
        }
    }

    #[test]
    fn studio_init_clap_defaults() {
        let cli = Cli::parse_from(["cratestack", "studio", "init"]);
        match cli.command {
            Command::Studio {
                cmd: StudioCmd::Init { out, force },
            } => {
                assert_eq!(out, PathBuf::from("."));
                assert!(!force);
            }
            _ => panic!("expected studio init command"),
        }
    }

    #[test]
    fn diff_clap_defaults() {
        let cli = Cli::parse_from(["cratestack", "diff", "old.cstack", "new.cstack"]);
        match cli.command {
            Command::Diff { old, new, json } => {
                assert_eq!(old, PathBuf::from("old.cstack"));
                assert_eq!(new, PathBuf::from("new.cstack"));
                assert!(!json);
            }
            _ => panic!("expected diff command"),
        }
    }

    #[test]
    fn version_long_flag_is_accepted() {
        let result = Cli::try_parse_from(["cratestack", "--version"]);
        let error = result.expect_err("--version should short-circuit parsing");
        assert_eq!(error.kind(), clap::error::ErrorKind::DisplayVersion);
    }

    #[test]
    fn version_short_flag_is_accepted() {
        let result = Cli::try_parse_from(["cratestack", "-V"]);
        let error = result.expect_err("-V should short-circuit parsing");
        assert_eq!(error.kind(), clap::error::ErrorKind::DisplayVersion);
    }

    #[test]
    fn studio_eject_requires_out() {
        let result = Cli::try_parse_from(["cratestack", "studio", "eject"]);
        assert!(
            result.is_err(),
            "studio eject must require --out, got {:?}",
            result.map(|cli| cli.command)
        );
    }
}