1use crate::error::{Error, Result};
14use serde::{Deserialize, Serialize};
15use std::path::PathBuf;
16
17#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
20pub struct SelfHostedConfig {
21 pub bucket: String,
23 pub region: String,
24 #[serde(default)]
27 pub profile: Option<String>,
28 #[serde(default)]
30 pub endpoint: Option<String>,
31 #[serde(default)]
33 pub table: Option<String>,
34 #[serde(default)]
39 pub gate_url: Option<String>,
40 #[serde(default)]
43 pub distribution_id: Option<String>,
44}
45
46impl SelfHostedConfig {
47 pub fn is_full(&self) -> bool {
49 self.gate_url.is_some()
50 }
51}
52
53#[derive(Serialize, Deserialize, Clone, Debug)]
56pub struct Backend {
57 pub name: String,
58 pub kind: String,
60 #[serde(flatten)]
61 pub config: toml::Table,
62}
63
64impl Backend {
65 pub fn self_hosted(name: &str, cfg: &SelfHostedConfig) -> Result<Backend> {
67 let value = toml::Value::try_from(cfg)
68 .map_err(|e| Error::Config(format!("serializing self-hosted config: {e}")))?;
69 let config = match value {
70 toml::Value::Table(t) => t,
71 _ => {
72 return Err(Error::Config(
73 "self-hosted config did not serialize to a table".into(),
74 ))
75 }
76 };
77 Ok(Backend {
78 name: name.to_string(),
79 kind: "self-hosted".to_string(),
80 config,
81 })
82 }
83}
84
85#[derive(Serialize, Deserialize, Default, Debug)]
87pub struct Registry {
88 pub active: String,
89 pub backends: Vec<Backend>,
90}
91
92impl Registry {
93 pub fn load() -> Result<Registry> {
104 let path = config_path()?;
105 let text = match std::fs::read_to_string(&path) {
106 Ok(t) => t,
107 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
108 return Ok(Registry {
109 active: String::new(),
110 backends: Vec::new(),
111 })
112 }
113 Err(e) => return Err(Error::Config(format!("reading {}: {e}", path.display()))),
114 };
115
116 let table: toml::Table = toml::from_str(&text)
123 .map_err(|e| Error::Config(format!("parsing dove config: {e}")))?;
124
125 if table.contains_key("active") && table.contains_key("backends") {
126 let reg: Registry = toml::Value::Table(table)
127 .try_into()
128 .map_err(|e| Error::Config(format!("parsing dove config registry: {e}")))?;
129 return Ok(reg);
130 }
131
132 let legacy: SelfHostedConfig = toml::Value::Table(table)
135 .try_into()
136 .map_err(|e| Error::Config(format!("parsing dove config: {e}")))?;
137 let backend = Backend::self_hosted("default", &legacy)?;
138 let reg = Registry {
139 active: "default".to_string(),
140 backends: vec![backend],
141 };
142 reg.save()?;
143 Ok(reg)
144 }
145
146 pub fn save(&self) -> Result<()> {
148 let path = config_path()?;
149 if let Some(parent) = path.parent() {
150 std::fs::create_dir_all(parent)
151 .map_err(|e| Error::Config(format!("creating {}: {e}", parent.display())))?;
152 }
153 let text = toml::to_string_pretty(self)
154 .map_err(|e| Error::Config(format!("serializing dove config: {e}")))?;
155 std::fs::write(&path, text)
156 .map_err(|e| Error::Config(format!("writing {}: {e}", path.display())))
157 }
158
159 pub fn active_backend(&self) -> Result<&Backend> {
162 if self.active.is_empty() {
163 return Err(Error::Config(
164 "no dove config yet — run `dove provision` first".into(),
165 ));
166 }
167 self.backends
168 .iter()
169 .find(|b| b.name == self.active)
170 .ok_or_else(|| {
171 Error::Config(format!(
172 "active backend '{}' not found in config — run `dove provision` first",
173 self.active
174 ))
175 })
176 }
177
178 pub fn set_active(&mut self, name: &str) -> Result<()> {
180 if !self.backends.iter().any(|b| b.name == name) {
181 return Err(Error::Config(format!("no backend named '{name}'")));
182 }
183 self.active = name.to_string();
184 Ok(())
185 }
186
187 pub fn upsert(&mut self, b: Backend) {
189 if let Some(existing) = self.backends.iter_mut().find(|x| x.name == b.name) {
190 *existing = b;
191 } else {
192 self.backends.push(b);
193 }
194 }
195
196 pub fn active_self_hosted(&self) -> Result<SelfHostedConfig> {
199 let backend = self.active_backend()?;
200 toml::Value::Table(backend.config.clone())
201 .try_into()
202 .map_err(|e| Error::Config(format!("reading '{}' backend config: {e}", backend.name)))
203 }
204}
205
206fn config_path() -> Result<PathBuf> {
210 if let Ok(p) = std::env::var("DOVE_CONFIG") {
211 if !p.is_empty() {
212 return Ok(PathBuf::from(p));
213 }
214 }
215 if let Ok(x) = std::env::var("XDG_CONFIG_HOME") {
216 if !x.is_empty() {
217 return Ok(PathBuf::from(x).join("dove/config.toml"));
218 }
219 }
220 let home = std::env::var("HOME").map_err(|_| Error::Config("HOME is not set".into()))?;
221 Ok(PathBuf::from(home).join(".config/dove/config.toml"))
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn self_hosted_round_trips_through_backend() {
230 let cfg = SelfHostedConfig {
231 bucket: "dove-shares-example".into(),
232 region: "us-east-1".into(),
233 profile: Some("work".into()),
234 endpoint: None,
235 table: Some("dove-shares-example".into()),
236 gate_url: Some("https://share.example.com".into()),
237 distribution_id: Some("E123ABC".into()),
238 };
239 let backend = Backend::self_hosted("default", &cfg).unwrap();
240 assert_eq!(backend.name, "default");
241 assert_eq!(backend.kind, "self-hosted");
242 assert!(cfg.is_full());
243
244 let reg = Registry {
245 active: "default".into(),
246 backends: vec![backend],
247 };
248 let recovered = reg.active_self_hosted().unwrap();
249 assert_eq!(recovered, cfg);
250 }
251
252 #[test]
253 fn active_backend_missing_is_config_error() {
254 let reg = Registry {
255 active: String::new(),
256 backends: vec![],
257 };
258 let err = reg.active_backend().unwrap_err();
259 assert!(matches!(err, Error::Config(_)));
260 }
261
262 #[test]
266 fn optional_fields_default_to_none() {
267 let cfg: SelfHostedConfig =
268 toml::from_str("bucket = \"b\"\nregion = \"us-east-1\"\n").unwrap();
269 assert_eq!(cfg.profile, None);
270 assert_eq!(cfg.endpoint, None);
271 }
272
273 #[test]
274 fn missing_required_field_is_an_error() {
275 let result: std::result::Result<SelfHostedConfig, _> =
276 toml::from_str("region = \"us-east-1\"\n"); assert!(result.is_err());
278 }
279}