blob: 1315f4bc139148409ab4c68c4adbedbfbce94f72 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use std::borrow::Borrow;
use std::collections::HashMap;
use std::fmt::Display;
use std::ops::Deref;
use anyhow::{bail, Error};
use glob::glob;
use lazy_static::lazy_static;
use regex::Regex;
use serde_derive::Deserialize;
/// The outer map is service_name; the inner is tag.
pub type Config = HashMap<ServiceName, HashMap<Tag, TagConfig>>;
/// Schema for config-file entries. Each config file is a JSON array of these.
#[derive(Deserialize, Default, Debug, PartialEq)]
#[cfg_attr(test, derive(Clone))]
#[serde(deny_unknown_fields)]
struct TaggedPersist {
/// The Inspect data defined here will be published under this tag.
/// Tags must not be duplicated within a service, even between files.
/// Tags must conform to /[a-z][a-z-]*/.
pub tag: String,
/// Each tag will only be requestable via a named service. Multiple tags can use the
/// same service name, which will be published and routed as DataPersistence_{service_name}.
/// Service names must conform to /[a-z][a-z-]*/.
pub service_name: String,
/// These selectors will be fetched and stored for publication on the next boot.
pub selectors: Vec<String>,
/// This is the max size of the file saved, which is the JSON-serialized version
/// of the selectors' data.
pub max_bytes: usize,
/// Persistence requests will be throttled to this. Requests received early will be delayed.
pub min_seconds_between_fetch: i64,
}
/// Configuration for a single tag for a single service.
///
/// See [`TaggedPersist`] for the meaning of corresponding fields.
#[derive(Debug, Eq, PartialEq)]
pub struct TagConfig {
pub selectors: Vec<String>,
pub max_bytes: usize,
pub min_seconds_between_fetch: i64,
}
/// Wrapper class for a valid tag name.
///
/// This is a witness class that can only be constructed from a `String` that
/// matches [`NAME_PATTERN`].
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Tag(String);
/// Wrapper class for a valid service name.
///
/// This is a witness class that can only be constructed from a `String` that
/// matches [`NAME_PATTERN`].
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct ServiceName(String);
/// A regular expression corresponding to a valid tag or service name.
const NAME_PATTERN: &'static str = r"^[a-z][a-z-]*$";
lazy_static! {
static ref NAME_VALIDATOR: Regex = 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))
}
}
/// Allow `Tag` to be treated like a `&str` for display, etc.
impl Deref for Tag {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
/// Allow `ServiceName` to be treated like a `&str` for display, etc.
impl Deref for ServiceName {
type Target = str;
fn deref(&self) -> &Self::Target {
let Self(tag) = self;
tag
}
}
/// Allow treating `Tag` as a `&str` for, e.g., HashMap indexing operations.
impl Borrow<str> for Tag {
fn borrow(&self) -> &str {
&*self
}
}
/// Allow treating `ServiceName` as a `&str` for, e.g., HashMap indexing
/// operations.
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_insert_with(|| HashMap::new())
.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']}]";
// Numbers not allowed in names
let bad_tag = "[{tag: 'tag-b1', service_name: 'serv-b', max_bytes: 30, \
min_seconds_between_fetch: 33, selectors: ['quux']}]";
// Underscores not allowed in names
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());
// Can't duplicate tags in the same service
assert!(try_insert_items(&mut config, tagb_servb).is_err());
}
#[test]
fn test_tag_equals_str() {
assert_eq!(&Tag::new("foo").unwrap(), "foo");
}
}