1use crate::pubsub::PubSub;
15use doido_core::{anyhow::anyhow, Environment, Result};
16use serde::Deserialize;
17use std::sync::Arc;
18use std::time::Duration;
19
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum Backend {
24 #[default]
25 Memory,
26 Redis,
27 Db,
28}
29
30#[derive(Clone, Debug)]
32pub struct CableConfig {
33 pub backend: Backend,
34 pub ping_interval: Duration,
36 pub mount_path: String,
38 pub redis_url: Option<String>,
39}
40
41impl Default for CableConfig {
42 fn default() -> Self {
43 Self {
44 backend: Backend::Memory,
45 ping_interval: Duration::from_secs(3),
46 mount_path: "/cable".to_string(),
47 redis_url: None,
48 }
49 }
50}
51
52pub async fn build_pubsub(config: &CableConfig) -> Result<Arc<dyn PubSub>> {
55 match config.backend {
56 Backend::Memory => Ok(Arc::new(crate::pubsub::MemoryPubSub::new())),
57 Backend::Redis => build_redis_pubsub(config).await,
58 Backend::Db => Err(anyhow!(
59 "the `db` cable backend must be built with a database connection via \
60 build_configured_pubsub()"
61 )),
62 }
63}
64
65pub async fn build_configured_pubsub(config: &CableConfig) -> Result<Arc<dyn PubSub>> {
68 match config.backend {
69 Backend::Db => build_db_pubsub().await,
70 _ => build_pubsub(config).await,
71 }
72}
73
74#[cfg(feature = "cable-redis")]
75async fn build_redis_pubsub(config: &CableConfig) -> Result<Arc<dyn PubSub>> {
76 let url = config
77 .redis_url
78 .as_deref()
79 .ok_or_else(|| anyhow!("redis cable backend selected but [cable.redis] url is not set"))?;
80 Ok(Arc::new(
81 crate::redis_pubsub::RedisPubSub::connect(url).await?,
82 ))
83}
84
85#[cfg(not(feature = "cable-redis"))]
86async fn build_redis_pubsub(_config: &CableConfig) -> Result<Arc<dyn PubSub>> {
87 Err(anyhow!(
88 "redis cable backend requires building doido-cable with the `cable-redis` feature"
89 ))
90}
91
92#[cfg(feature = "cable-db")]
93async fn build_db_pubsub() -> Result<Arc<dyn PubSub>> {
94 let conn = match doido_model::pool::try_pool() {
95 Some(pool) => pool.clone(),
96 None => doido_model::pool::connect()
97 .await
98 .map_err(|e| anyhow!("cable db backend: could not connect to the database: {e}"))?,
99 };
100 Ok(Arc::new(crate::db_pubsub::DbPubSub::connect(conn).await?))
101}
102
103#[cfg(not(feature = "cable-db"))]
104async fn build_db_pubsub() -> Result<Arc<dyn PubSub>> {
105 Err(anyhow!(
106 "db cable backend requires building doido-cable with the `cable-db` feature"
107 ))
108}
109
110#[derive(Debug, Clone, Default, Deserialize)]
113pub struct RedisSettings {
114 #[serde(default)]
115 pub url: Option<String>,
116}
117
118#[derive(Debug, Clone, Default, Deserialize)]
120pub struct CableSettings {
121 #[serde(default, rename = "type")]
122 pub backend: Backend,
123 #[serde(default)]
124 pub ping_interval: Option<u64>,
125 #[serde(default)]
126 pub mount_path: Option<String>,
127 #[serde(default)]
128 pub redis: Option<RedisSettings>,
129}
130
131impl CableSettings {
132 pub fn into_config(self) -> CableConfig {
133 let d = CableConfig::default();
134 CableConfig {
135 backend: self.backend,
136 ping_interval: self
137 .ping_interval
138 .filter(|s| *s > 0)
139 .map(Duration::from_secs)
140 .unwrap_or(d.ping_interval),
141 mount_path: self.mount_path.unwrap_or(d.mount_path),
142 redis_url: self.redis.and_then(|r| r.url).or(d.redis_url),
143 }
144 }
145}
146
147#[derive(Debug, Clone, Default, Deserialize)]
149pub struct CableFileConfig {
150 #[serde(default)]
151 pub cable: CableSettings,
152}
153
154impl CableFileConfig {
155 pub fn load() -> std::io::Result<Self> {
156 Self::load_env(Environment::get_env())
157 }
158
159 pub fn load_env(env: Environment) -> std::io::Result<Self> {
160 let path = format!("config/{}.yml", env.as_str());
161 let contents = std::fs::read_to_string(&path)?;
162 Self::from_yaml(&contents)
163 }
164
165 pub fn from_yaml(yaml: &str) -> std::io::Result<Self> {
166 serde_norway::from_str(yaml)
167 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
168 }
169}
170
171pub fn load() -> CableConfig {
174 CableFileConfig::load()
175 .map(|c| c.cable.into_config())
176 .unwrap_or_default()
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn parses_cable_section() {
185 let yaml = "cable:\n type: redis\n ping_interval: 10\n mount_path: /ws\n \
186 redis:\n url: redis://x:6379\n";
187 let cfg = CableFileConfig::from_yaml(yaml)
188 .unwrap()
189 .cable
190 .into_config();
191 assert_eq!(cfg.backend, Backend::Redis);
192 assert_eq!(cfg.ping_interval, Duration::from_secs(10));
193 assert_eq!(cfg.mount_path, "/ws");
194 assert_eq!(cfg.redis_url.as_deref(), Some("redis://x:6379"));
195 }
196
197 #[test]
198 fn absent_section_falls_back_to_memory_defaults() {
199 let cfg = CableFileConfig::from_yaml("server:\n port: 3000\n")
200 .unwrap()
201 .cable
202 .into_config();
203 assert_eq!(cfg.backend, Backend::Memory);
204 assert_eq!(cfg.ping_interval, Duration::from_secs(3));
205 assert_eq!(cfg.mount_path, "/cable");
206 }
207}