use anyhow::{bail, Error};
use glob::glob;
use regex::Regex;
use serde_derive::Deserialize;
use std::borrow::Borrow;
use std::collections::HashMap;
use std::fmt::Display;
use std::ops::Deref;
use std::sync::LazyLock;
pub type Config = HashMap<ServiceName, HashMap<Tag, TagConfig>>;
#[derive(Deserialize, Default, Debug, PartialEq)]
#[cfg_attr(test, derive(Clone))]
#[serde(deny_unknown_fields)]
struct TaggedPersist {
pub tag: String,
pub service_name: String,
pub selectors: Vec<String>,
pub max_bytes: usize,
pub min_seconds_between_fetch: i64,
}
#[derive(Debug, Eq, PartialEq)]
pub struct TagConfig {
pub selectors: Vec<String>,
pub max_bytes: usize,
pub min_seconds_between_fetch: i64,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Tag(String);
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct ServiceName(String);
const NAME_PATTERN: &str = r"^[a-z][a-z-]*$";
static NAME_VALIDATOR: LazyLock<Regex> = LazyLock::new(|| Regex::new(NAME_PATTERN).unwrap());
impl Tag {
pub fn new(tag: impl Into<String>) -> Result<Self, Error> {
let tag = tag.into();
if !NAME_VALIDATOR.is_match(&tag) {
bail!("Invalid tag {} must match [a-z][a-z-]*", tag);
}
Ok(Self(tag))
}
pub fn as_str(&self) -> &str {
self.0.as_ref()
}
}
impl ServiceName {
pub fn new(name: String) -> Result<Self, Error> {
if !NAME_VALIDATOR.is_match(&name) {
bail!("Invalid service name {} must match [a-z][a-z-]*", name);
}
Ok(Self(name))
}
}
impl Deref for Tag {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl Deref for ServiceName {
type Target = str;
fn deref(&self) -> &Self::Target {
let Self(tag) = self;
tag
}
}
impl Borrow<str> for Tag {
fn borrow(&self) -> &str {
self
}
}
impl Borrow<str> for ServiceName {
fn borrow(&self) -> &str {
self
}
}
impl Display for Tag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self(name) = self;
name.fmt(f)
}
}
impl PartialEq<str> for Tag {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl Display for ServiceName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self(name) = self;
name.fmt(f)
}
}
const CONFIG_GLOB: &str = "/config/data/*.persist";
fn try_insert_items(config: &mut Config, config_text: &str) -> Result<(), Error> {
let items = serde_json5::from_str::<Vec<TaggedPersist>>(config_text)?;
for item in items {
let TaggedPersist { tag, service_name, selectors, max_bytes, min_seconds_between_fetch } =
item;
let tag = Tag::new(tag)?;
let name = ServiceName::new(service_name)?;
if let Some(existing) = config
.entry(name.clone())
.or_default()
.insert(tag, TagConfig { selectors, max_bytes, min_seconds_between_fetch })
{
bail!("Duplicate TagConfig found: {:?}", existing);
}
}
Ok(())
}
pub fn load_configuration_files() -> Result<Config, Error> {
load_configuration_files_from(CONFIG_GLOB)
}
pub fn load_configuration_files_from(path: &str) -> Result<Config, Error> {
let mut config = HashMap::new();
for file_path in glob(path)? {
try_insert_items(&mut config, &std::fs::read_to_string(file_path?)?)?;
}
Ok(config)
}
#[cfg(test)]
mod test {
use super::*;
impl From<TaggedPersist> for TagConfig {
fn from(
TaggedPersist {
tag: _,
service_name: _,
selectors,
max_bytes,
min_seconds_between_fetch,
}: TaggedPersist,
) -> Self {
Self { selectors, max_bytes, min_seconds_between_fetch }
}
}
#[fuchsia::test]
fn verify_insert_logic() {
let mut config = HashMap::new();
let taga_servab = "[{tag: 'tag-a', service_name: 'serv-a', max_bytes: 10, \
min_seconds_between_fetch: 31, selectors: ['foo', 'bar']}, \
{tag: 'tag-a', service_name: 'serv-b', max_bytes: 20, \
min_seconds_between_fetch: 32, selectors: ['baz']}, ]";
let tagb_servb = "[{tag: 'tag-b', service_name: 'serv-b', max_bytes: 30, \
min_seconds_between_fetch: 33, selectors: ['quux']}]";
let bad_tag = "[{tag: 'tag-b1', service_name: 'serv-b', max_bytes: 30, \
min_seconds_between_fetch: 33, selectors: ['quux']}]";
let bad_serv = "[{tag: 'tag-b', service_name: 'serv_b', max_bytes: 30, \
min_seconds_between_fetch: 33, selectors: ['quux']}]";
let persist_aa = TaggedPersist {
tag: "tag-a".to_string(),
service_name: "serv-a".to_string(),
max_bytes: 10,
min_seconds_between_fetch: 31,
selectors: vec!["foo".to_string(), "bar".to_string()],
};
let persist_ba = TaggedPersist {
tag: "tag-a".to_string(),
service_name: "serv-b".to_string(),
max_bytes: 20,
min_seconds_between_fetch: 32,
selectors: vec!["baz".to_string()],
};
let persist_bb = TaggedPersist {
tag: "tag-b".to_string(),
service_name: "serv-b".to_string(),
max_bytes: 30,
min_seconds_between_fetch: 33,
selectors: vec!["quux".to_string()],
};
assert!(try_insert_items(&mut config, taga_servab).is_ok());
assert!(try_insert_items(&mut config, tagb_servb).is_ok());
assert_eq!(config.len(), 2);
let service_a = config.get("serv-a").unwrap();
assert_eq!(service_a.len(), 1);
assert_eq!(service_a.get("tag-a"), Some(&persist_aa.clone().into()));
let service_b = config.get("serv-b").unwrap();
assert_eq!(service_b.len(), 2);
assert_eq!(service_b.get("tag-a"), Some(&persist_ba.clone().into()));
assert_eq!(service_b.get("tag-b"), Some(&persist_bb.clone().into()));
assert!(try_insert_items(&mut config, bad_tag).is_err());
assert!(try_insert_items(&mut config, bad_serv).is_err());
assert!(try_insert_items(&mut config, tagb_servb).is_err());
}
#[test]
fn test_tag_equals_str() {
assert_eq!(&Tag::new("foo").unwrap(), "foo");
}
}