use std::collections::BTreeMap;
use crate::canonical::{Canonical, CanonicalWriter};
use crate::serde_support::validating_string_serde;
pub const MAX_LABEL_LEN: usize = 64;
pub const MAX_DIMENSIONS: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum DimensionError {
#[error("label is empty")]
Empty,
#[error("label exceeds {MAX_LABEL_LEN} characters")]
TooLong,
#[error("label contains a control character")]
ControlCharacter,
#[error("a posting may carry at most {MAX_DIMENSIONS} dimensions")]
TooManyDimensions,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Label(String);
validating_string_serde!(Label);
impl Label {
pub fn new(s: impl Into<String>) -> Result<Self, DimensionError> {
let s = s.into();
if s.is_empty() {
return Err(DimensionError::Empty);
}
if s.chars().count() > MAX_LABEL_LEN {
return Err(DimensionError::TooLong);
}
if s.chars().any(char::is_control) {
return Err(DimensionError::ControlCharacter);
}
Ok(Self(s))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for Label {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::str::FromStr for Label {
type Err = DimensionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl std::borrow::Borrow<str> for Label {
fn borrow(&self) -> &str {
&self.0
}
}
impl Canonical for Label {
fn encode(&self, w: &mut CanonicalWriter) {
w.str(&self.0);
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Dimensions {
axes: BTreeMap<Label, Label>,
}
impl Dimensions {
#[must_use]
pub fn none() -> Self {
Self::default()
}
pub fn with(mut self, axis: Label, value: Label) -> Result<Self, DimensionError> {
self.set(axis, value)?;
Ok(self)
}
pub fn set(&mut self, axis: Label, value: Label) -> Result<Option<Label>, DimensionError> {
if !self.axes.contains_key(&axis) && self.axes.len() >= MAX_DIMENSIONS {
return Err(DimensionError::TooManyDimensions);
}
Ok(self.axes.insert(axis, value))
}
pub fn from_pairs(
pairs: impl IntoIterator<Item = (Label, Label)>,
) -> Result<Self, DimensionError> {
let mut out = Self::none();
for (axis, value) in pairs {
out.set(axis, value)?;
}
Ok(out)
}
#[must_use]
pub fn get(&self, axis: &str) -> Option<&Label> {
self.axes.get(axis)
}
#[must_use]
pub fn contains(&self, axis: &str) -> bool {
self.get(axis).is_some()
}
pub fn iter(&self) -> impl ExactSizeIterator<Item = (&Label, &Label)> {
self.axes.iter()
}
#[must_use]
pub fn len(&self) -> usize {
self.axes.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.axes.is_empty()
}
}
impl<'a> IntoIterator for &'a Dimensions {
type Item = (&'a Label, &'a Label);
type IntoIter = std::collections::btree_map::Iter<'a, Label, Label>;
fn into_iter(self) -> Self::IntoIter {
self.axes.iter()
}
}
impl Canonical for Dimensions {
fn encode(&self, w: &mut CanonicalWriter) {
w.seq(self.axes.iter(), |w, (axis, value)| {
axis.encode(w);
value.encode(w);
});
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Dimensions {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
serde::Serialize::serialize(&self.axes, s)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Dimensions {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let axes = <BTreeMap<Label, Label> as serde::Deserialize>::deserialize(d)?;
Self::from_pairs(axes).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DimensionFilter {
clauses: BTreeMap<Label, Option<Label>>,
}
impl DimensionFilter {
#[must_use]
pub fn any() -> Self {
Self::default()
}
#[must_use]
pub fn matching(mut self, axis: Label, value: Label) -> Self {
self.clauses.insert(axis, Some(value));
self
}
#[must_use]
pub fn missing(mut self, axis: Label) -> Self {
self.clauses.insert(axis, None);
self
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.clauses.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.clauses.len()
}
pub fn clauses(&self) -> impl ExactSizeIterator<Item = (&Label, Option<&Label>)> {
self.clauses.iter().map(|(a, v)| (a, v.as_ref()))
}
#[must_use]
pub fn matches(&self, dimensions: &Dimensions) -> bool {
self.clauses.iter().all(|(axis, expected)| {
match (dimensions.get(axis.as_str()), expected.as_ref()) {
(Some(actual), Some(want)) => actual == want,
(None, None) => true,
_ => false,
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn label(s: &str) -> Label {
Label::new(s).expect("valid label")
}
#[test]
fn validates_labels() {
assert!(Label::new("Network").is_ok());
assert_eq!(Label::new(""), Err(DimensionError::Empty));
assert_eq!(
Label::new("x".repeat(MAX_LABEL_LEN + 1)),
Err(DimensionError::TooLong)
);
assert_eq!(
Label::new("bad\nvalue"),
Err(DimensionError::ControlCharacter)
);
assert!(Label::new("x".repeat(MAX_LABEL_LEN)).is_ok());
}
#[test]
fn empty_dimensions_report_empty() {
assert!(Dimensions::none().is_empty());
let d = Dimensions::none()
.with(label("activity"), label("Network"))
.expect("fits");
assert!(!d.is_empty());
assert_eq!(d.len(), 1);
assert!(d.contains("activity"));
assert!(!d.contains("segment"));
}
#[test]
fn setting_an_axis_twice_replaces_it() {
let d = Dimensions::none()
.with(label("activity"), label("Network"))
.expect("fits")
.with(label("activity"), label("Supply"))
.expect("fits");
assert_eq!(d.len(), 1);
assert_eq!(d.get("activity").map(Label::as_str), Some("Supply"));
}
#[test]
fn the_axis_count_is_bounded() {
let mut d = Dimensions::none();
for i in 0..MAX_DIMENSIONS {
d = d
.with(label(&format!("axis{i}")), label("v"))
.expect("fits");
}
assert_eq!(
d.clone().with(label("one-too-many"), label("v")),
Err(DimensionError::TooManyDimensions)
);
assert!(d.with(label("axis0"), label("w")).is_ok());
}
#[test]
fn encoding_is_order_independent_and_stable() {
let a = Dimensions::none()
.with(label("activity"), label("N"))
.expect("fits")
.with(label("segment"), label("E"))
.expect("fits");
let b = Dimensions::none()
.with(label("segment"), label("E"))
.expect("fits")
.with(label("activity"), label("N"))
.expect("fits");
assert_eq!(a.to_canonical_bytes(), b.to_canonical_bytes());
}
#[test]
fn encoding_distinguishes_the_axis_from_the_value() {
let forward = Dimensions::none()
.with(label("a"), label("b"))
.expect("fits");
let backward = Dimensions::none()
.with(label("b"), label("a"))
.expect("fits");
assert_ne!(forward.to_canonical_bytes(), backward.to_canonical_bytes());
}
#[test]
fn encoding_distinguishes_a_split_axis_name() {
let one = Dimensions::none()
.with(label("ab"), label("c"))
.expect("fits");
let other = Dimensions::none()
.with(label("a"), label("bc"))
.expect("fits");
assert_ne!(one.to_canonical_bytes(), other.to_canonical_bytes());
}
#[test]
fn an_absent_axis_is_not_an_empty_value() {
let absent = Dimensions::none();
let present = Dimensions::none()
.with(label("activity"), label("-"))
.expect("fits");
assert_ne!(absent.to_canonical_bytes(), present.to_canonical_bytes());
}
#[cfg(feature = "serde")]
#[test]
fn serde_round_trips_and_re_validates() {
let d = Dimensions::none()
.with(label("activity"), label("Network"))
.expect("fits");
let json = serde_json::to_string(&d).expect("serialises");
assert_eq!(json, r#"{"activity":"Network"}"#);
assert_eq!(
serde_json::from_str::<Dimensions>(&json).expect("deserialises"),
d
);
let overflowing: String = format!(
"{{{}}}",
(0..=MAX_DIMENSIONS)
.map(|i| format!("\"axis{i}\":\"v\""))
.collect::<Vec<_>>()
.join(",")
);
assert!(serde_json::from_str::<Dimensions>(&overflowing).is_err());
assert!(serde_json::from_str::<Dimensions>(r#"{"":"v"}"#).is_err());
}
}