Skip to main content

src/aws/config.gleam

//// Customer-facing construction settings — the service-agnostic knobs
//// every `Client` is built from. AWS endpoint-rule-set parameters
//// (`UseFIPS`, `UseDualStack`, S3 `ForcePathStyle`, …) are NOT here:
//// they vary per service and live on each service's own typed
//// `EndpointParams` record, kept separate so customer config and AWS
//// rule-set attributes never mix.
////
//// Two construction entry points per service:
////   1. `<service>.new()` — full auto: region resolves from the standard
////      AWS sources, credentials from the default chain. Zero config.
////   2. `<service>.new_with(settings, endpoint_params)` — start each
////      record from its defaults and override only the fields you need:
////
////        import aws/config.{Settings, default_settings}
////        import aws/services/s3
////
////        let assert Ok(client) =
////          s3.new_with(
////            Settings(..default_settings(), region: Some("eu-west-1")),
////            s3.EndpointParams(..s3.default_endpoint_params(), use_fips: Some(True)),
////          )

import aws/credentials.{type Provider}
import aws/internal/client/runtime.{type ClientConfig}
import aws/internal/http_send.{type Send, type StreamingSend}
import aws/region
import aws/retry.{type Strategy}
import gleam/option.{type Option, None, Some}
import gleam/result

/// Declarative, service-agnostic settings for building a `Client`. Start
/// from `default_settings()` and override the fields you care about with a
/// record-update spread — see the module docs. AWS endpoint-rule-set
/// parameters are not here; they live on each service's `EndpointParams`.
pub type Settings {
  Settings(
    /// AWS region. `None` (the default) resolves it at build time from
    /// `AWS_REGION`, `AWS_DEFAULT_REGION`, then `~/.aws/config` under
    /// `profile`. `Some(r)` pins it explicitly.
    region: Option(String),
    /// Profile name used for both region and credential resolution.
    profile: String,
    /// Credentials provider. `None` (the default) uses the standard chain
    /// (env → web-identity → SSO → profile → process → ECS → IMDS).
    credentials: Option(Provider),
    /// Endpoint URL override (LocalStack, FIPS, custom DNS). `None` lets
    /// the service's Smithy endpoint rule set compute the URL.
    endpoint_url: Option(String),
    /// Retry attempt budget. `Some(1)` disables retries (one attempt per
    /// request); `None` keeps the standard 3-attempt strategy (or
    /// `retry_strategy`, when that is set).
    max_attempts: Option(Int),
    /// Full retry-strategy override. `None` keeps `retry.standard()`. When
    /// `max_attempts` is also set, the attempt budget is applied on top of
    /// this strategy.
    retry_strategy: Option(Strategy),
    /// Buffered HTTP transport override (test doubles, proxies). `None`
    /// uses the default httpc sender.
    http_send: Option(Send),
    /// Streaming HTTP transport override for `@streaming` operations.
    /// `None` uses the default chunked sender.
    streaming_http_send: Option(StreamingSend),
    /// Use the HTTP/2 streaming transport. Buffered requests are
    /// unaffected; peers that don't speak HTTP/2 negotiate down via ALPN.
    use_http2: Bool,
    /// Opt into SigV4a (asymmetric ECDSA P-256) signing: `Some(region_set)`
    /// becomes the `X-Amz-Region-Set` header — required for S3 Multi-Region
    /// Access Points. `None` uses SigV4.
    sigv4a_region_set: Option(List(String)),
    /// RFC-3986 dot-segment removal under SigV4a. Leave `True` for most
    /// services; S3 needs `False` so object keys with `.` / `..` survive
    /// the canonical-request step. No effect unless `sigv4a_region_set`
    /// is `Some`.
    sigv4a_normalize_path: Bool,
  )
}

/// The full-auto baseline: region + credentials resolve themselves and
/// every other knob keeps the runtime default. Spread this with a
/// record update and override only the fields you need.
pub fn default_settings() -> Settings {
  Settings(
    region: None,
    profile: "default",
    credentials: None,
    endpoint_url: None,
    max_attempts: None,
    retry_strategy: None,
    http_send: None,
    streaming_http_send: None,
    use_http2: False,
    sigv4a_region_set: None,
    sigv4a_normalize_path: True,
  )
}

/// Resolve `Settings` into a runtime `ClientConfig` for a service,
/// identified by its endpoint prefix + SigV4 signing name. The only
/// failure is region resolution when `region` is `None` and no source
/// supplies one; credentials stay lazy (the per-Client cache fetches them
/// on the first request), so a missing chain surfaces at call time, not
/// here.
///
/// Generated `new` / `new_with` call this, then apply the service's typed
/// `EndpointParams`, attach its endpoint rule set, and start the
/// credentials cache.
pub fn resolve(
  settings: Settings,
  endpoint_prefix endpoint_prefix: String,
  signing_name signing_name: String,
) -> Result(ClientConfig, region.ResolveError) {
  use resolved <- result.map(resolve_region(settings))
  let provider = case settings.credentials {
    Some(p) -> p
    None ->
      credentials.default_chain(
        send: http_send.default_send,
        profile: settings.profile,
      )
  }
  runtime.default_config(resolved, endpoint_prefix, signing_name)
  |> runtime.with_credentials_provider(provider)
  |> apply_endpoint_url(settings)
  |> apply_retry(settings)
  |> apply_transports(settings)
  |> apply_sigv4a(settings)
}

fn resolve_region(settings: Settings) -> Result(String, region.ResolveError) {
  case settings.region {
    Some(r) -> Ok(r)
    None -> region.resolve(profile: settings.profile)
  }
}

fn apply_endpoint_url(
  config: ClientConfig,
  settings: Settings,
) -> ClientConfig {
  case settings.endpoint_url {
    Some(url) -> runtime.with_endpoint_url(config, url)
    None -> config
  }
}

fn apply_retry(config: ClientConfig, settings: Settings) -> ClientConfig {
  let config = case settings.retry_strategy {
    Some(strategy) -> runtime.with_retry_strategy(config, strategy)
    None -> config
  }
  case settings.max_attempts {
    Some(n) -> runtime.with_max_attempts(config, n)
    None -> config
  }
}

fn apply_transports(config: ClientConfig, settings: Settings) -> ClientConfig {
  let config = case settings.http_send {
    Some(send) -> runtime.with_http_send(config, send)
    None -> config
  }
  let config = case settings.streaming_http_send {
    Some(send) -> runtime.with_streaming_http_send(config, send)
    None -> config
  }
  case settings.use_http2 {
    True -> runtime.with_http2(config)
    False -> config
  }
}

fn apply_sigv4a(config: ClientConfig, settings: Settings) -> ClientConfig {
  case settings.sigv4a_region_set {
    None -> config
    Some(region_set) ->
      runtime.with_sigv4a_region_set(config, region_set)
      |> runtime.with_sigv4a_path_normalization(settings.sigv4a_normalize_path)
  }
}