use crate::error::{ErrorData, Result};
use crate::traits::{Binding, Postgres, PostgresConnectionParams, SslMode};
use alien_core::bindings::{BindingValue, PostgresBinding};
use alien_error::{AlienError, Context};
#[derive(Debug)]
pub struct LocalPostgres {
params: PostgresConnectionParams,
}
impl LocalPostgres {
pub fn new(params: PostgresConnectionParams) -> Self {
Self { params }
}
pub fn from_binding(binding_name: &str, binding: &PostgresBinding) -> Result<Self> {
let params = match binding {
PostgresBinding::Local(b) => resolve_params(
binding_name,
&b.host,
&b.port,
&b.database,
&b.username,
&b.password,
SslMode::Disable,
)?,
PostgresBinding::External(b) => resolve_params(
binding_name,
&b.host,
&b.port,
&b.database,
&b.username,
&b.password,
SslMode::Prefer,
)?,
PostgresBinding::Aurora(_)
| PostgresBinding::CloudSql(_)
| PostgresBinding::FlexibleServer(_) => {
let backend = match binding {
PostgresBinding::Aurora(_) => "Aurora (AWS)",
PostgresBinding::CloudSql(_) => "Cloud SQL (GCP)",
PostgresBinding::FlexibleServer(_) => "Azure Flexible Server",
_ => "cloud",
};
return Err(AlienError::new(ErrorData::BindingConfigInvalid {
binding_name: binding_name.to_string(),
reason: format!(
"{backend} Postgres bindings are resolved in-process by the workload SDK, \
not this Rust provider"
),
}));
}
};
Ok(Self::new(params))
}
}
#[allow(clippy::too_many_arguments)]
fn resolve_params(
binding_name: &str,
host: &BindingValue<String>,
port: &BindingValue<u16>,
database: &BindingValue<String>,
username: &BindingValue<String>,
password: &str,
sslmode: SslMode,
) -> Result<PostgresConnectionParams> {
let invalid = |field: &str| ErrorData::BindingConfigInvalid {
binding_name: binding_name.to_string(),
reason: format!("Failed to extract '{}' from Postgres binding", field),
};
Ok(PostgresConnectionParams {
host: host
.clone()
.into_value(binding_name, "host")
.context(invalid("host"))?,
port: port
.clone()
.into_value(binding_name, "port")
.context(invalid("port"))?,
database: database
.clone()
.into_value(binding_name, "database")
.context(invalid("database"))?,
username: username
.clone()
.into_value(binding_name, "username")
.context(invalid("username"))?,
password: password.to_string(),
sslmode,
})
}
impl Binding for LocalPostgres {}
impl Postgres for LocalPostgres {
fn connection_params(&self) -> PostgresConnectionParams {
self.params.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_binding_resolves_to_disable_sslmode_connection_string() {
let binding = PostgresBinding::local("127.0.0.1", 6543, "db", "alien", "p@ss/word");
let pg = LocalPostgres::from_binding("db", &binding).expect("local binding resolves");
let params = pg.connection_params();
assert_eq!(params.host, "127.0.0.1");
assert_eq!(params.port, 6543);
assert_eq!(
pg.connection_string(),
"postgres://alien:p%40ss%2Fword@127.0.0.1:6543/db?sslmode=disable"
);
}
#[test]
fn connection_string_percent_encodes_rfc3986_sub_delims() {
let binding = PostgresBinding::local("h", 5432, "db", "alien", "a!b*c'd(e)f");
let pg = LocalPostgres::from_binding("db", &binding).expect("local binding resolves");
assert_eq!(
pg.connection_string(),
"postgres://alien:a%21b%2Ac%27d%28e%29f@h:5432/db?sslmode=disable"
);
}
#[test]
fn cloud_binding_resolution_is_rejected_in_this_build() {
let binding = PostgresBinding::Aurora(alien_core::bindings::AuroraPostgresBinding {
cluster_endpoint: "cluster.rds.amazonaws.com".into(),
port: BindingValue::value(5432),
database: "db".into(),
username: "alien".into(),
password_secret_arn: "arn:aws:secretsmanager:...".into(),
});
assert!(LocalPostgres::from_binding("db", &binding).is_err());
}
}