use serde_json::{json, Value};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct AwsContext {
pub profile: String,
pub region: String,
}
fn config_path() -> PathBuf {
if let Some(p) = std::env::var_os("AWS_CONFIG_FILE") {
return PathBuf::from(p);
}
let home = std::env::var_os("HOME").unwrap_or_default();
PathBuf::from(home).join(".aws").join("config")
}
pub fn region_from_config(text: &str, profile: &str) -> Option<String> {
let target = if profile == "default" {
"default".to_string()
} else {
format!("profile {profile}")
};
let mut in_section = false;
for raw in text.lines() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
continue;
}
if let Some(rest) = line.strip_prefix('[') {
if let Some(name) = rest.strip_suffix(']') {
in_section = name.trim() == target;
}
continue;
}
if !in_section {
continue;
}
if let Some(rest) = line.strip_prefix("region") {
let rest = rest.trim_start();
if let Some(rest) = rest.strip_prefix('=') {
let r = rest.trim().to_string();
if !r.is_empty() {
return Some(r);
}
}
}
}
None
}
pub fn resolve(
env_profile: Option<&str>,
env_region: Option<&str>,
env_default_region: Option<&str>,
config_text: Option<&str>,
) -> Option<AwsContext> {
let profile = env_profile
.filter(|s| !s.is_empty())
.unwrap_or("default")
.to_string();
let region = env_region
.or(env_default_region)
.filter(|s| !s.is_empty())
.map(str::to_string)
.or_else(|| config_text.and_then(|t| region_from_config(t, &profile)))
.unwrap_or_default();
if profile == "default"
&& region.is_empty()
&& env_profile.is_none()
&& env_region.is_none()
&& env_default_region.is_none()
&& config_text.is_none()
{
return None;
}
Some(AwsContext { profile, region })
}
pub fn read_aws_context() -> Option<AwsContext> {
let env_profile = std::env::var("AWS_PROFILE").ok();
let env_region = std::env::var("AWS_REGION").ok();
let env_default_region = std::env::var("AWS_DEFAULT_REGION").ok();
let cfg = fs::read_to_string(config_path()).ok();
resolve(
env_profile.as_deref(),
env_region.as_deref(),
env_default_region.as_deref(),
cfg.as_deref(),
)
}
pub fn context(format: &str, hide_default_profile: bool) -> Option<Vec<Value>> {
let state = read_aws_context()?;
let contents = if hide_default_profile && state.profile == "default" {
drop_token(format, "{profile}", &state.region)
} else {
format
.replace("{profile}", &state.profile)
.replace("{region}", &state.region)
};
Some(vec![json!({
"contents": contents,
"highlight_groups": ["aws_context", "aws"],
"divider_highlight_group": "background:divider",
})])
}
fn drop_token(fmt: &str, drop: &str, region: &str) -> String {
let mut s = fmt.to_string();
for sep in [
format!("{drop}@"),
format!("{drop}:"),
format!("{drop}/"),
format!("@{drop}"),
format!(":{drop}"),
format!("/{drop}"),
] {
if s.contains(&sep) {
s = s.replace(&sep, "");
return s.replace("{region}", region);
}
}
s.replace(drop, "").replace("{region}", region)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn region_from_config_default_section() {
let text = "[default]\nregion = us-east-1\noutput = json\n";
assert_eq!(
region_from_config(text, "default"),
Some("us-east-1".into())
);
}
#[test]
fn region_from_config_named_profile() {
let text = "[default]\nregion = us-east-1\n\n[profile prod]\nregion = eu-west-2\n";
assert_eq!(region_from_config(text, "prod"), Some("eu-west-2".into()));
}
#[test]
fn region_from_config_missing_profile_returns_none() {
let text = "[default]\nregion = us-east-1\n";
assert!(region_from_config(text, "staging").is_none());
}
#[test]
fn region_from_config_ignores_comments() {
let text = "[default]\n# region = wrong\nregion = us-west-1\n";
assert_eq!(
region_from_config(text, "default"),
Some("us-west-1".into())
);
}
#[test]
fn region_from_config_handles_empty_value() {
let text = "[default]\nregion =\n";
assert!(region_from_config(text, "default").is_none());
}
#[test]
fn resolve_env_profile_and_region() {
let s = resolve(Some("prod"), Some("us-east-2"), None, None).unwrap();
assert_eq!(s.profile, "prod");
assert_eq!(s.region, "us-east-2");
}
#[test]
fn resolve_default_env_region_fallback() {
let s = resolve(Some("staging"), None, Some("us-west-1"), None).unwrap();
assert_eq!(s.profile, "staging");
assert_eq!(s.region, "us-west-1");
}
#[test]
fn resolve_config_region_when_env_missing() {
let cfg = "[profile prod]\nregion = ap-south-1\n";
let s = resolve(Some("prod"), None, None, Some(cfg)).unwrap();
assert_eq!(s.region, "ap-south-1");
}
#[test]
fn resolve_returns_none_when_no_signal() {
assert!(resolve(None, None, None, None).is_none());
}
#[test]
fn resolve_default_with_config_returns_some() {
let cfg = "[profile prod]\nregion = eu-west-1\n";
let s = resolve(None, None, None, Some(cfg)).unwrap();
assert_eq!(s.profile, "default");
assert_eq!(s.region, "");
}
#[test]
fn drop_token_strips_at_separator() {
let s = drop_token("{profile}@{region}", "{profile}", "us-east-1");
assert_eq!(s, "us-east-1");
}
#[test]
fn drop_token_strips_colon_separator() {
let s = drop_token("{profile}:{region}", "{profile}", "eu-west-2");
assert_eq!(s, "eu-west-2");
}
#[test]
fn drop_token_handles_no_separator() {
let s = drop_token("{profile} {region}", "{profile}", "us-west-1");
assert_eq!(s, " us-west-1");
}
}