use anyhow::Result;
use clap::Args;
use crate::env;
use crate::ui::display;
#[derive(Args)]
pub struct TemplateArgs {
#[arg(default_value = ".env")]
pub file: String,
#[arg(long)]
pub output: Option<String>,
#[arg(long)]
pub config: Option<String>,
}
pub fn run(args: TemplateArgs) -> Result<()> {
let content = std::fs::read_to_string(&args.file)
.map_err(|e| anyhow::anyhow!("failed to read '{}': {}", args.file, e))?;
let env_file = env::parser::parse(&content)?;
let schema = env::schema::load_schema(args.config.as_deref())?;
let mut output = String::new();
for entry in &env_file.entries {
match entry {
env::Entry::KeyValue { key, value } => {
let description = schema
.as_ref()
.and_then(|s| s.rules.get(key.as_str()))
.and_then(|r| r.description.as_deref());
let hint = if let Some(desc) = description {
desc.to_string()
} else {
infer_type_hint(value)
};
output.push_str(&format!("{}=<{}>\n", key, hint));
}
env::Entry::Comment(text) => {
output.push_str(text);
output.push('\n');
}
env::Entry::Blank => {
output.push('\n');
}
}
}
if let Some(ref path) = args.output {
if std::path::Path::new(path.as_str()).exists() {
anyhow::bail!(
"'{}' already exists. This command replaces values with type hints -- \
overwriting a real .env would destroy secret values. \
Delete the file first if this is intentional",
path
);
}
std::fs::write(path, &output)?;
display::ok(&format!(
"template written to {} ({} variables)",
path,
env_file.var_count()
));
} else {
print!("{}", output);
}
Ok(())
}
fn infer_type_hint(value: &str) -> String {
let lower = value.to_lowercase();
if ["true", "false", "1", "0", "yes", "no"].contains(&lower.as_str()) {
return "boolean".to_string();
}
if value.parse::<i64>().is_ok() {
if let Ok(n) = value.parse::<u16>() {
if (1024..=65535).contains(&n) {
return "integer, port".to_string();
}
}
return "integer".to_string();
}
if value.starts_with("http://")
|| value.starts_with("https://")
|| value.starts_with("postgres://")
|| value.starts_with("mysql://")
|| value.starts_with("redis://")
|| value.starts_with("mongodb://")
{
if let Some(scheme) = value.split("://").next() {
return format!("{} connection string", scheme);
}
return "URL".to_string();
}
if value.contains('@') && value.contains('.') && !value.contains(' ') {
return "email address".to_string();
}
let len = value.len();
if len > 20 {
format!("{}+ character string", len)
} else {
"string".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn infer_boolean() {
assert_eq!(infer_type_hint("true"), "boolean");
assert_eq!(infer_type_hint("false"), "boolean");
assert_eq!(infer_type_hint("0"), "boolean");
}
#[test]
fn infer_integer() {
assert_eq!(infer_type_hint("42"), "integer");
assert_eq!(infer_type_hint("99999"), "integer");
}
#[test]
fn infer_url() {
assert!(infer_type_hint("https://api.example.com").contains("https"));
assert!(infer_type_hint("postgres://localhost/db").contains("postgres"));
}
#[test]
fn infer_email() {
assert_eq!(infer_type_hint("user@example.com"), "email address");
}
#[test]
fn infer_long_string() {
let long = "abcdefghijklmnopqrstuvwxyz12345";
assert!(infer_type_hint(long).contains("character string"));
}
#[test]
fn infer_short_string() {
assert_eq!(infer_type_hint("hello"), "string");
}
}