use crate::canonical::{Canonical, CanonicalWriter};
use crate::serde_support::validating_string_serde;
pub const MAX_LABEL_LEN: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum DimensionError {
#[error("dimension value is empty")]
Empty,
#[error("dimension value exceeds {MAX_LABEL_LEN} characters")]
TooLong,
#[error("dimension value contains a control character")]
ControlCharacter,
}
#[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 Canonical for Label {
fn encode(&self, w: &mut CanonicalWriter) {
w.str(&self.0);
}
}
macro_rules! dimension {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct $name(Label);
validating_string_serde!($name);
impl $name {
pub fn new(s: impl Into<String>) -> Result<Self, DimensionError> {
Label::new(s).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
impl Canonical for $name {
fn encode(&self, w: &mut CanonicalWriter) {
self.0.encode(w);
}
}
};
}
dimension! {
ActivityId
}
dimension! {
SegmentId
}
dimension! {
CostObjectId
}
dimension! {
PartyId
}
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Dimensions {
pub activity: Option<ActivityId>,
pub segment: Option<SegmentId>,
pub cost_object: Option<CostObjectId>,
pub party: Option<PartyId>,
}
impl Dimensions {
#[must_use]
pub fn none() -> Self {
Self::default()
}
#[must_use]
pub fn with_activity(mut self, v: ActivityId) -> Self {
self.activity = Some(v);
self
}
#[must_use]
pub fn with_segment(mut self, v: SegmentId) -> Self {
self.segment = Some(v);
self
}
#[must_use]
pub fn with_cost_object(mut self, v: CostObjectId) -> Self {
self.cost_object = Some(v);
self
}
#[must_use]
pub fn with_party(mut self, v: PartyId) -> Self {
self.party = Some(v);
self
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.activity.is_none()
&& self.segment.is_none()
&& self.cost_object.is_none()
&& self.party.is_none()
}
}
impl Canonical for Dimensions {
fn encode(&self, w: &mut CanonicalWriter) {
w.option(self.activity.as_ref(), |w, v| v.encode(w));
w.option(self.segment.as_ref(), |w, v| v.encode(w));
w.option(self.cost_object.as_ref(), |w, v| v.encode(w));
w.option(self.party.as_ref(), |w, v| v.encode(w));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validates_values() {
assert!(ActivityId::new("Network").is_ok());
assert_eq!(ActivityId::new(""), Err(DimensionError::Empty));
assert_eq!(
ActivityId::new("x".repeat(MAX_LABEL_LEN + 1)),
Err(DimensionError::TooLong)
);
assert_eq!(
ActivityId::new("bad\nvalue"),
Err(DimensionError::ControlCharacter)
);
}
#[test]
fn accepts_maximum_length() {
assert!(ActivityId::new("x".repeat(MAX_LABEL_LEN)).is_ok());
}
#[test]
fn empty_dimensions_report_empty() {
assert!(Dimensions::none().is_empty());
let d = Dimensions::none().with_activity(ActivityId::new("N").expect("valid"));
assert!(!d.is_empty());
}
#[test]
fn encoding_distinguishes_which_field_is_set() {
let by_activity = Dimensions::none().with_activity(ActivityId::new("X").expect("valid"));
let by_segment = Dimensions::none().with_segment(SegmentId::new("X").expect("valid"));
assert_ne!(
by_activity.to_canonical_bytes(),
by_segment.to_canonical_bytes()
);
}
#[test]
fn encoding_is_order_independent_and_stable() {
let a = Dimensions::none()
.with_activity(ActivityId::new("N").expect("valid"))
.with_segment(SegmentId::new("E").expect("valid"));
let b = Dimensions::none()
.with_segment(SegmentId::new("E").expect("valid"))
.with_activity(ActivityId::new("N").expect("valid"));
assert_eq!(a.to_canonical_bytes(), b.to_canonical_bytes());
}
#[test]
fn different_dimension_types_do_not_collide() {
let a = ActivityId::new("X").expect("valid");
let s = SegmentId::new("X").expect("valid");
assert_eq!(a.to_canonical_bytes(), s.to_canonical_bytes());
let da = Dimensions::none().with_activity(a);
let ds = Dimensions::none().with_segment(s);
assert_ne!(da.to_canonical_bytes(), ds.to_canonical_bytes());
}
}