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")]
pub enum RedisBinding {
#[serde(rename = "elasticache")]
ElastiCache(ElastiCacheRedisBinding),
#[serde(rename = "memorystore")]
Memorystore(MemorystoreRedisBinding),
#[serde(rename = "azure-managed-redis")]
AzureManagedRedis(AzureManagedRedisBinding),
#[serde(rename = "external")]
External(ExternalRedisBinding),
#[serde(rename = "local-redis")]
Local(LocalRedisBinding),
}
#[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 ElastiCacheRedisBinding {
pub host: BindingValue<String>,
pub port: BindingValue<u16>,
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 MemorystoreRedisBinding {
pub host: BindingValue<String>,
pub port: BindingValue<u16>,
pub username: BindingValue<String>,
pub server_ca_certificates: BindingValue<Vec<String>>,
pub instance_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 AzureManagedRedisBinding {
pub host: BindingValue<String>,
pub port: BindingValue<u16>,
pub username: BindingValue<String>,
pub database_resource_id: BindingValue<String>,
}
#[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 ExternalRedisBinding {
pub host: BindingValue<String>,
pub port: BindingValue<u16>,
pub username: BindingValue<String>,
#[serde(deserialize_with = "deserialize_external_password")]
#[cfg_attr(feature = "openapi", schema(value_type = ExternalPasswordInput, inline))]
#[cfg_attr(feature = "jsonschema", schemars(with = "ExternalPasswordInput"))]
pub password: BindingValue<String>,
#[serde(default = "default_true")]
pub tls: bool,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[serde(untagged)]
enum ExternalPasswordInput {
Literal(String),
Reference(ExternalPasswordSecretKeyRef),
}
#[derive(Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct ExternalPasswordSecretKeyRef {
secret_key_ref: super::SecretReference,
}
fn deserialize_external_password<'de, D>(deserializer: D) -> Result<BindingValue<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
match serde_json::from_value::<ExternalPasswordInput>(value).map_err(|_| {
serde::de::Error::custom(
"expected a password string or {\"secretKeyRef\": {\"name\": ..., \"key\": ...}}",
)
})? {
ExternalPasswordInput::Literal(value) => Ok(BindingValue::Value(value)),
ExternalPasswordInput::Reference(reference) => {
Ok(BindingValue::Expression(serde_json::json!({
"secretKeyRef": {"name": reference.secret_key_ref.name, "key": reference.secret_key_ref.key}
})))
}
}
}
#[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 LocalRedisBinding {
pub host: BindingValue<String>,
pub port: BindingValue<u16>,
pub username: BindingValue<String>,
pub password: String,
}
fn default_true() -> bool {
true
}
impl std::fmt::Debug for ExternalRedisBinding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExternalRedisBinding")
.field("host", &self.host)
.field("port", &self.port)
.field("username", &self.username)
.field("password", &"<redacted>")
.field("tls", &self.tls)
.finish()
}
}
impl std::fmt::Debug for LocalRedisBinding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LocalRedisBinding")
.field("host", &self.host)
.field("port", &self.port)
.field("username", &self.username)
.field("password", &"<redacted>")
.finish()
}
}
impl RedisBinding {
pub fn local(
host: impl Into<BindingValue<String>>,
port: u16,
username: impl Into<BindingValue<String>>,
password: impl Into<String>,
) -> Self {
Self::Local(LocalRedisBinding {
host: host.into(),
port: BindingValue::value(port),
username: username.into(),
password: password.into(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tags_are_distinct_from_the_key_value_store() {
let local = RedisBinding::local("127.0.0.1", 6379, "default", "secret");
let json = serde_json::to_value(&local).unwrap();
assert_eq!(json["service"], "local-redis");
assert_eq!(serde_json::from_value::<RedisBinding>(json).unwrap(), local);
let kv = serde_json::json!({"service": "redis", "host": "h", "port": 1});
assert!(serde_json::from_value::<RedisBinding>(kv).is_err());
}
#[test]
fn external_binding_defaults_to_tls_and_keeps_secret_refs() {
let input = serde_json::json!({
"service": "external", "host": "cache.internal", "port": 6379,
"username": "default",
"password": {"secretKeyRef": {"name": "redis-credentials", "key": "password"}}
});
let binding: RedisBinding = serde_json::from_value(input.clone()).unwrap();
let RedisBinding::External(external) = &binding else {
panic!("expected external binding");
};
assert!(external.tls);
assert!(!format!("{binding:?}").contains("redis-credentials"));
assert_eq!(
serde_json::to_value(&binding).unwrap()["password"],
input["password"]
);
}
#[test]
fn external_password_accepts_a_string_or_secret_key_ref_only() {
let binding = |password: serde_json::Value| {
serde_json::from_value::<RedisBinding>(serde_json::json!({
"service": "external", "host": "h", "port": 6379,
"username": "default", "password": password
}))
};
assert!(binding(serde_json::json!("p\"w\\d")).is_ok());
assert!(binding(serde_json::json!({"secretKeyRef": {"name": "n", "key": "k"}})).is_ok());
assert!(binding(serde_json::json!({"secretRef": {"name": "n", "key": "k"}})).is_err());
assert!(binding(serde_json::json!(5)).is_err());
assert!(binding(serde_json::json!({"secretKeyRef": {"name": "n"}})).is_err());
}
#[test]
fn debug_output_never_prints_passwords() {
let local = RedisBinding::local("127.0.0.1", 6379, "default", "hunter2");
assert!(!format!("{local:?}").contains("hunter2"));
}
}