1use super::BindingValue;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
17#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
18#[serde(tag = "service")]
19pub enum RedisBinding {
20 #[serde(rename = "elasticache")]
22 ElastiCache(ElastiCacheRedisBinding),
23 #[serde(rename = "memorystore")]
25 Memorystore(MemorystoreRedisBinding),
26 #[serde(rename = "azure-managed-redis")]
28 AzureManagedRedis(AzureManagedRedisBinding),
29 #[serde(rename = "external")]
31 External(ExternalRedisBinding),
32 #[serde(rename = "local-redis")]
34 Local(LocalRedisBinding),
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
40#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
41#[serde(rename_all = "camelCase")]
42pub struct ElastiCacheRedisBinding {
43 pub host: BindingValue<String>,
44 pub port: BindingValue<u16>,
45 pub username: BindingValue<String>,
46 pub password_secret_arn: BindingValue<String>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
53#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
54#[serde(rename_all = "camelCase")]
55pub struct MemorystoreRedisBinding {
56 pub host: BindingValue<String>,
57 pub port: BindingValue<u16>,
58 pub username: BindingValue<String>,
59 pub server_ca_certificates: BindingValue<Vec<String>>,
61 pub instance_name: BindingValue<String>,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
69#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
70#[serde(rename_all = "camelCase")]
71pub struct AzureManagedRedisBinding {
72 pub host: BindingValue<String>,
73 pub port: BindingValue<u16>,
74 pub username: BindingValue<String>,
75 pub database_resource_id: BindingValue<String>,
78}
79
80#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
83#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
84#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
85#[serde(rename_all = "camelCase")]
86pub struct ExternalRedisBinding {
87 pub host: BindingValue<String>,
88 pub port: BindingValue<u16>,
89 pub username: BindingValue<String>,
90 #[serde(deserialize_with = "deserialize_external_password")]
94 #[cfg_attr(feature = "openapi", schema(value_type = ExternalPasswordInput, inline))]
95 #[cfg_attr(feature = "jsonschema", schemars(with = "ExternalPasswordInput"))]
96 pub password: BindingValue<String>,
97 #[serde(default = "default_true")]
99 pub tls: bool,
100}
101
102#[derive(Deserialize)]
105#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
106#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
107#[serde(untagged)]
108enum ExternalPasswordInput {
109 Literal(String),
110 Reference(ExternalPasswordSecretKeyRef),
111}
112
113#[derive(Deserialize)]
114#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
115#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
116#[serde(rename_all = "camelCase", deny_unknown_fields)]
117struct ExternalPasswordSecretKeyRef {
118 secret_key_ref: super::SecretReference,
119}
120
121fn deserialize_external_password<'de, D>(deserializer: D) -> Result<BindingValue<String>, D::Error>
122where
123 D: serde::Deserializer<'de>,
124{
125 let value = serde_json::Value::deserialize(deserializer)?;
126 match serde_json::from_value::<ExternalPasswordInput>(value).map_err(|_| {
127 serde::de::Error::custom(
128 "expected a password string or {\"secretKeyRef\": {\"name\": ..., \"key\": ...}}",
129 )
130 })? {
131 ExternalPasswordInput::Literal(value) => Ok(BindingValue::Value(value)),
132 ExternalPasswordInput::Reference(reference) => {
133 Ok(BindingValue::Expression(serde_json::json!({
134 "secretKeyRef": {"name": reference.secret_key_ref.name, "key": reference.secret_key_ref.key}
135 })))
136 }
137 }
138}
139
140#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
143#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
144#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
145#[serde(rename_all = "camelCase")]
146pub struct LocalRedisBinding {
147 pub host: BindingValue<String>,
148 pub port: BindingValue<u16>,
149 pub username: BindingValue<String>,
150 pub password: String,
151}
152
153fn default_true() -> bool {
154 true
155}
156
157impl std::fmt::Debug for ExternalRedisBinding {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 f.debug_struct("ExternalRedisBinding")
160 .field("host", &self.host)
161 .field("port", &self.port)
162 .field("username", &self.username)
163 .field("password", &"<redacted>")
164 .field("tls", &self.tls)
165 .finish()
166 }
167}
168
169impl std::fmt::Debug for LocalRedisBinding {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 f.debug_struct("LocalRedisBinding")
172 .field("host", &self.host)
173 .field("port", &self.port)
174 .field("username", &self.username)
175 .field("password", &"<redacted>")
176 .finish()
177 }
178}
179
180impl RedisBinding {
181 pub fn local(
183 host: impl Into<BindingValue<String>>,
184 port: u16,
185 username: impl Into<BindingValue<String>>,
186 password: impl Into<String>,
187 ) -> Self {
188 Self::Local(LocalRedisBinding {
189 host: host.into(),
190 port: BindingValue::value(port),
191 username: username.into(),
192 password: password.into(),
193 })
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn tags_are_distinct_from_the_key_value_store() {
203 let local = RedisBinding::local("127.0.0.1", 6379, "default", "secret");
204 let json = serde_json::to_value(&local).unwrap();
205 assert_eq!(json["service"], "local-redis");
206 assert_eq!(serde_json::from_value::<RedisBinding>(json).unwrap(), local);
207
208 let kv = serde_json::json!({"service": "redis", "host": "h", "port": 1});
210 assert!(serde_json::from_value::<RedisBinding>(kv).is_err());
211 }
212
213 #[test]
214 fn external_binding_defaults_to_tls_and_keeps_secret_refs() {
215 let input = serde_json::json!({
216 "service": "external", "host": "cache.internal", "port": 6379,
217 "username": "default",
218 "password": {"secretKeyRef": {"name": "redis-credentials", "key": "password"}}
219 });
220 let binding: RedisBinding = serde_json::from_value(input.clone()).unwrap();
221 let RedisBinding::External(external) = &binding else {
222 panic!("expected external binding");
223 };
224 assert!(external.tls);
225 assert!(!format!("{binding:?}").contains("redis-credentials"));
226 assert_eq!(
227 serde_json::to_value(&binding).unwrap()["password"],
228 input["password"]
229 );
230 }
231
232 #[test]
233 fn external_password_accepts_a_string_or_secret_key_ref_only() {
234 let binding = |password: serde_json::Value| {
235 serde_json::from_value::<RedisBinding>(serde_json::json!({
236 "service": "external", "host": "h", "port": 6379,
237 "username": "default", "password": password
238 }))
239 };
240 assert!(binding(serde_json::json!("p\"w\\d")).is_ok());
241 assert!(binding(serde_json::json!({"secretKeyRef": {"name": "n", "key": "k"}})).is_ok());
242 assert!(binding(serde_json::json!({"secretRef": {"name": "n", "key": "k"}})).is_err());
244 assert!(binding(serde_json::json!(5)).is_err());
245 assert!(binding(serde_json::json!({"secretKeyRef": {"name": "n"}})).is_err());
246 }
247
248 #[test]
249 fn debug_output_never_prints_passwords() {
250 let local = RedisBinding::local("127.0.0.1", 6379, "default", "hunter2");
251 assert!(!format!("{local:?}").contains("hunter2"));
252 }
253}