nils-api-testing-core 1.12.0

Library crate for nils-api-testing-core in the nils-cli workspace.
Documentation
use std::io::Write;
use std::path::{Path, PathBuf};

use crate::Result;
use crate::suite::resolve::{
    resolve_gql_url_for_env, resolve_grpc_url_for_env, resolve_path_from_repo_root,
    resolve_rest_base_url_for_env, resolve_ws_url_for_env,
};
use crate::suite::schema::SuiteDefaults;

pub(crate) fn sanitize_id(id: &str) -> String {
    let mut out = String::new();
    let mut prev_dash = false;
    for c in id.chars() {
        let ok = c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-';
        if ok {
            out.push(c);
            prev_dash = false;
        } else if !out.is_empty() && !prev_dash {
            out.push('-');
            prev_dash = true;
        }
    }
    while out.ends_with('-') {
        out.pop();
    }
    if out.is_empty() {
        "case".to_string()
    } else {
        out
    }
}

pub(crate) fn time_run_id_now() -> Result<String> {
    let format = time::format_description::parse_borrowed::<2>(
        "[year][month][day]-[hour][minute][second]Z",
    )?;
    Ok(time::OffsetDateTime::now_utc().format(&format)?)
}

pub(crate) fn time_iso_now() -> Result<String> {
    let format = time::format_description::parse_borrowed::<2>(
        "[year]-[month]-[day]T[hour]:[minute]:[second]Z",
    )?;
    Ok(time::OffsetDateTime::now_utc().format(&format)?)
}

pub(crate) fn path_relative_to_repo_or_abs(repo_root: &Path, path: &Path) -> String {
    if let Ok(stripped) = path.strip_prefix(repo_root) {
        let s = stripped.to_string_lossy();
        if s.is_empty() {
            ".".to_string()
        } else {
            s.to_string()
        }
    } else {
        path.to_string_lossy().to_string()
    }
}

pub(crate) fn resolve_rest_base_url(
    repo_root: &Path,
    setup_dir_raw: &str,
    url_override: &str,
    env_value: &str,
    suite_defaults: &SuiteDefaults,
    env_rest_url: &str,
) -> Result<String> {
    let url_override = url_override.trim();
    if !url_override.is_empty() {
        return Ok(url_override.to_string());
    }
    let default_url = suite_defaults.rest.url.trim();
    if !default_url.is_empty() {
        return Ok(default_url.to_string());
    }
    let env_rest_url = env_rest_url.trim();
    if !env_rest_url.is_empty() {
        return Ok(env_rest_url.to_string());
    }
    let env_value = env_value.trim();
    if !env_value.is_empty() {
        let setup_dir = resolve_path_from_repo_root(repo_root, setup_dir_raw);
        return resolve_rest_base_url_for_env(&setup_dir, env_value);
    }
    Ok("http://localhost:6700".to_string())
}

pub(crate) fn resolve_gql_url(
    repo_root: &Path,
    setup_dir_raw: &str,
    url_override: &str,
    env_value: &str,
    suite_defaults: &SuiteDefaults,
    env_gql_url: &str,
) -> Result<String> {
    let url_override = url_override.trim();
    if !url_override.is_empty() {
        return Ok(url_override.to_string());
    }
    let default_url = suite_defaults.graphql.url.trim();
    if !default_url.is_empty() {
        return Ok(default_url.to_string());
    }
    let env_gql_url = env_gql_url.trim();
    if !env_gql_url.is_empty() {
        return Ok(env_gql_url.to_string());
    }
    let env_value = env_value.trim();
    if !env_value.is_empty() {
        let setup_dir = resolve_path_from_repo_root(repo_root, setup_dir_raw);
        return resolve_gql_url_for_env(&setup_dir, env_value);
    }
    Ok("http://localhost:6700/graphql".to_string())
}

pub(crate) fn resolve_grpc_url(
    repo_root: &Path,
    setup_dir_raw: &str,
    url_override: &str,
    env_value: &str,
    suite_defaults: &SuiteDefaults,
    env_grpc_url: &str,
) -> Result<String> {
    let url_override = url_override.trim();
    if !url_override.is_empty() {
        return Ok(url_override.to_string());
    }
    let default_url = suite_defaults.grpc.url.trim();
    if !default_url.is_empty() {
        return Ok(default_url.to_string());
    }
    let env_grpc_url = env_grpc_url.trim();
    if !env_grpc_url.is_empty() {
        return Ok(env_grpc_url.to_string());
    }
    let env_value = env_value.trim();
    if !env_value.is_empty() {
        let setup_dir = resolve_path_from_repo_root(repo_root, setup_dir_raw);
        return resolve_grpc_url_for_env(&setup_dir, env_value);
    }
    Ok("127.0.0.1:50051".to_string())
}

