//// Region resolution. //// //// Precedence (matches go-v2 / Rust SDK): //// //// 1. `AWS_REGION` environment variable //// 2. `AWS_DEFAULT_REGION` environment variable //// 3. `region` setting in `~/.aws/config` under the active profile //// //// Resolution from EC2 IMDS is not implemented — most workloads set //// `AWS_REGION` explicitly, and the few that rely on IMDS region //// auto-detection can call `from_imds` themselves and feed the result in. import aws/env.{get_env} import aws/internal/ini import gleam/bit_array import gleam/option.{type Option, None, Some} import gleam/result pub type ResolveError { /// No region found in any of the sources tried. The accompanying list /// records every source that was checked, in order, so an end-user log /// message can read like "tried AWS_REGION, AWS_DEFAULT_REGION, /// ~/.aws/config[profile=default]". NoRegion(sources_tried: List(String)) } /// Resolve a region using the real OS env and `~/.aws/config`. Standard /// production wiring. pub fn resolve(profile profile: String) -> Result(String, ResolveError) { resolve_with( profile: profile, env_lookup: get_env, config_reader: read_default_config, ) } /// Resolve a region with injectable env and config-file access — used by /// tests, and available to callers that want to feed in their own sources. pub fn resolve_with( profile profile: String, env_lookup env_lookup: fn(String) -> Result(String, Nil), config_reader config_reader: fn() -> Result(String, Nil), ) -> Result(String, ResolveError) { case try_env(env_lookup) { Some(region) -> Ok(region) None -> case try_config(config_reader, profile) { Some(region) -> Ok(region) None -> Error( NoRegion(sources_tried: [ "AWS_REGION", "AWS_DEFAULT_REGION", "~/.aws/config[profile=" <> profile <> "].region", ]), ) } } } fn try_env(lookup: fn(String) -> Result(String, Nil)) -> Option(String) { case non_empty(lookup("AWS_REGION")) { Some(r) -> Some(r) None -> non_empty(lookup("AWS_DEFAULT_REGION")) } } fn try_config( config_reader: fn() -> Result(String, Nil), profile: String, ) -> Option(String) { use text <- option_try(config_reader()) use parsed <- option_try(ini.parse(text)) let section = case profile { "default" -> "default" other -> "profile " <> other } non_empty(ini.get_property(parsed, section: section, key: "region")) } fn non_empty(result: Result(String, e)) -> Option(String) { case result { Ok(s) -> case s { "" -> None _ -> Some(s) } Error(_) -> None } } fn option_try( result: Result(a, e), then next: fn(a) -> Option(b), ) -> Option(b) { case result { Ok(value) -> next(value) Error(_) -> None } } // --- FFI wrapper, scoped to this module --- @external(erlang, "aws_ffi", "read_file") fn read_file_bits(path: String) -> Result(BitArray, Nil) fn read_default_config() -> Result(String, Nil) { use home <- result.try(get_env("HOME")) use bits <- result.try(read_file_bits(home <> "/.aws/config")) bit_array.to_string(bits) |> result.replace_error(Nil) }