use super::BindingValue;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[serde(tag = "service", rename_all = "lowercase")]
pub enum PostgresBinding {
Aurora(AuroraPostgresBinding),
#[serde(rename = "cloud-sql")]
CloudSql(CloudSqlPostgresBinding),
#[serde(rename = "flexible-server")]
FlexibleServer(FlexibleServerPostgresBinding),
External(ExternalPostgresBinding),
#[serde(rename = "local-postgres")]
Local(LocalPostgresBinding),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct AuroraPostgresBinding {
pub cluster_endpoint: BindingValue<String>,
pub port: BindingValue<u16>,
pub database: BindingValue<String>,
pub username: BindingValue<String>,
pub password_secret_arn: BindingValue<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct CloudSqlPostgresBinding {
pub host: BindingValue<String>,
pub port: BindingValue<u16>,
pub database: BindingValue<String>,
pub username: BindingValue<String>,
pub server_ca_certificates: BindingValue<Vec<String>>,
pub password_secret_name: BindingValue<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct FlexibleServerPostgresBinding {
pub host: BindingValue<String>,
pub port: BindingValue<u16>,
pub database: BindingValue<String>,
pub username: BindingValue<String>,
pub password_secret_uri: BindingValue<String>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum ExternalPostgresSslMode {
#[default]
VerifyFull,
Disable,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ExternalPostgresBinding {
pub host: BindingValue<String>,
pub port: BindingValue<u16>,
pub database: BindingValue<String>,
pub username: BindingValue<String>,
pub password: String,
#[serde(default)]
pub ssl_mode: ExternalPostgresSslMode,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct LocalPostgresBinding {
pub host: BindingValue<String>,
pub port: BindingValue<u16>,
pub database: BindingValue<String>,
pub username: BindingValue<String>,
pub password: String,
}
impl std::fmt::Debug for ExternalPostgresBinding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExternalPostgresBinding")
.field("host", &self.host)
.field("port", &self.port)
.field("database", &self.database)
.field("username", &self.username)
.field("password", &"<redacted>")
.field("ssl_mode", &self.ssl_mode)
.finish()
}
}
impl std::fmt::Debug for LocalPostgresBinding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LocalPostgresBinding")
.field("host", &self.host)
.field("port", &self.port)
.field("database", &self.database)
.field("username", &self.username)
.field("password", &"<redacted>")
.finish()
}
}
impl PostgresBinding {
pub fn local(
host: impl Into<BindingValue<String>>,
port: u16,
database: impl Into<BindingValue<String>>,
username: impl Into<BindingValue<String>>,
password: impl Into<String>,
) -> Self {
Self::Local(LocalPostgresBinding {
host: host.into(),
port: BindingValue::value(port),
database: database.into(),
username: username.into(),
password: password.into(),
})
}
pub fn external(
host: impl Into<BindingValue<String>>,
port: u16,
database: impl Into<BindingValue<String>>,
username: impl Into<BindingValue<String>>,
password: impl Into<String>,
) -> Self {
Self::External(ExternalPostgresBinding {
host: host.into(),
port: BindingValue::value(port),
database: database.into(),
username: username.into(),
password: password.into(),
ssl_mode: ExternalPostgresSslMode::default(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_binding_uses_local_postgres_tag() {
let binding = PostgresBinding::local("127.0.0.1", 5432, "db", "alien", "secret");
let json = serde_json::to_string(&binding).unwrap();
assert!(json.contains(r#""service":"local-postgres""#));
let deserialized: PostgresBinding = serde_json::from_str(&json).unwrap();
assert_eq!(binding, deserialized);
}
#[test]
fn external_binding_uses_external_tag() {
let binding = PostgresBinding::external("db.internal", 5432, "app", "alien", "secret");
let json = serde_json::to_string(&binding).expect("external binding serializes");
assert!(json.contains(r#""service":"external""#));
assert!(json.contains(r#""sslMode":"verify-full""#));
let deserialized: PostgresBinding =
serde_json::from_str(&json).expect("external binding deserializes");
assert_eq!(binding, deserialized);
}
#[test]
fn external_binding_without_ssl_mode_defaults_to_verified_tls() {
let json = r#"{
"service": "external",
"host": "db.internal",
"port": 5432,
"database": "app",
"username": "alien",
"password": "secret"
}"#;
let binding: PostgresBinding =
serde_json::from_str(json).expect("legacy external binding deserializes");
let PostgresBinding::External(binding) = binding else {
panic!("expected external Postgres binding");
};
assert_eq!(binding.ssl_mode, ExternalPostgresSslMode::VerifyFull);
}
#[test]
fn external_binding_accepts_explicit_plaintext_opt_out() {
let json = r#"{
"service": "external",
"host": "db.internal",
"port": 5432,
"database": "app",
"username": "alien",
"password": "secret",
"sslMode": "disable"
}"#;
let binding: PostgresBinding =
serde_json::from_str(json).expect("plaintext external binding deserializes");
let PostgresBinding::External(binding) = binding else {
panic!("expected external Postgres binding");
};
assert_eq!(binding.ssl_mode, ExternalPostgresSslMode::Disable);
}
#[test]
fn cloud_variants_keep_hyphenated_tags() {
let aurora = PostgresBinding::Aurora(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!(serde_json::to_string(&aurora)
.unwrap()
.contains(r#""service":"aurora""#));
let cloud_sql = PostgresBinding::CloudSql(CloudSqlPostgresBinding {
host: "10.0.0.5".into(),
port: BindingValue::value(5432),
database: "db".into(),
username: "alien".into(),
server_ca_certificates: BindingValue::value(vec!["cloud-sql-root".to_string()]),
password_secret_name: "pg-credentials".into(),
});
assert!(serde_json::to_string(&cloud_sql)
.unwrap()
.contains(r#""service":"cloud-sql""#));
let flexible = PostgresBinding::FlexibleServer(FlexibleServerPostgresBinding {
host: "10.0.0.6".into(),
port: BindingValue::value(5432),
database: "db".into(),
username: "alien".into(),
password_secret_uri: "https://vault.vault.azure.net/secrets/pg".into(),
});
assert!(serde_json::to_string(&flexible)
.unwrap()
.contains(r#""service":"flexible-server""#));
}
}