pub(crate) fn resolve_ws_url(
    repo_root: &Path,
    setup_dir_raw: &str,
    url_override: &str,
    env_value: &str,
    suite_defaults: &SuiteDefaults,
    env_ws_url: &str,
) -> Result<String> {
    let url_override = url_override.trim();
    if !url_override.is_empty() {
        return Ok(url_override.to_string());
    }
    let default_url = suite_defaults.websocket.url.trim();
    if !default_url.is_empty() {
        return Ok(default_url.to_string());
    }
    let env_ws_url = env_ws_url.trim();
    if !env_ws_url.is_empty() {
        return Ok(env_ws_url.to_string());
    }
    let env_value = env_value.trim();
    if !env_value.is_empty() {
        let setup_dir = resolve_path_from_repo_root(repo_root, setup_dir_raw);
        return resolve_ws_url_for_env(&setup_dir, env_value);
    }
    Ok("ws://127.0.0.1:9001/ws".to_string())
}

pub(crate) fn resolve_rest_token_profile(setup_dir: &Path, profile: &str) -> Result<String> {
    let tokens_env = setup_dir.join("tokens.env");
    let tokens_local = setup_dir.join("tokens.local.env");
    let files: Vec<&Path> = if tokens_env.is_file() || tokens_local.is_file() {
        vec![&tokens_env, &tokens_local]
    } else {
        Vec::new()
    };

    let found = crate::env_file::read_prefixed_var("REST_TOKEN_", profile, &files)?;
    found.ok_or_else(|| anyhow::anyhow!("Token profile '{profile}' is empty/missing."))
}

pub(crate) fn resolve_grpc_token_profile(setup_dir: &Path, profile: &str) -> Result<String> {
    let tokens_env = setup_dir.join("tokens.env");
    let tokens_local = setup_dir.join("tokens.local.env");
    let files: Vec<&Path> = if tokens_env.is_file() || tokens_local.is_file() {
        vec![&tokens_env, &tokens_local]
    } else {
        Vec::new()
    };

    let found = crate::env_file::read_prefixed_var("GRPC_TOKEN_", profile, &files)?;
    found.ok_or_else(|| anyhow::anyhow!("Token profile '{profile}' is empty/missing."))
}

pub(crate) fn resolve_ws_token_profile(setup_dir: &Path, profile: &str) -> Result<String> {
    let tokens_env = setup_dir.join("tokens.env");
    let tokens_local = setup_dir.join("tokens.local.env");
    let files: Vec<&Path> = if tokens_env.is_file() || tokens_local.is_file() {
        vec![&tokens_env, &tokens_local]
    } else {
        Vec::new()
    };

    let found = crate::env_file::read_prefixed_var("WS_TOKEN_", profile, &files)?;
    found.ok_or_else(|| anyhow::anyhow!("Token profile '{profile}' is empty/missing."))
}

pub(crate) fn resolve_graphql_bearer_token(
    setup_dir: &Path,
    endpoint_url: &str,
    operation_file: &Path,
    jwt_profile: &str,
    stderr: &mut dyn Write,
) -> Result<Option<String>> {
    if jwt_profile.trim().is_empty() {
        return Ok(None);
    }
    let auth = crate::graphql::auth::resolve_bearer_token(
        setup_dir,
        endpoint_url,
        Some(operation_file),
        Some(jwt_profile),
        stderr,
    )?;
    Ok(auth.bearer_token)
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CaseOutputPaths {
    pub stdout_path: PathBuf,
    pub stderr_path: PathBuf,
}

pub(crate) fn plan_case_output_paths(run_dir_abs: &Path, safe_id: &str) -> CaseOutputPaths {
    CaseOutputPaths {
        stdout_path: run_dir_abs.join(format!("{safe_id}.response.json")),
        stderr_path: run_dir_abs.join(format!("{safe_id}.stderr.log")),
    }
}

pub(crate) fn resolve_effective_env(raw_env: &str, defaults: &SuiteDefaults) -> String {
    let effective_env = raw_env.trim().to_string();
    if effective_env.is_empty() {
        defaults.env.trim().to_string()
    } else {
        effective_env
    }
}

pub(crate) fn resolve_effective_no_history(
    case_no_history: Option<bool>,
    defaults: &SuiteDefaults,
) -> bool {
    case_no_history.unwrap_or(defaults.no_history)
}