1use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11pub const DEFAULT_PORT: u16 = 5439;
13
14fn default_port() -> u16 {
15 DEFAULT_PORT
16}
17
18fn default_tls() -> bool {
19 true
20}
21
22#[derive(Clone, Serialize, Deserialize, JsonSchema)]
37#[serde(tag = "type", content = "config", rename_all = "snake_case")]
38pub enum RedshiftCredentials {
39 Password {
42 password: String,
45 },
46 Iam {
50 #[serde(default, skip_serializing_if = "Option::is_none")]
52 region: Option<String>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
55 cluster_identifier: Option<String>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
58 db_user: Option<String>,
59 },
60 RedshiftDataApi {
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 region: Option<String>,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 cluster_identifier: Option<String>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
72 workgroup_name: Option<String>,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
75 secret_arn: Option<String>,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
78 db_user: Option<String>,
79 },
80}
81
82impl std::fmt::Debug for RedshiftCredentials {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 match self {
85 Self::Password { .. } => write!(f, "Password(***)"),
86 Self::Iam {
87 region,
88 cluster_identifier,
89 db_user,
90 } => f
91 .debug_struct("Iam")
92 .field("region", region)
93 .field("cluster_identifier", cluster_identifier)
94 .field("db_user", db_user)
95 .finish(),
96 Self::RedshiftDataApi {
97 region,
98 cluster_identifier,
99 workgroup_name,
100 secret_arn,
101 db_user,
102 } => f
103 .debug_struct("RedshiftDataApi")
104 .field("region", region)
105 .field("cluster_identifier", cluster_identifier)
106 .field("workgroup_name", workgroup_name)
107 .field("secret_arn", secret_arn)
108 .field("db_user", db_user)
109 .finish(),
110 }
111 }
112}
113
114#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
118pub struct RedshiftConnection {
119 pub host: String,
121 #[serde(default = "default_port")]
123 pub port: u16,
124 pub database: String,
126 pub user: String,
128 pub credentials: RedshiftCredentials,
130 #[serde(default = "default_tls")]
135 pub tls: bool,
136}
137
138impl RedshiftConnection {
139 pub fn new(
141 host: impl Into<String>,
142 database: impl Into<String>,
143 user: impl Into<String>,
144 password: impl Into<String>,
145 ) -> Self {
146 Self {
147 host: host.into(),
148 port: DEFAULT_PORT,
149 database: database.into(),
150 user: user.into(),
151 credentials: RedshiftCredentials::Password {
152 password: password.into(),
153 },
154 tls: true,
155 }
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn debug_masks_password() {
165 let c = RedshiftCredentials::Password {
166 password: "s3cr3t".into(),
167 };
168 let dbg = format!("{c:?}");
169 assert!(dbg.contains("***"));
170 assert!(!dbg.contains("s3cr3t"));
171 }
172
173 #[test]
174 fn connection_debug_does_not_leak_password() {
175 let conn = RedshiftConnection::new("host", "db", "user", "hunter2");
176 let dbg = format!("{conn:?}");
177 assert!(!dbg.contains("hunter2"));
178 assert!(dbg.contains("host"));
179 assert!(dbg.contains("user"));
180 }
181
182 #[test]
183 fn password_credentials_round_trip() {
184 let c = RedshiftCredentials::Password {
185 password: "pw".into(),
186 };
187 let json = serde_json::to_string(&c).unwrap();
188 assert_eq!(json, r#"{"type":"password","config":{"password":"pw"}}"#);
189 let back: RedshiftCredentials = serde_json::from_str(&json).unwrap();
190 assert!(matches!(back, RedshiftCredentials::Password { .. }));
191 }
192
193 #[test]
194 fn connection_defaults_port_and_tls() {
195 let json = r#"{
196 "host": "h",
197 "database": "db",
198 "user": "u",
199 "credentials": {"type": "password", "config": {"password": "pw"}}
200 }"#;
201 let conn: RedshiftConnection = serde_json::from_str(json).unwrap();
202 assert_eq!(conn.port, DEFAULT_PORT);
203 assert!(conn.tls);
204 }
205
206 #[test]
207 fn iam_variant_deserializes() {
208 let json = r#"{"type":"iam","config":{"region":"us-east-1","db_user":"analyst"}}"#;
209 let c: RedshiftCredentials = serde_json::from_str(json).unwrap();
210 match c {
211 RedshiftCredentials::Iam {
212 region, db_user, ..
213 } => {
214 assert_eq!(region.as_deref(), Some("us-east-1"));
215 assert_eq!(db_user.as_deref(), Some("analyst"));
216 }
217 _ => panic!("expected Iam"),
218 }
219 }
220
221 #[test]
222 fn redshift_data_api_variant_deserializes() {
223 let json =
224 r#"{"type":"redshift_data_api","config":{"workgroup_name":"wg","secret_arn":"arn:x"}}"#;
225 let c: RedshiftCredentials = serde_json::from_str(json).unwrap();
226 assert!(matches!(c, RedshiftCredentials::RedshiftDataApi { .. }));
227 }
228
229 #[test]
230 fn iam_debug_renders_fields() {
231 let c = RedshiftCredentials::Iam {
232 region: Some("us-west-2".into()),
233 cluster_identifier: Some("prod-cluster".into()),
234 db_user: Some("analyst".into()),
235 };
236 let dbg = format!("{c:?}");
237 assert!(dbg.contains("Iam"));
238 assert!(dbg.contains("us-west-2"));
239 assert!(dbg.contains("prod-cluster"));
240 assert!(dbg.contains("analyst"));
241 }
242
243 #[test]
244 fn redshift_data_api_debug_renders_fields() {
245 let c = RedshiftCredentials::RedshiftDataApi {
246 region: Some("eu-central-1".into()),
247 cluster_identifier: None,
248 workgroup_name: Some("wg-1".into()),
249 secret_arn: Some("arn:aws:secretsmanager:x".into()),
250 db_user: Some("svc".into()),
251 };
252 let dbg = format!("{c:?}");
253 assert!(dbg.contains("RedshiftDataApi"));
254 assert!(dbg.contains("eu-central-1"));
255 assert!(dbg.contains("wg-1"));
256 assert!(dbg.contains("svc"));
257 }
258
259 #[test]
260 fn iam_and_data_api_round_trip_full_fields() {
261 let iam = r#"{"type":"iam","config":{"region":"us-east-1","cluster_identifier":"c1","db_user":"u"}}"#;
262 let back: RedshiftCredentials = serde_json::from_str(iam).unwrap();
263 assert_eq!(serde_json::to_string(&back).unwrap(), iam);
264
265 let api = r#"{"type":"redshift_data_api","config":{"region":"us-east-1","cluster_identifier":"c1","workgroup_name":"wg","secret_arn":"arn","db_user":"u"}}"#;
266 let back: RedshiftCredentials = serde_json::from_str(api).unwrap();
267 assert_eq!(serde_json::to_string(&back).unwrap(), api);
268 }
269}