1use crate::fake::FakeRedis;
2use crate::pool::RedisPool;
3use sova_core::{App, Error, Plugin};
4
5pub struct Redis {
7 mode: Mode,
8}
9
10enum Mode {
11 Url(String),
12 Fake(FakeRedis),
13}
14
15impl Redis {
16 pub fn from_env() -> Self {
17 let url = std::env::var("REDIS_URL").unwrap_or_default();
18 Self {
19 mode: Mode::Url(url),
20 }
21 }
22
23 pub fn url(mut self, url: impl Into<String>) -> Self {
24 self.mode = Mode::Url(url.into());
25 self
26 }
27
28 pub fn fake(fake: FakeRedis) -> Self {
30 Self {
31 mode: Mode::Fake(fake),
32 }
33 }
34}
35
36impl Plugin for Redis {
37 fn id(&self) -> &'static str {
38 "redis"
39 }
40
41 fn meta(&self) -> sova_core::PluginMeta {
42 sova_core::PluginMeta::new("Redis")
43 .description(
44 "Shared Redis/Valkey connection for KvStore, tasks, cache, pub/sub, list queues",
45 )
46 .version(env!("CARGO_PKG_VERSION"))
47 }
48
49 fn install(mut self, app: &mut App) {
50 if let Mode::Fake(fake) = self.mode {
51 let pool = RedisPool::new();
52 pool.set_fake(fake);
53 app.state(pool);
54 return;
55 }
56
57 if let Mode::Url(url) = &mut self.mode {
59 if let Ok(u) = std::env::var("REDIS_URL") {
60 if !u.is_empty() {
61 *url = u;
62 }
63 }
64 if url.is_empty() {
65 if let Some(u) = app
66 .config_doc()
67 .and_then(|d| d.section("redis"))
68 .and_then(|s| s.get("url").and_then(|v| v.as_str()).map(str::to_string))
69 {
70 *url = u;
71 }
72 }
73 }
74
75 let Mode::Url(url) = self.mode else {
76 return;
77 };
78
79 if url.is_empty() {
80 app.on_startup(|_state| async {
81 Err(Error::Internal(
82 "redis url is empty; set REDIS_URL or [redis] url in sova.toml".into(),
83 ))
84 });
85 return;
86 }
87
88 let pool = RedisPool::new();
89 app.state(pool.clone());
90
91 let url_start = url.clone();
92 let pool_start = pool.clone();
93 app.on_startup(move |_state| {
94 let url = url_start.clone();
95 let pool = pool_start.clone();
96 async move {
97 pool.set_url(url.clone());
98 let conn = RedisPool::connect(&url)
99 .await
100 .map_err(|e| Error::Internal(format!("redis connect: {e}")))?;
101 pool.set(conn);
102 Ok(())
103 }
104 });
105
106 let pool_stop = pool.clone();
107 app.on_shutdown(move || {
108 let pool = pool_stop.clone();
109 async move {
110 pool.clear();
111 }
112 });
113
114 let pool_check = pool.clone();
115 app.register_check("redis", move |_state| {
116 let pool = pool_check.clone();
117 async move {
118 let mut conn = pool.get().map_err(Error::from)?;
119 let _: String = redis::cmd("PING")
120 .query_async(&mut conn)
121 .await
122 .map_err(|e| Error::Internal(format!("redis ping: {e}")))?;
123 Ok(())
124 }
125 });
126 }
127}