1use std::collections::HashMap;
2
3use serde::Deserialize;
4
5use super::Validate;
6
7#[derive(Debug, Clone, Default, Deserialize)]
13pub struct RedisConfig {
14 pub url: String,
16}
17
18impl Validate for RedisConfig {
19 fn validate(&self) -> anyhow::Result<()> {
20 if self.url.is_empty() {
21 anyhow::bail!("Redis url 不能为空");
22 }
23 if !self.url.starts_with("redis://") && !self.url.starts_with("rediss://") {
24 anyhow::bail!(
25 "Redis url 格式无效: `{}`,需以 `redis://` 或 `rediss://` 开头",
26 self.url
27 );
28 }
29 Ok(())
30 }
31}
32
33pub struct RedisConfigBuilder(pub(crate) RedisConfig);
39
40impl RedisConfigBuilder {
41 pub fn url(mut self, v: impl Into<String>) -> Self {
42 self.0.url = v.into();
43 self
44 }
45}
46
47pub(crate) fn collect_env_redis(
52 prefix: &str,
53 existing: &HashMap<String, RedisConfig>,
54) -> anyhow::Result<HashMap<String, RedisConfig>> {
55 let mut result = HashMap::new();
56 let pfx_upper = prefix.to_uppercase();
57
58 for (key, val) in std::env::vars() {
59 let upper = key.to_uppercase();
60 let rest = match upper.strip_prefix(&format!("{pfx_upper}_REDIS_")) {
61 Some(r) => r,
62 None => continue,
63 };
64 let (name, field) = match rest.rsplit_once('_') {
65 Some((n, f)) => (n.to_lowercase(), f),
66 None => continue,
67 };
68
69 if field == "URL" {
70 let entry = result
71 .entry(name.clone())
72 .or_insert_with(|| existing.get(&name).cloned().unwrap_or_default());
73 entry.url = val;
74 }
75 }
76 Ok(result)
77}
78
79#[cfg(test)]
84mod tests {
85 use crate::ConfigBuilder;
86
87 #[test]
88 fn validation_rejects_bad_redis_url() {
89 let result = ConfigBuilder::new()
90 .with_redis("default", |r| r.url("http://wrong"))
91 .build();
92 assert!(result.is_err());
93 assert!(result.unwrap_err().to_string().contains("redis://"));
94 }
95}