1#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum StoreConfig {
12 Fs { root: String },
14 S3 {
18 bucket: String,
19 endpoint: Option<String>,
20 region: Option<String>,
21 },
22}
23
24impl StoreConfig {
25 pub fn from_env(get: impl Fn(&str) -> Option<String>) -> Result<StoreConfig, String> {
37 match get("GONZALO_STORE").as_deref() {
38 None | Some("") | Some("fs") => Ok(StoreConfig::Fs {
39 root: get("GONZALO_ROOT").unwrap_or_else(|| "./gonzalo-data".into()),
40 }),
41 Some("s3") => {
42 let bucket = get("GONZALO_S3_BUCKET")
43 .filter(|b| !b.is_empty())
44 .ok_or("GONZALO_STORE=s3 requires GONZALO_S3_BUCKET")?;
45 Ok(StoreConfig::S3 {
46 bucket,
47 endpoint: get("GONZALO_S3_ENDPOINT").filter(|s| !s.is_empty()),
48 region: get("GONZALO_S3_REGION").filter(|s| !s.is_empty()),
49 })
50 }
51 Some(other) => Err(format!(
52 "unknown GONZALO_STORE={other:?} (expected \"fs\" or \"s3\")"
53 )),
54 }
55 }
56}
57
58pub fn ancestor_cap_from_env(get: impl Fn(&str) -> Option<String>) -> Result<usize, String> {
66 let Some(raw) = get("GONZALO_ANCESTOR_CAP").filter(|s| !s.is_empty()) else {
67 return Ok(gonzalo_core::DEFAULT_ANCESTOR_CAP);
68 };
69 let cap: usize = raw
70 .parse()
71 .map_err(|e| format!("GONZALO_ANCESTOR_CAP must be a positive integer: {e}"))?;
72 gonzalo_core::validate_ancestor_cap(cap).map_err(|e| format!("GONZALO_ANCESTOR_CAP: {e}"))
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use std::collections::HashMap;
79
80 fn env(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> {
82 let map: HashMap<String, String> = pairs
83 .iter()
84 .map(|(k, v)| (k.to_string(), v.to_string()))
85 .collect();
86 move |k: &str| map.get(k).cloned()
87 }
88
89 #[test]
90 fn default_is_fs_with_default_root() {
91 let cfg = StoreConfig::from_env(env(&[])).unwrap();
92 assert_eq!(
93 cfg,
94 StoreConfig::Fs {
95 root: "./gonzalo-data".into()
96 }
97 );
98 }
99
100 #[test]
101 fn fs_honors_root() {
102 let cfg = StoreConfig::from_env(env(&[("GONZALO_STORE", "fs"), ("GONZALO_ROOT", "/data")]))
103 .unwrap();
104 assert_eq!(
105 cfg,
106 StoreConfig::Fs {
107 root: "/data".into()
108 }
109 );
110 }
111
112 #[test]
113 fn s3_requires_bucket() {
114 let err = StoreConfig::from_env(env(&[("GONZALO_STORE", "s3")])).unwrap_err();
115 assert!(err.contains("GONZALO_S3_BUCKET"), "got {err}");
116 }
117
118 #[test]
119 fn s3_reads_bucket_endpoint_region() {
120 let cfg = StoreConfig::from_env(env(&[
121 ("GONZALO_STORE", "s3"),
122 ("GONZALO_S3_BUCKET", "gonzalo"),
123 ("GONZALO_S3_ENDPOINT", "http://garage:3900"),
124 ("GONZALO_S3_REGION", "garage"),
125 ]))
126 .unwrap();
127 assert_eq!(
128 cfg,
129 StoreConfig::S3 {
130 bucket: "gonzalo".into(),
131 endpoint: Some("http://garage:3900".into()),
132 region: Some("garage".into()),
133 }
134 );
135 }
136
137 #[test]
138 fn s3_endpoint_and_region_optional() {
139 let cfg =
140 StoreConfig::from_env(env(&[("GONZALO_STORE", "s3"), ("GONZALO_S3_BUCKET", "b")]))
141 .unwrap();
142 assert_eq!(
143 cfg,
144 StoreConfig::S3 {
145 bucket: "b".into(),
146 endpoint: None,
147 region: None,
148 }
149 );
150 }
151
152 #[test]
153 fn unknown_store_is_an_error() {
154 let err = StoreConfig::from_env(env(&[("GONZALO_STORE", "cassandra")])).unwrap_err();
155 assert!(err.contains("cassandra"), "got {err}");
156 }
157
158 #[test]
159 fn ancestor_cap_defaults_when_unset_or_empty() {
160 assert_eq!(
161 ancestor_cap_from_env(env(&[])).unwrap(),
162 gonzalo_core::DEFAULT_ANCESTOR_CAP
163 );
164 assert_eq!(
166 ancestor_cap_from_env(env(&[("GONZALO_ANCESTOR_CAP", "")])).unwrap(),
167 gonzalo_core::DEFAULT_ANCESTOR_CAP
168 );
169 }
170
171 #[test]
172 fn ancestor_cap_reads_the_variable() {
173 assert_eq!(
174 ancestor_cap_from_env(env(&[("GONZALO_ANCESTOR_CAP", "8")])).unwrap(),
175 8
176 );
177 }
178
179 #[test]
180 fn ancestor_cap_rejects_zero_and_garbage() {
181 let zero = ancestor_cap_from_env(env(&[("GONZALO_ANCESTOR_CAP", "0")])).unwrap_err();
182 assert!(zero.contains("GONZALO_ANCESTOR_CAP"), "{zero}");
183 let garbage = ancestor_cap_from_env(env(&[("GONZALO_ANCESTOR_CAP", "lots")])).unwrap_err();
184 assert!(garbage.contains("GONZALO_ANCESTOR_CAP"), "{garbage}");
185 assert!(ancestor_cap_from_env(env(&[("GONZALO_ANCESTOR_CAP", "-1")])).is_err());
186 }
187}