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}
38
39#[derive(Debug, Eq, PartialEq)]
43pub struct TagConfig {
44 pub selectors: Vec<String>,
45 pub max_bytes: usize,
46 pub min_seconds_between_fetch: i64,
47}
48
49#[derive(Clone, Debug, Eq, Hash, PartialEq)]
54pub struct Tag(String);
55
56#[derive(Clone, Debug, Eq, Hash, PartialEq)]
61pub struct ServiceName(String);
62
63const NAME_PATTERN: &str = r"^[a-z][a-z-]*$";
65
66static NAME_VALIDATOR: LazyLock<Regex> = LazyLock::new(|| Regex::new(NAME_PATTERN).unwrap());
67
68impl Tag {
69 pub fn new(tag: impl Into<String>) -> Result<Self, Error> {
70 let tag = tag.into();
71 if !NAME_VALIDATOR.is_match(&tag) {
72 bail!("Invalid tag {} must match [a-z][a-z-]*", tag);
73 }
74 Ok(Self(tag))
75 }
76
77 pub fn as_str(&self) -> &str {
78 self.0.as_ref()
79 }
80}
81
82impl ServiceName {
83 pub fn new(name: String) -> Result<Self, Error> {
84 if !NAME_VALIDATOR.is_match(&name) {
85 bail!("Invalid service name {} must match [a-z][a-z-]*", name);
86 }
87 Ok(Self(name))
88 }
89}
90
91impl Deref for Tag {
93 type Target = str;
94
95 fn deref(&self) -> &Self::Target {
96 self.as_str()
97 }
98}
99
100impl Deref for ServiceName {
102 type Target = str;
103
104 fn deref(&self) -> &Self::Target {
105 let Self(tag) = self;
106 tag
107 }
108}
109
110impl Borrow<str> for Tag {
112 fn borrow(&self) -> &str {
113 self
114 }
115}
116
117impl Borrow<str> for ServiceName {
120 fn borrow(&self) -> &str {
121 self
122 }
123}
124
125impl Display for Tag {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 let Self(name) = self;
128 name.fmt(f)
129 }
130}
131
132impl PartialEq<str> for Tag {
133 fn eq(&self, other: &str) -> bool {
134 self.0 == other
135 }
136}
137
138impl Display for ServiceName {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 let Self(name) = self;
141 name.fmt(f)
142 }
143}
144
145const CONFIG_GLOB: &str = "/config/data/*.persist";
146
147fn try_insert_items(config: &mut Config, config_text: &str) -> Result<(), Error> {
148 let items = serde_json5::from_str::<Vec<TaggedPersist>>(config_text)?;
149 for item in items {
150 let TaggedPersist { tag, service_name, selectors, max_bytes, min_seconds_between_fetch } =
151 item;
152 let tag = Tag::new(tag)?;
153 let name = ServiceName::new(service_name)?;
154 if let Some(existing) = config
155 .entry(name.clone())
156 .or_default()
157 .insert(tag, TagConfig { selectors, max_bytes, min_seconds_between_fetch })
158 {
159 bail!("Duplicate TagConfig found: {:?}", existing);
160 }
161 }
162 Ok(())
163}
164
165pub fn load_configuration_files() -> Result<Config, Error> {
166 load_configuration_files_from(CONFIG_GLOB)
167}
168
169pub fn load_configuration_files_from(path: &str) -> Result<Config, Error> {
170 let mut config = HashMap::new();
171 for file_path in glob(path)? {
172 try_insert_items(&mut config, &std::fs::read_to_string(file_path?)?)?;
173 }
174 Ok(config)
175}
176
177#[cfg(test)]
178mod test {
179 use super::*;
180
181 impl From<TaggedPersist> for TagConfig {
182 fn from(
183 TaggedPersist {
184 tag: _,
185 service_name: _,
186 selectors,
187 max_bytes,
188 min_seconds_between_fetch,
189 }: TaggedPersist,
190 ) -> Self {
191 Self { selectors, max_bytes, min_seconds_between_fetch }
192 }
193 }
194
195 #[fuchsia::test]
196 fn verify_insert_logic() {
197 let mut config = HashMap::new();
198 let taga_servab = "[{tag: 'tag-a', service_name: 'serv-a', max_bytes: 10, \
199 min_seconds_between_fetch: 31, selectors: ['foo', 'bar']}, \
200 {tag: 'tag-a', service_name: 'serv-b', max_bytes: 20, \
201 min_seconds_between_fetch: 32, selectors: ['baz']}, ]";
202 let tagb_servb = "[{tag: 'tag-b', service_name: 'serv-b', max_bytes: 30, \
203 min_seconds_between_fetch: 33, selectors: ['quux']}]";
204 let bad_tag = "[{tag: 'tag-b1', service_name: 'serv-b', max_bytes: 30, \
206 min_seconds_between_fetch: 33, selectors: ['quux']}]";
207 let bad_serv = "[{tag: 'tag-b', service_name: 'serv_b', max_bytes: 30, \
209 min_seconds_between_fetch: 33, selectors: ['quux']}]";
210 let persist_aa = TaggedPersist {
211 tag: "tag-a".to_string(),
212 service_name: "serv-a".to_string(),
213 max_bytes: 10,
214 min_seconds_between_fetch: 31,
215 selectors: vec!["foo".to_string(), "bar".to_string()],
216 };
217 let persist_ba = TaggedPersist {
218 tag: "tag-a".to_string(),
219 service_name: "serv-b".to_string(),
220 max_bytes: 20,
221 min_seconds_between_fetch: 32,
222 selectors: vec!["baz".to_string()],
223 };
224 let persist_bb = TaggedPersist {
225 tag: "tag-b".to_string(),
226 service_name: "serv-b".to_string(),
227 max_bytes: 30,
228 min_seconds_between_fetch: 33,
229 selectors: vec!["quux".to_string()],
230 };
231
232 assert!(try_insert_items(&mut config, taga_servab).is_ok());
233 assert!(try_insert_items(&mut config, tagb_servb).is_ok());
234 assert_eq!(config.len(), 2);
235 let service_a = config.get("serv-a").unwrap();
236 assert_eq!(service_a.len(), 1);
237 assert_eq!(service_a.get("tag-a"), Some(&persist_aa.clone().into()));
238 let service_b = config.get("serv-b").unwrap();
239 assert_eq!(service_b.len(), 2);
240 assert_eq!(service_b.get("tag-a"), Some(&persist_ba.clone().into()));
241 assert_eq!(service_b.get("tag-b"), Some(&persist_bb.clone().into()));
242
243 assert!(try_insert_items(&mut config, bad_tag).is_err());
244 assert!(try_insert_items(&mut config, bad_serv).is_err());
245 assert!(try_insert_items(&mut config, tagb_servb).is_err());
247 }
248
249 #[test]
250 fn test_tag_equals_str() {
251 assert_eq!(&Tag::new("foo").unwrap(), "foo");
252 }
253}