cratestack-cli 0.3.0

Rust-native schema-first framework for typed HTTP APIs, generated clients, and backend services.
use std::path::PathBuf;

use anyhow::Result;

use crate::cli_support::{
    ensure_output_dir_is_empty, into_generated_files, json_check_failure, json_check_success,
    parse_schema_or_render, render_schema_error, resolve_context_keys, validate_mount_path,
    validate_service_url, validate_studio_context_inputs, validate_studio_name,
    write_generated_files,
};
use crate::cli_types::{Cli, Command, OutputFormat, StudioProfileArg};

pub(crate) fn run(cli: Cli) -> Result<()> {
    match cli.command {
        Command::Check { schema, format } => handle_check(schema, format)?,
        Command::GenerateDart {
            schema,
            out,
            library_name,
            base_path,
            template_dir,
        } => handle_generate_dart(schema, out, library_name, base_path, template_dir)?,
        Command::GenerateTypeScript {
            schema,
            out,
            package_name,
            base_path,
            template_dir,
        } => handle_generate_typescript(schema, out, package_name, base_path, template_dir)?,
        Command::GenerateStudio {
            schema,
            out,
            name,
            service_url,
            context,
            mount_path,
            profile,
            template_dir,
        } => handle_generate_studio(
            schema,
            out,
            name,
            service_url,
            context,
            mount_path,
            profile,
            template_dir,
        )?,
        Command::PrintIr { schema } => handle_print_ir(schema)?,
    }

    Ok(())
}

fn handle_check(schema: PathBuf, format: OutputFormat) -> Result<()> {
    match cratestack_parser::parse_schema_file(&schema) {
        Ok(_) => match format {
            OutputFormat::Human => {
                println!("schema OK: {}", schema.display());
            }
            OutputFormat::Json => {
                println!(
                    "{}",
                    serde_json::to_string_pretty(&json_check_success(&schema))?
                );
            }
        },
        Err(error) => match format {
            OutputFormat::Human => {
                return Err(anyhow::anyhow!(render_schema_error(&schema, &error)));
            }
            OutputFormat::Json => {
                println!(
                    "{}",
                    serde_json::to_string_pretty(&json_check_failure(&schema, &error))?
                );
                std::process::exit(1);
            }
        },
    }

    Ok(())
}

fn handle_generate_dart(
    schema: PathBuf,
    out: PathBuf,
    library_name: String,
    base_path: String,
    template_dir: Option<PathBuf>,
) -> Result<()> {
    let parsed = parse_schema_or_render(&schema)?;
    let package = cratestack_client_dart::generate_package(
        &parsed,
        &cratestack_client_dart::DartGeneratorConfig {
            library_name,
            base_path,
            template_dir,
        },
    )?;

    write_generated_files(&out, into_generated_files(package.files))?;
    println!("generated Dart client package: {}", out.display());
    Ok(())
}

fn handle_generate_typescript(
    schema: PathBuf,
    out: PathBuf,
    package_name: String,
    base_path: String,
    template_dir: Option<PathBuf>,
) -> Result<()> {
    let parsed = parse_schema_or_render(&schema)?;
    let package = cratestack_client_typescript::generate_package(
        &parsed,
        &cratestack_client_typescript::TypeScriptGeneratorConfig {
            package_name,
            base_path,
            template_dir,
        },
    )?;

    write_generated_files(&out, into_generated_files(package.files))?;
    println!("generated TypeScript client package: {}", out.display());
    Ok(())
}

#[allow(clippy::too_many_arguments)]
fn handle_generate_studio(
    schema: Vec<PathBuf>,
    out: PathBuf,
    name: String,
    service_url: Vec<String>,
    context: Vec<String>,
    mount_path: String,
    profile: StudioProfileArg,
    template_dir: Option<PathBuf>,
) -> Result<()> {
    validate_studio_name(&name)?;
    validate_mount_path(&mount_path)?;
    validate_studio_context_inputs(&schema, &service_url, &context)?;
    for url in &service_url {
        validate_service_url(url)?;
    }
    ensure_output_dir_is_empty(&out)?;

    let parsed_schemas = schema
        .iter()
        .map(parse_schema_or_render)
        .collect::<Result<Vec<_>>>()?;
    let context_keys = resolve_context_keys(&schema, &context)?;
    let generation_contexts =
        build_studio_contexts(&parsed_schemas, &schema, &service_url, &context_keys, &name);
    let package = cratestack_studio_generator::generate_package(
        &generation_contexts,
        &cratestack_studio_generator::StudioGeneratorConfig {
            name,
            mount_path,
            profile: match profile {
                StudioProfileArg::Dev => cratestack_studio_generator::StudioProfile::Dev,
                StudioProfileArg::Prod => cratestack_studio_generator::StudioProfile::Prod,
            },
            template_dir,
        },
    )?;

    write_generated_files(&out, into_generated_files(package.files))?;
    println!("generated Studio app: {}", out.display());
    Ok(())
}

fn handle_print_ir(schema: PathBuf) -> Result<()> {
    let parsed = parse_schema_or_render(&schema)?;
    println!("{parsed:#?}");
    Ok(())
}

fn build_studio_contexts<'a>(
    parsed_schemas: &'a [cratestack_core::Schema],
    schema_paths: &[PathBuf],
    service_urls: &[String],
    context_keys: &[String],
    studio_name: &str,
) -> Vec<cratestack_studio_generator::StudioGeneratorContext<'a>> {
    parsed_schemas
        .iter()
        .zip(schema_paths.iter())
        .zip(service_urls.iter())
        .zip(context_keys.iter())
        .map(|(((parsed, schema_path), upstream_url), context_key)| {
            let service_name = crate::cli_support::derive_service_name(schema_path, studio_name);
            cratestack_studio_generator::StudioGeneratorContext {
                key: context_key.clone(),
                display_name: service_name.clone(),
                service_name,
                schema_path: schema_path.clone(),
                service_url: upstream_url.clone(),
                schema: parsed,
            }
        })
        .collect()
}