denki 0.1.2

CLI for controlling TP-Link smart home devices — denki (電気) means electricity in Japanese
Documentation
//! Tapo credentials — env vars (TAPO_USER/TAPO_PASS) take precedence over
//! denki/credentials.json in the platform configuration directory (mode 0600 on Unix).

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

#[derive(Debug, Serialize, Deserialize)]
struct CredFile {
    tapo_user: String,
    tapo_pass: String,
}

fn creds_path() -> PathBuf {
    dirs::config_dir()
        .unwrap_or_else(|| PathBuf::from(std::env::var("HOME").unwrap_or_default()))
        .join("denki")
        .join("credentials.json")
}

fn missing_creds_context(path: &std::path::Path) -> String {
    format!(
        "No Tapo credentials found.\n\
         Set both TAPO_USER and TAPO_PASS, or run `denki login <email>` to save them.\n\
         File: {}",
        path.display()
    )
}

fn malformed_creds_context() -> &'static str {
    "credentials.json is malformed — remove it and run `denki login <email>` again"
}

pub fn load() -> Result<(String, String)> {
    if let (Ok(user), Ok(pass)) = (std::env::var("TAPO_USER"), std::env::var("TAPO_PASS")) {
        return Ok((user, pass));
    }

    let path = creds_path();
    let data = std::fs::read_to_string(&path).with_context(|| missing_creds_context(&path))?;

    let creds: CredFile = serde_json::from_str(&data).context(malformed_creds_context())?;

    Ok((creds.tapo_user, creds.tapo_pass))
}

pub fn save(user: &str, pass: &str) -> Result<()> {
    let path = creds_path();
    let _lock = crate::storage::FileLock::acquire(&path)?;
    let data = serde_json::to_vec_pretty(&CredFile {
        tapo_user: user.to_string(),
        tapo_pass: pass.to_string(),
    })?;
    crate::storage::atomic_write(&path, &data)?;

    Ok(())
}

pub fn path_display() -> String {
    creds_path().display().to_string()
}

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

    #[test]
    fn missing_creds_context_mentions_env_and_file_path() {
        let path = PathBuf::from("/tmp/denki/credentials.json");
        let msg = missing_creds_context(&path);
        assert!(msg.contains("TAPO_USER"));
        assert!(msg.contains("TAPO_PASS"));
        assert!(msg.contains("denki login <email>"));
        assert!(msg.contains("/tmp/denki/credentials.json"));
    }

    #[test]
    fn malformed_creds_message_mentions_reset_flow() {
        assert!(malformed_creds_context().contains("malformed"));
        assert!(malformed_creds_context().contains("remove it"));
        assert!(malformed_creds_context().contains("denki login <email>"));
    }
}