use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::fmt::{Debug, Display, Formatter};
pub use static_assertions::{const_assert, const_assert_ne};
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Tag {
value: Cow<'static, str>,
}
impl Tag {
pub const unsafe fn from_static_unchecked(value: &'static str) -> Self {
Self {
value: Cow::Borrowed(value),
}
}
}
#[macro_export]
macro_rules! tag {
($key:expr, $val:expr) => {{
$crate::tag::const_assert!(!$val.is_empty());
const COMBINED: &'static str = $crate::const_format::concatcp!($key, ":", $val);
$crate::tag::const_assert!(COMBINED.as_bytes()[0].is_ascii_alphabetic());
$crate::tag::const_assert!(COMBINED.as_bytes().len() <= 200);
#[allow(unused_unsafe)]
let tag = unsafe { $crate::tag::Tag::from_static_unchecked(COMBINED) };
tag
}};
}
impl Debug for Tag {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Tag").field("value", &self.value).finish()
}
}
impl AsRef<str> for Tag {
fn as_ref(&self) -> &str {
self.value.as_ref()
}
}
impl Display for Tag {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.value)
}
}
impl Tag {
fn from_value<'a, IntoCow>(chunk: IntoCow) -> anyhow::Result<Self>
where
IntoCow: Into<Cow<'a, str>>,
{
let chunk = chunk.into();
anyhow::ensure!(!chunk.is_empty(), "tag is empty");
let mut chars = chunk.chars();
anyhow::ensure!(
chars.next() != Some(':'),
"tag '{chunk}' begins with a colon"
);
anyhow::ensure!(chars.last() != Some(':'), "tag '{chunk}' ends with a colon");
let value = Cow::Owned(chunk.into_owned());
Ok(Tag { value })
}
pub fn new<K, V>(key: K, value: V) -> anyhow::Result<Self>
where
K: AsRef<str>,
V: AsRef<str>,
{
let key = key.as_ref();
let value = value.as_ref();
Tag::from_value(format!("{key}:{value}"))
}
}
pub fn parse_tags(str: &str) -> (Vec<Tag>, Option<String>) {
let chunks = str
.split(&[',', ' '][..])
.filter(|str| !str.is_empty())
.map(Tag::from_value);
let mut tags = vec![];
let mut error_message = String::new();
for result in chunks {
match result {
Ok(tag) => tags.push(tag),
Err(err) => {
if error_message.is_empty() {
error_message += "Errors while parsing tags: ";
} else {
error_message += ", ";
}
error_message += &err.to_string();
}
}
}
let error_message = if error_message.is_empty() {
None
} else {
Some(error_message)
};
(tags, error_message)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_send() {
fn is_send<T: Send>(_t: T) -> bool {
true
}
assert!(is_send(tag!("src_library", "libdatadog")));
}
#[test]
fn test_empty_key() {
let _ = Tag::new("", "woof").expect_err("empty key is not allowed");
}
#[test]
fn test_empty_value() {
let _ = Tag::new("key1", "").expect_err("empty value is an error");
}
#[test]
fn test_bad_utf8() {
let bytes = &[b'a', 0b1111_0111];
let key = String::from_utf8_lossy(bytes);
let t = Tag::new(key, "value").unwrap();
assert_eq!("a\u{FFFD}:value", t.to_string());
}
#[test]
fn test_value_has_colon() {
let result = Tag::new("env", "staging:east").expect("values can have colons");
assert_eq!("env:staging:east", result.to_string());
let result = tag!("env", "staging:east");
assert_eq!("env:staging:east", result.to_string());
}
#[test]
fn test_suspicious_tags() {
let cases = [
("_begins_with_non-letter".to_string(), "value"),
("the-tag-length-is-over-200-characters".repeat(6), "value"),
];
for case in cases {
let result = Tag::new(case.0, case.1);
assert!(result.is_ok())
}
}
#[test]
fn test_missing_colon_parsing() {
let tag = Tag::from_value("tag").unwrap();
assert_eq!("tag", tag.to_string());
}
#[test]
fn test_leading_colon_parsing() {
let _ = Tag::from_value(":tag").expect_err("Cannot start with a colon");
}
#[test]
fn test_tailing_colon_parsing() {
let _ = Tag::from_value("tag:").expect_err("Cannot end with a colon");
}
#[test]
fn test_tags_parsing() {
let cases = [
("", vec![]),
(",", vec![]),
(" , ", vec![]),
(
"env:staging:east,location:nyc:ny",
vec![
Tag::new("env", "staging:east").unwrap(),
Tag::new("location", "nyc:ny").unwrap(),
],
),
("value", vec![Tag::from_value("value").unwrap()]),
(
"state:utah,state:idaho",
vec![
Tag::new("state", "utah").unwrap(),
Tag::new("state", "idaho").unwrap(),
],
),
(
"key1:value1 key2:value2 key3:value3",
vec![
Tag::new("key1", "value1").unwrap(),
Tag::new("key2", "value2").unwrap(),
Tag::new("key3", "value3").unwrap(),
],
),
(
"key1:value1, key2:value2 ,key3:value3 , key4:value4",
vec![
Tag::new("key1", "value1").unwrap(),
Tag::new("key2", "value2").unwrap(),
Tag::new("key3", "value3").unwrap(),
Tag::new("key4", "value4").unwrap(),
],
),
];
for case in cases {
let expected = case.1;
let (actual, error_message) = parse_tags(case.0);
assert_eq!(expected, actual);
assert!(error_message.is_none());
}
}
}