1use anyhow::{bail, Error};
5use glob::glob;
6use regex::Regex;
7use serde_derive::Deserialize;
8use std::borrow::Borrow;
9use std::collections::HashMap;
10use std::fmt::Display;
11use std::ops::Deref;
12use std::sync::LazyLock;
13
14pub type Config = HashMap<ServiceName, HashMap<Tag, TagConfig>>;
16
17#[derive(Deserialize, Default, Debug, PartialEq)]
19#[cfg_attr(test, derive(Clone))]
20#[serde(deny_unknown_fields)]
21struct TaggedPersist {
22 pub tag: String,
26 pub service_name: String,
30 pub selectors: Vec<String>,
32 pub max_bytes: usize,
35 pub min_seconds_between_fetch: i64,
37 #[serde(default)]
39 pub persist_across_boot: bool,
40}
41
42#[derive(Debug, Eq, PartialEq)]
46pub struct TagConfig {
47 pub selectors: Vec<String>,
48 pub max_bytes: usize,
49 pub min_seconds_between_fetch: i64,
50 pub persist_across_boot: bool,
51}
52
53#[derive(Clone, Debug, Eq, Hash, PartialEq)]
58pub struct Tag(String);
59
60#[derive(Clone, Debug, Eq, Hash, PartialEq)]
65pub struct ServiceName(String);
66
67const NAME_PATTERN: &str = r"^[a-z][a-z-]*$";
69
70static NAME_VALIDATOR: LazyLock<Regex> = LazyLock::new(|| Regex::new(NAME_PATTERN).unwrap());
71
72impl Tag {
73 pub fn new(tag: impl Into<String>) -> Result<Self, Error> {
74 let tag = tag.into();
75 if !NAME_VALIDATOR.is_match(&tag) {
76 bail!("Invalid tag {} must match [a-z][a-z-]*", tag);
77 }
78 Ok(Self(tag))
79 }
80
81 pub fn as_str(&self) -> &str {
82 self.0.as_ref()
83 }
84}
85
86impl ServiceName {
87 pub fn new(name: String) -> Result<Self, Error> {
88 if !NAME_VALIDATOR.is_match(&name) {
89 bail!("Invalid service name {} must match [a-z][a-z-]*", name);
90 }
91 Ok(Self(name))
92 }
93}
94
95impl Deref for Tag {
97 type Target = str;
98
99 fn deref(&self) -> &Self::Target {
100 self.as_str()
101 }
102}
103
104impl Deref for ServiceName {
106 type Target = str;
107
108 fn deref(&self) -> &Self::Target {
109 let Self(tag) = self;
110 tag
111 }
112}
113
114impl Borrow<str> for Tag {
116 fn borrow(&self) -> &str {
117 self
118 }
119}
120
121impl Borrow<str> for ServiceName {
124 fn borrow(&self) -> &str {
125 self
126 }
127}
128
129impl Display for Tag {
130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 let Self(name) = self;
132 name.fmt(f)
133 }
134}
135
136impl PartialEq<str> for Tag {
137 fn eq(&self, other: &str) -> bool {
138 self.0 == other
139 }
140}
141
142impl Display for ServiceName {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 let Self(name) = self;
145 name.fmt(f)
146 }
147}
148
149impl From<ServiceName> for String {
150 fn from(ServiceName(value): ServiceName) -> Self {
151 value
152 }
153}
154
155const CONFIG_GLOB: &str = "/config/data/*.persist";
156
157fn try_insert_items(config: &mut Config, config_text: &str) -> Result<(), Error> {
158 let items: Vec<TaggedPersist> = serde_json5::from_str(config_text)?;
159 for item in items {
160 let TaggedPersist {
161 tag,
162 service_name,
163 selectors,
164 max_bytes,
165 min_seconds_between_fetch,
166 persist_across_boot,
167 } = item;
168 let tag = Tag::new(tag)?;
169 let name = ServiceName::new(service_name)?;
170 if let Some(existing) = config.entry(name.clone()).or_default().insert(
171 tag,
172 TagConfig { selectors, max_bytes, min_seconds_between_fetch, persist_across_boot },
173 ) {
174 bail!("Duplicate TagConfig found: {:?}", existing);
175 }
176 }
177 Ok(())
178}
179
180pub fn load_configuration_files() -> Result<Config, Error> {
181 load_configuration_files_from(CONFIG_GLOB)
182}
183
184pub fn load_configuration_files_from(path: &str) -> Result<Config, Error> {
185 let mut config = HashMap::new();
186 for file_path in glob(path)? {
187 try_insert_items(&mut config, &std::fs::read_to_string(file_path?)?)?;
188 }
189 Ok(config)
190}
191
192#[cfg(test)]
193mod test {
194 use super::*;
195
196 impl From<TaggedPersist> for TagConfig {
197 fn from(
198 TaggedPersist {
199 tag: _,
200 service_name: _,
201 selectors,
202 max_bytes,
203 min_seconds_between_fetch,
204 persist_across_boot,
205 }: TaggedPersist,
206 ) -> Self {
207 Self { selectors, max_bytes, min_seconds_between_fetch, persist_across_boot }
208 }
209 }
210
211 #[fuchsia::test]
212 fn verify_insert_logic() {
213 let mut config = HashMap::new();
214 let taga_servab = "[{tag: 'tag-a', service_name: 'serv-a', max_bytes: 10, \
215 min_seconds_between_fetch: 31, selectors: ['foo', 'bar']}, \
216 {tag: 'tag-a', service_name: 'serv-b', max_bytes: 20, \
217 min_seconds_between_fetch: 32, selectors: ['baz'], \
218 persist_across_boot: true }]";
219 let tagb_servb = "[{tag: 'tag-b', service_name: 'serv-b', max_bytes: 30, \
220 min_seconds_between_fetch: 33, selectors: ['quux']}]";
221 let bad_tag = "[{tag: 'tag-b1', service_name: 'serv-b', max_bytes: 30, \
223 min_seconds_between_fetch: 33, selectors: ['quux']}]";
224 let bad_serv = "[{tag: 'tag-b', service_name: 'serv_b', max_bytes: 30, \
226 min_seconds_between_fetch: 33, selectors: ['quux']}]";
227 let persist_aa = TaggedPersist {
228 tag: "tag-a".to_string(),
229 service_name: "serv-a".to_string(),
230 max_bytes: 10,
231 min_seconds_between_fetch: 31,
232 selectors: vec!["foo".to_string(), "bar".to_string()],
233 persist_across_boot: false,
234 };
235 let persist_ba = TaggedPersist {
236 tag: "tag-a".to_string(),
237 service_name: "serv-b".to_string(),
238 max_bytes: 20,
239 min_seconds_between_fetch: 32,
240 selectors: vec!["baz".to_string()],
241 persist_across_boot: true,
242 };
243 let persist_bb = TaggedPersist {
244 tag: "tag-b".to_string(),
245 service_name: "serv-b".to_string(),
246 max_bytes: 30,
247 min_seconds_between_fetch: 33,
248 selectors: vec!["quux".to_string()],
249 persist_across_boot: false,
250 };
251
252 try_insert_items(&mut config, taga_servab).unwrap();
253 try_insert_items(&mut config, tagb_servb).unwrap();
254 assert_eq!(config.len(), 2);
255 let service_a = config.get("serv-a").unwrap();
256 assert_eq!(service_a.len(), 1);
257 assert_eq!(service_a.get("tag-a"), Some(&persist_aa.clone().into()));
258 let service_b = config.get("serv-b").unwrap();
259 assert_eq!(service_b.len(), 2);
260 assert_eq!(service_b.get("tag-a"), Some(&persist_ba.clone().into()));
261 assert_eq!(service_b.get("tag-b"), Some(&persist_bb.clone().into()));
262
263 assert!(try_insert_items(&mut config, bad_tag).is_err());
264 assert!(try_insert_items(&mut config, bad_serv).is_err());
265 assert!(try_insert_items(&mut config, tagb_servb).is_err());
267 }
268
269 #[test]
270 fn test_tag_equals_str() {
271 assert_eq!(&Tag::new("foo").unwrap(), "foo");
272 }
273}