k2db-api-server 0.1.1

Single-binary Rust server for the k2db API
// SPDX-FileCopyrightText: 2026 Alexander R. Croft
// SPDX-License-Identifier: MIT

use std::env;

use crate::cli::BootstrapArgs;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BootstrapConfig {
    pub mongo_uri: String,
    pub bootstrap_token: String,
    pub system_db_name: String,
}

#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum BootstrapResolveError {
    #[error("missing bootstrap input: {0}")]
    MissingInput(&'static str),
    #[error("environment variable is not set: {0}")]
    MissingEnvVar(String),
    #[error("resolved bootstrap input is empty: {0}")]
    EmptyValue(&'static str),
}

impl BootstrapConfig {
    pub fn resolve_runtime(args: &BootstrapArgs) -> Result<Self, BootstrapResolveError> {
        let mongo_uri = resolve_value(
            args.mongo_uri.as_deref(),
            args.mongo_uri_env.as_deref(),
            "mongo_uri",
        )?;
        let system_db_name = args.system_db_name.trim().to_owned();
        if system_db_name.is_empty() {
            return Err(BootstrapResolveError::EmptyValue("system_db_name"));
        }

        Ok(Self {
            mongo_uri,
            bootstrap_token: String::new(),
            system_db_name,
        })
    }

    pub fn resolve(args: &BootstrapArgs) -> Result<Self, BootstrapResolveError> {
        let mongo_uri = resolve_value(
            args.mongo_uri.as_deref(),
            args.mongo_uri_env.as_deref(),
            "mongo_uri",
        )?;
        let bootstrap_token = resolve_value(
            args.bootstrap_token.as_deref(),
            args.bootstrap_token_env.as_deref(),
            "bootstrap_token",
        )?;
        let system_db_name = args.system_db_name.trim().to_owned();
        if system_db_name.is_empty() {
            return Err(BootstrapResolveError::EmptyValue("system_db_name"));
        }

        Ok(Self {
            mongo_uri,
            bootstrap_token,
            system_db_name,
        })
    }

    pub fn redacted_mongo_uri(&self) -> String {
        self.mongo_uri.replace(|c: char| c == '\n' || c == '\r', "")
    }
}

fn resolve_value(
    direct: Option<&str>,
    env_name: Option<&str>,
    field: &'static str,
) -> Result<String, BootstrapResolveError> {
    if let Some(value) = direct {
        let trimmed = value.trim();
        if trimmed.is_empty() {
            return Err(BootstrapResolveError::EmptyValue(field));
        }
        return Ok(trimmed.to_owned());
    }

    if let Some(name) = env_name {
        let trimmed_name = name.trim();
        if trimmed_name.is_empty() {
            return Err(BootstrapResolveError::EmptyValue(field));
        }
        let value = env::var(trimmed_name)
            .map_err(|_| BootstrapResolveError::MissingEnvVar(trimmed_name.to_owned()))?;
        let trimmed = value.trim();
        if trimmed.is_empty() {
            return Err(BootstrapResolveError::EmptyValue(field));
        }
        return Ok(trimmed.to_owned());
    }

    Err(BootstrapResolveError::MissingInput(field))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cli::BootstrapArgs;

    fn base_args() -> BootstrapArgs {
        BootstrapArgs {
            mongo_uri: None,
            mongo_uri_env: None,
            bootstrap_token: None,
            bootstrap_token_env: None,
            system_db_name: "k2_system".to_owned(),
            listen_host: None,
            listen_port: None,
            ownership_mode: None,
            slow_query_ms: None,
            seed_key_name: None,
            seed_key_database: None,
            seed_key_permissions: Vec::new(),
        }
    }

    #[test]
    fn resolve_prefers_direct_values() {
        let args = BootstrapArgs {
            mongo_uri: Some("mongodb://localhost:27017".to_owned()),
            mongo_uri_env: Some("IGNORED_MONGO_URI".to_owned()),
            bootstrap_token: Some("root-token".to_owned()),
            bootstrap_token_env: Some("IGNORED_BOOTSTRAP_TOKEN".to_owned()),
            ..base_args()
        };

        let config = BootstrapConfig::resolve(&args).expect("bootstrap config");
        assert_eq!(config.mongo_uri, "mongodb://localhost:27017");
        assert_eq!(config.bootstrap_token, "root-token");
        assert_eq!(config.system_db_name, "k2_system");
    }

    #[test]
    fn resolve_reads_from_environment_when_requested() {
        let mongo_name = "K2DB_API_RUST_TEST_MONGO_URI";
        let token_name = "K2DB_API_RUST_TEST_BOOTSTRAP_TOKEN";
        unsafe {
            env::set_var(mongo_name, "mongodb://env-host:27017");
            env::set_var(token_name, "env-token");
        }

        let args = BootstrapArgs {
            mongo_uri_env: Some(mongo_name.to_owned()),
            bootstrap_token_env: Some(token_name.to_owned()),
            ..base_args()
        };

        let config = BootstrapConfig::resolve(&args).expect("bootstrap config");
        assert_eq!(config.mongo_uri, "mongodb://env-host:27017");
        assert_eq!(config.bootstrap_token, "env-token");

        unsafe {
            env::remove_var(mongo_name);
            env::remove_var(token_name);
        }
    }

    #[test]
    fn resolve_rejects_missing_inputs() {
        let args = base_args();

        let error = BootstrapConfig::resolve(&args).expect_err("missing mongo input should fail");
        assert_eq!(error, BootstrapResolveError::MissingInput("mongo_uri"));
    }

    #[test]
    fn resolve_runtime_only_requires_mongo_uri() {
        let args = BootstrapArgs {
            mongo_uri: Some("mongodb://localhost:27017".to_owned()),
            ..base_args()
        };

        let config = BootstrapConfig::resolve_runtime(&args).expect("runtime bootstrap config");
        assert_eq!(config.mongo_uri, "mongodb://localhost:27017");
        assert_eq!(config.bootstrap_token, "");
    }
}