use std::env;
use std::ffi::OsStr;
use std::path::Path;
use std::process::Command;
use thiserror::Error;
use regex::Regex;
use which::which;
#[derive(Error, Debug)]
pub enum Error {
#[error("Vaultarq CLI not found")]
NotFound,
#[error("Failed to execute Vaultarq: {0}")]
ExecutionError(String),
#[error("Failed to switch environment: {0}")]
EnvironmentSwitchError(String),
#[error("Invalid format: {0}")]
InvalidFormat(String),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Format {
Bash,
Dotenv,
Json,
}
impl Format {
fn as_arg(&self) -> &'static str {
match self {
Format::Bash => "--bash",
Format::Dotenv => "--dotenv",
Format::Json => "--json",
}
}
}
#[derive(Debug, Clone)]
pub struct Config {
bin_path: String,
throw_if_not_found: bool,
environment: Option<String>,
format: Format,
}
impl Default for Config {
fn default() -> Self {
Self {
bin_path: "vaultarq".to_string(),
throw_if_not_found: false,
environment: None,
format: Format::Bash,
}
}
}
impl Config {
pub fn new() -> Self {
Self::default()
}
pub fn with_bin_path(mut self, path: &str) -> Self {
self.bin_path = path.to_string();
self
}
pub fn with_throw_if_not_found(mut self, throw: bool) -> Self {
self.throw_if_not_found = throw;
self
}
pub fn with_environment(mut self, env: &str) -> Self {
self.environment = Some(env.to_string());
self
}
pub fn with_format(mut self, format: Format) -> Self {
self.format = format;
self
}
}
pub fn is_available() -> bool {
is_available_with_path("vaultarq")
}
pub fn is_available_with_path<S: AsRef<OsStr>>(bin_path: S) -> bool {
let path = Path::new(&bin_path);
if path.is_absolute() {
if !path.exists() || !path.is_file() {
return false;
}
} else {
if which(&bin_path).is_err() {
return false;
}
}
match Command::new(&bin_path).output() {
Ok(_) => true,
Err(_) => false,
}
}
pub fn init() -> Result<(), Error> {
init_with_config(&Config::default())
}
pub fn init_with_config(config: &Config) -> Result<(), Error> {
if !is_available_with_path(&config.bin_path) {
if config.throw_if_not_found {
return Err(Error::NotFound);
}
return Ok(());
}
if let Some(environment) = &config.environment {
let output = Command::new(&config.bin_path)
.arg("link")
.arg(environment)
.output()
.map_err(|e| Error::ExecutionError(e.to_string()))?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
return Err(Error::EnvironmentSwitchError(error.to_string()));
}
}
let output = Command::new(&config.bin_path)
.arg("export")
.arg(config.format.as_arg())
.output()
.map_err(|e| Error::ExecutionError(e.to_string()))?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
return Err(Error::ExecutionError(error.to_string()));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let lines = stdout.lines().filter(|line| !line.trim().is_empty());
for line in lines {
if line.starts_with("export ") {
let re = Regex::new(r#"^export\s+([A-Za-z0-9_]+)="(.*)"$"#).unwrap();
if let Some(captures) = re.captures(line) {
let key = captures.get(1).unwrap().as_str();
let value = captures.get(2).unwrap().as_str();
env::set_var(key, value);
}
} else {
let re = Regex::new(r"^([A-Za-z0-9_]+)=(.*)$").unwrap();
if let Some(captures) = re.captures(line) {
let key = captures.get(1).unwrap().as_str();
let value = captures.get(2).unwrap().as_str();
let value = if value.starts_with('"') && value.ends_with('"') {
&value[1..value.len() - 1]
} else {
value
};
env::set_var(key, value);
}
}
}
Ok(())
}