via-cli 0.2.0

Run commands and API requests with 1Password-backed credentials without exposing secrets to your shell
Documentation
use secrecy::{ExposeSecret, SecretString};
use serde::{Deserialize, Deserializer};

#[derive(Clone)]
pub struct SecretValue {
    value: SecretString,
}

impl SecretValue {
    pub fn new(value: String) -> Self {
        Self {
            value: SecretString::from(value),
        }
    }

    pub fn from_utf8_lossy_trimmed(bytes: Vec<u8>) -> Self {
        let value = match String::from_utf8(bytes) {
            Ok(value) => value,
            Err(error) => String::from_utf8_lossy(error.as_bytes()).into_owned(),
        };
        Self::new_trimmed(value)
    }

    pub fn expose(&self) -> &str {
        self.value.expose_secret()
    }

    fn new_trimmed(mut value: String) -> Self {
        while value.ends_with(['\r', '\n']) {
            value.pop();
        }
        Self::new(value)
    }
}

impl<'de> Deserialize<'de> for SecretValue {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        String::deserialize(deserializer).map(Self::new)
    }
}

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

    #[test]
    fn exposes_secret_value_when_explicitly_requested() {
        let secret = SecretValue::new("secret-token".to_owned());

        assert_eq!(secret.expose(), "secret-token");
    }

    #[test]
    fn builds_secret_value_from_trimmed_utf8_output() {
        let secret = SecretValue::from_utf8_lossy_trimmed(b"secret-token\r\n".to_vec());

        assert_eq!(secret.expose(), "secret-token");
    }
}