use std::collections::BTreeMap;
use crate::{ParameterName, Value, name_type};
use serde::{Deserialize, Serialize};
use crate::error::ElementError;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TokenExpr(String);
impl TokenExpr {
pub fn new(s: impl Into<String>) -> Result<Self, ElementError> {
let s = s.into();
if s.is_empty() {
return Err(ElementError::EmptyTokenExpr);
}
Ok(Self(s))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_inner(self) -> String {
self.0
}
}
impl std::fmt::Display for TokenExpr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl Serialize for TokenExpr {
fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
s.serialize_str(&self.0)
}
}
impl<'de> Deserialize<'de> for TokenExpr {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
let s = String::deserialize(d)?;
Self::new(s).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ConfigEntry {
Literal {
value: Value,
},
Token {
expr: TokenExpr,
},
}
impl ConfigEntry {
#[must_use]
pub const fn literal(value: Value) -> Self {
Self::Literal { value }
}
#[must_use]
pub const fn token(expr: TokenExpr) -> Self {
Self::Token { expr }
}
#[must_use]
pub const fn is_token(&self) -> bool {
matches!(self, Self::Token { .. })
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Configuration(BTreeMap<ParameterName, ConfigEntry>);
impl Configuration {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(
&mut self,
name: ParameterName,
entry: ConfigEntry,
) -> Option<ConfigEntry> {
self.0.insert(name, entry)
}
#[must_use]
pub fn get(&self, name: &ParameterName) -> Option<&ConfigEntry> {
self.0.get(name)
}
pub fn iter(&self) -> impl Iterator<Item = (&ParameterName, &ConfigEntry)> {
self.0.iter()
}
pub fn keys(&self) -> impl Iterator<Item = &ParameterName> {
self.0.keys()
}
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl FromIterator<(ParameterName, ConfigEntry)> for Configuration {
fn from_iter<I: IntoIterator<Item = (ParameterName, ConfigEntry)>>(iter: I) -> Self {
Self(iter.into_iter().collect())
}
}
name_type! {
pub struct ExportName { kind: "ExportName" }
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Exports(BTreeMap<ExportName, TokenExpr>);
impl Exports {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, name: ExportName, expr: TokenExpr) -> Option<TokenExpr> {
self.0.insert(name, expr)
}
#[must_use]
pub fn get(&self, name: &ExportName) -> Option<&TokenExpr> {
self.0.get(name)
}
pub fn iter(&self) -> impl Iterator<Item = (&ExportName, &TokenExpr)> {
self.0.iter()
}
pub fn keys(&self) -> impl Iterator<Item = &ExportName> {
self.0.keys()
}
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl FromIterator<(ExportName, TokenExpr)> for Exports {
fn from_iter<I: IntoIterator<Item = (ExportName, TokenExpr)>>(iter: I) -> Self {
Self(iter.into_iter().collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ParameterName;
fn pname(s: &str) -> ParameterName {
ParameterName::new(s).unwrap()
}
#[test]
fn token_expr_rejects_empty() {
assert!(TokenExpr::new("").is_err());
let t = TokenExpr::new("${self.ip}").unwrap();
assert_eq!(t.as_str(), "${self.ip}");
}
#[test]
fn config_entry_helpers() {
let lit = ConfigEntry::literal(Value::integer(pname("n"), 8, None));
let tok = ConfigEntry::token(TokenExpr::new("${self.ip}").unwrap());
assert!(!lit.is_token());
assert!(tok.is_token());
}
#[test]
fn configuration_iter_is_sorted_by_name() {
let mut c = Configuration::new();
c.insert(
pname("zebra"),
ConfigEntry::literal(Value::integer(pname("zebra"), 1, None)),
);
c.insert(
pname("apple"),
ConfigEntry::literal(Value::integer(pname("apple"), 2, None)),
);
let names: Vec<&str> = c.keys().map(ParameterName::as_str).collect();
assert_eq!(names, vec!["apple", "zebra"]);
}
#[test]
fn exports_insert_and_get() {
let mut e = Exports::new();
let n = ExportName::new("service_addr").unwrap();
let t = TokenExpr::new("${self.ip}:4567").unwrap();
e.insert(n.clone(), t.clone());
assert_eq!(e.get(&n), Some(&t));
assert_eq!(e.len(), 1);
}
#[test]
fn token_expr_serde_roundtrip() {
let t = TokenExpr::new("${foo.bar}").unwrap();
let json = serde_json::to_string(&t).unwrap();
assert_eq!(json, "\"${foo.bar}\"");
let back: TokenExpr = serde_json::from_str(&json).unwrap();
assert_eq!(t, back);
}
#[test]
fn token_expr_deserialise_rejects_empty() {
let err: Result<TokenExpr, _> = serde_json::from_str("\"\"");
assert!(err.is_err());
}
#[test]
fn config_entry_serde_roundtrip() {
let lit = ConfigEntry::literal(Value::integer(pname("n"), 8, None));
let json = serde_json::to_string(&lit).unwrap();
let back: ConfigEntry = serde_json::from_str(&json).unwrap();
assert_eq!(lit, back);
}
}