dbnest 0.1.2

dbnest CLI – cozy local databases in seconds
mod human;
mod json;

pub use human::{print_instance_human, print_instances_human};
pub use json::{print_instance_json, print_instances_json};

use dbnest_core::{Instance, InstanceSummary};

use serde_json::json;

pub fn print_error(err: &dbnest_core::DbnestError, json_mode: bool) {
    if json_mode {
        eprintln!(
            "{}",
            serde_json::to_string_pretty(&error_json(err)).unwrap()
        );
    } else {
        eprintln!("{err}");
    }
}

pub fn error_json(err: &dbnest_core::DbnestError) -> serde_json::Value {
    json!({
        "ok": false,
        "error": {
            "kind": err.kind(),
            "message": err.to_string()
        }
    })
}

pub fn print_ok(json_mode: bool, action: &str, id: Option<&str>) {
    if json_mode {
        println!(
            "{}",
            serde_json::to_string_pretty(&json!({
                "ok": true,
                "action": action,
                "id": id
            }))
            .unwrap()
        );
    } else {
        if let Some(id) = id {
            println!("{action} ok: {id}");
        } else {
            println!("{action} ok");
        }
    }
}

pub fn print_status(json_mode: bool, res: crate::cli::StatusResult) {
    if json_mode {
        match res {
            crate::cli::StatusResult::One(r) => {
                println!("{}", serde_json::to_string_pretty(&r).unwrap())
            }
            crate::cli::StatusResult::Many(v) => {
                println!("{}", serde_json::to_string_pretty(&v).unwrap())
            }
        }
    } else {
        match res {
            crate::cli::StatusResult::One(r) => print_status_human(&[r]),
            crate::cli::StatusResult::Many(v) => print_status_human(&v),
        }
    }
}

fn print_status_human(list: &[dbnest_core::InstanceStatusReport]) {
    if list.is_empty() {
        println!("No instances found.");
        return;
    }

    for r in list {
        println!("{}  {:8}  {:?}", r.id, r.engine.as_str(), r.status);
    }
}

pub fn print_instance(inst: &Instance, json: bool, show_secrets: bool) {
    if json {
        print_instance_json(inst, show_secrets);
    } else {
        print_instance_human(inst, show_secrets);
    }
}

pub fn print_instances(list: &[InstanceSummary], json: bool, show_secrets: bool) {
    if json {
        print_instances_json(list, show_secrets);
    } else {
        print_instances_human(list, show_secrets);
    }
}

pub fn redact_database_url(url: &str) -> String {
    let Some(scheme_end) = url.find("://") else {
        return url.to_string();
    };
    let authority_start = scheme_end + 3;
    let rest = &url[authority_start..];
    let Some(at_rel) = rest.find('@') else {
        return url.to_string();
    };
    let at = authority_start + at_rel;
    let credentials = &url[authority_start..at];
    let Some(colon_rel) = credentials.find(':') else {
        return url.to_string();
    };
    let password_start = authority_start + colon_rel + 1;

    format!("{}****{}", &url[..password_start], &url[at..])
}

#[cfg(test)]
mod tests {
    use super::{error_json, redact_database_url};

    #[test]
    fn redacts_password_in_database_url() {
        assert_eq!(
            redact_database_url("postgres://dev:secret@127.0.0.1:5432/appdb"),
            "postgres://dev:****@127.0.0.1:5432/appdb"
        );
    }

    #[test]
    fn leaves_urls_without_password_unchanged() {
        assert_eq!(
            redact_database_url("sqlite:////tmp/db.sqlite"),
            "sqlite:////tmp/db.sqlite"
        );
    }

    #[test]
    fn leaves_urls_without_credentials_unchanged() {
        assert_eq!(
            redact_database_url("postgres://127.0.0.1:5432/appdb"),
            "postgres://127.0.0.1:5432/appdb"
        );
    }

    #[test]
    fn redacts_password_containing_colon() {
        assert_eq!(
            redact_database_url("postgres://dev:secret:extra@127.0.0.1:5432/appdb"),
            "postgres://dev:****@127.0.0.1:5432/appdb"
        );
    }

    #[test]
    fn json_error_uses_stable_kind() {
        let err = dbnest_core::DbnestError::InvalidArgument("bad input".into());
        let value = error_json(&err);

        assert_eq!(value["ok"], false);
        assert_eq!(value["error"]["kind"], "invalid_argument");
        assert_eq!(value["error"]["message"], "invalid argument: bad input");
    }
}