1use super::BindingValue;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
13#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
14#[serde(tag = "service", rename_all = "lowercase")]
18pub enum PostgresBinding {
19 Aurora(AuroraPostgresBinding),
21 #[serde(rename = "aurora-managed")]
24 AuroraManaged(AuroraPostgresBinding),
25 #[serde(rename = "cloud-sql")]
27 CloudSql(CloudSqlPostgresBinding),
28 #[serde(rename = "flexible-server")]
30 FlexibleServer(FlexibleServerPostgresBinding),
31 External(ExternalPostgresBinding),
33 #[serde(rename = "local-postgres")]
35 Local(LocalPostgresBinding),
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
41#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
42#[serde(rename_all = "camelCase")]
43pub struct AuroraPostgresBinding {
44 pub cluster_endpoint: BindingValue<String>,
45 pub port: BindingValue<u16>,
46 pub database: BindingValue<String>,
47 pub username: BindingValue<String>,
48 pub password_secret_arn: BindingValue<String>,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
55#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
56#[serde(rename_all = "camelCase")]
57pub struct CloudSqlPostgresBinding {
58 pub host: BindingValue<String>,
59 pub port: BindingValue<u16>,
60 pub database: BindingValue<String>,
61 pub username: BindingValue<String>,
62 pub server_ca_certificates: BindingValue<Vec<String>>,
65 pub password_secret_name: BindingValue<String>,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
72#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
73#[serde(rename_all = "camelCase")]
74pub struct FlexibleServerPostgresBinding {
75 pub host: BindingValue<String>,
76 pub port: BindingValue<u16>,
77 pub database: BindingValue<String>,
78 pub username: BindingValue<String>,
79 pub password_secret_uri: BindingValue<String>,
81}
82
83#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
88#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
89#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
90#[serde(rename_all = "kebab-case")]
91pub enum ExternalPostgresSslMode {
92 #[default]
94 VerifyFull,
95 Disable,
97}
98
99#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
102#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
103#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
104#[serde(rename_all = "camelCase")]
105pub struct ExternalPostgresBinding {
106 pub host: BindingValue<String>,
107 pub port: BindingValue<u16>,
108 pub database: BindingValue<String>,
109 pub username: BindingValue<String>,
110 pub password: String,
114 #[serde(default)]
116 pub ssl_mode: ExternalPostgresSslMode,
117}
118
119#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
122#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
123#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
124#[serde(rename_all = "camelCase")]
125pub struct LocalPostgresBinding {
126 pub host: BindingValue<String>,
127 pub port: BindingValue<u16>,
128 pub database: BindingValue<String>,
129 pub username: BindingValue<String>,
130 pub password: String,
131}
132
133impl std::fmt::Debug for ExternalPostgresBinding {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 f.debug_struct("ExternalPostgresBinding")
138 .field("host", &self.host)
139 .field("port", &self.port)
140 .field("database", &self.database)
141 .field("username", &self.username)
142 .field("password", &"<redacted>")
143 .field("ssl_mode", &self.ssl_mode)
144 .finish()
145 }
146}
147
148impl std::fmt::Debug for LocalPostgresBinding {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 f.debug_struct("LocalPostgresBinding")
151 .field("host", &self.host)
152 .field("port", &self.port)
153 .field("database", &self.database)
154 .field("username", &self.username)
155 .field("password", &"<redacted>")
156 .finish()
157 }
158}
159
160impl PostgresBinding {
161 pub fn local(
163 host: impl Into<BindingValue<String>>,
164 port: u16,
165 database: impl Into<BindingValue<String>>,
166 username: impl Into<BindingValue<String>>,
167 password: impl Into<String>,
168 ) -> Self {
169 Self::Local(LocalPostgresBinding {
170 host: host.into(),
171 port: BindingValue::value(port),
172 database: database.into(),
173 username: username.into(),
174 password: password.into(),
175 })
176 }
177
178 pub fn external(
180 host: impl Into<BindingValue<String>>,
181 port: u16,
182 database: impl Into<BindingValue<String>>,
183 username: impl Into<BindingValue<String>>,
184 password: impl Into<String>,
185 ) -> Self {
186 Self::External(ExternalPostgresBinding {
187 host: host.into(),
188 port: BindingValue::value(port),
189 database: database.into(),
190 username: username.into(),
191 password: password.into(),
192 ssl_mode: ExternalPostgresSslMode::default(),
193 })
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn local_binding_uses_local_postgres_tag() {
203 let binding = PostgresBinding::local("127.0.0.1", 5432, "db", "alien", "secret");
204 let json = serde_json::to_string(&binding).unwrap();
205 assert!(json.contains(r#""service":"local-postgres""#));
206 let deserialized: PostgresBinding = serde_json::from_str(&json).unwrap();
207 assert_eq!(binding, deserialized);
208 }
209
210 #[test]
211 fn external_binding_uses_external_tag() {
212 let binding = PostgresBinding::external("db.internal", 5432, "app", "alien", "secret");
213 let json = serde_json::to_string(&binding).expect("external binding serializes");
214 assert!(json.contains(r#""service":"external""#));
215 assert!(json.contains(r#""sslMode":"verify-full""#));
216 let deserialized: PostgresBinding =
217 serde_json::from_str(&json).expect("external binding deserializes");
218 assert_eq!(binding, deserialized);
219 }
220
221 #[test]
222 fn external_binding_without_ssl_mode_defaults_to_verified_tls() {
223 let json = r#"{
224 "service": "external",
225 "host": "db.internal",
226 "port": 5432,
227 "database": "app",
228 "username": "alien",
229 "password": "secret"
230 }"#;
231
232 let binding: PostgresBinding =
233 serde_json::from_str(json).expect("legacy external binding deserializes");
234
235 let PostgresBinding::External(binding) = binding else {
236 panic!("expected external Postgres binding");
237 };
238 assert_eq!(binding.ssl_mode, ExternalPostgresSslMode::VerifyFull);
239 }
240
241 #[test]
242 fn external_binding_accepts_explicit_plaintext_opt_out() {
243 let json = r#"{
244 "service": "external",
245 "host": "db.internal",
246 "port": 5432,
247 "database": "app",
248 "username": "alien",
249 "password": "secret",
250 "sslMode": "disable"
251 }"#;
252
253 let binding: PostgresBinding =
254 serde_json::from_str(json).expect("plaintext external binding deserializes");
255
256 let PostgresBinding::External(binding) = binding else {
257 panic!("expected external Postgres binding");
258 };
259 assert_eq!(binding.ssl_mode, ExternalPostgresSslMode::Disable);
260 }
261
262 #[test]
263 fn cloud_variants_keep_hyphenated_tags() {
264 let aurora = PostgresBinding::Aurora(AuroraPostgresBinding {
265 cluster_endpoint: "cluster.rds.amazonaws.com".into(),
266 port: BindingValue::value(5432),
267 database: "db".into(),
268 username: "alien".into(),
269 password_secret_arn: "arn:aws:secretsmanager:...".into(),
270 });
271 assert!(serde_json::to_string(&aurora)
272 .unwrap()
273 .contains(r#""service":"aurora""#));
274
275 let cloud_sql = PostgresBinding::CloudSql(CloudSqlPostgresBinding {
276 host: "10.0.0.5".into(),
277 port: BindingValue::value(5432),
278 database: "db".into(),
279 username: "alien".into(),
280 server_ca_certificates: BindingValue::value(vec!["cloud-sql-root".to_string()]),
281 password_secret_name: "pg-credentials".into(),
282 });
283 assert!(serde_json::to_string(&cloud_sql)
284 .unwrap()
285 .contains(r#""service":"cloud-sql""#));
286
287 let flexible = PostgresBinding::FlexibleServer(FlexibleServerPostgresBinding {
288 host: "10.0.0.6".into(),
289 port: BindingValue::value(5432),
290 database: "db".into(),
291 username: "alien".into(),
292 password_secret_uri: "https://vault.vault.azure.net/secrets/pg".into(),
293 });
294 assert!(serde_json::to_string(&flexible)
295 .unwrap()
296 .contains(r#""service":"flexible-server""#));
297 }
298}