persistence_config/
lib.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4use 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
14/// The outer map is service_name; the inner is tag.
15pub type Config = HashMap<ServiceName, HashMap<Tag, TagConfig>>;
16
17/// Schema for config-file entries. Each config file is a JSON array of these.
18#[derive(Deserialize, Default, Debug, PartialEq)]
19#[cfg_attr(test, derive(Clone))]
20#[serde(deny_unknown_fields)]
21struct TaggedPersist {
22    /// The Inspect data defined here will be published under this tag.
23    /// Tags must not be duplicated within a service, even between files.
24    /// Tags must conform to /[a-z][a-z-]*/.
25    pub tag: String,
26    /// Each tag will only be requestable via a named service. Multiple tags can use the
27    /// same service name, which will be published and routed as DataPersistence_{service_name}.
28    /// Service names must conform to /[a-z][a-z-]*/.
29    pub service_name: String,
30    /// These selectors will be fetched and stored for publication on the next boot.
31    pub selectors: Vec<String>,
32    /// This is the max size of the file saved, which is the JSON-serialized version
33    /// of the selectors' data.
34    pub max_bytes: usize,
35    /// Persistence requests will be throttled to this. Requests received early will be delayed.
36    pub min_seconds_between_fetch: i64,
37}
38
39/// Configuration for a single tag for a single service.
40///
41/// See [`TaggedPersist`] for the meaning of corresponding fields.
42#[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/// Wrapper class for a valid tag name.
50///
51/// This is a witness class that can only be constructed from a `String` that
52/// matches [`NAME_PATTERN`].
53#[derive(Clone, Debug, Eq, Hash, PartialEq)]
54pub struct Tag(String);
55
56/// Wrapper class for a valid service name.
57///
58/// This is a witness class that can only be constructed from a `String` that
59/// matches [`NAME_PATTERN`].
60#[derive(Clone, Debug, Eq, Hash, PartialEq)]
61pub struct ServiceName(String);
62
63/// A regular expression corresponding to a valid tag or service name.
64const 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
91/// Allow `Tag` to be treated like a `&str` for display, etc.
92impl Deref for Tag {
93    type Target = str;
94
95    fn deref(&self) -> &Self::Target {
96        self.as_str()
97    }
98}
99
100/// Allow `ServiceName` to be treated like a `&str` for display, etc.
101impl Deref for ServiceName {
102    type Target = str;
103
104    fn deref(&self) -> &Self::Target {
105        let Self(tag) = self;
106        tag
107    }
108}
109
110/// Allow treating `Tag` as a `&str` for, e.g., HashMap indexing operations.
111impl Borrow<str> for Tag {
112    fn borrow(&self) -> &str {
113        self
114    }
115}
116
117/// Allow treating `ServiceName` as a `&str` for, e.g., HashMap indexing
118/// operations.
119impl 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        // Numbers not allowed in names
205        let bad_tag = "[{tag: 'tag-b1', service_name: 'serv-b', max_bytes: 30, \
206                       min_seconds_between_fetch: 33, selectors: ['quux']}]";
207        // Underscores not allowed in names
208        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        // Can't duplicate tags in the same service
246        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}