1use alloc::borrow::Cow;
15use core::fmt::{Debug, Display, Formatter};
16use serde::{Deserialize, Serialize};
17
18pub use static_assertions::{const_assert, const_assert_ne};
19
20#[allow(missing_docs, reason = "variant names are self-documenting")]
22#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
23#[non_exhaustive] pub enum TagValidationError {
25 #[error("tag is empty")]
26 Empty,
27 #[error("tag begins with a colon")]
28 BeginsWithColon,
29 #[error("tag ends with a colon")]
30 EndsWithColon,
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35pub struct InvalidTag<'a> {
36 pub value: &'a str,
38 pub error: TagValidationError,
40}
41
42impl Display for InvalidTag<'_> {
43 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
44 match self.error {
45 TagValidationError::Empty => f.write_str("tag is empty"),
46 TagValidationError::BeginsWithColon => {
47 write!(f, "tag '{}' begins with a colon", self.value)
48 }
49 TagValidationError::EndsWithColon => {
50 write!(f, "tag '{}' ends with a colon", self.value)
51 }
52 }
53 }
54}
55
56impl core::error::Error for InvalidTag<'_> {}
57
58#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
59#[serde(transparent)]
60pub struct Tag {
61 value: Cow<'static, str>,
68}
69
70impl Tag {
71 pub const unsafe fn from_static_unchecked(value: &'static str) -> Self {
77 Self {
78 value: Cow::Borrowed(value),
79 }
80 }
81}
82
83#[macro_export]
91macro_rules! tag {
92 ($key:expr, $val:expr) => {{
93 $crate::tag::const_assert!(!$val.is_empty());
98
99 const COMBINED: &'static str = $crate::const_format::concatcp!($key, ":", $val);
100
101 $crate::tag::const_assert!(COMBINED.as_bytes()[0].is_ascii_alphabetic());
105
106 $crate::tag::const_assert!(COMBINED.as_bytes().len() <= 200);
114
115 #[allow(unused_unsafe)]
116 let tag = unsafe { $crate::tag::Tag::from_static_unchecked(COMBINED) };
117 tag
118 }};
119}
120
121impl Debug for Tag {
122 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
123 f.debug_struct("Tag").field("value", &self.value).finish()
124 }
125}
126
127impl AsRef<str> for Tag {
128 fn as_ref(&self) -> &str {
129 self.value.as_ref()
130 }
131}
132
133impl Display for Tag {
135 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
136 write!(f, "{}", self.value)
137 }
138}
139
140impl Tag {
141 #[inline]
143 pub fn validate(key: &str, value: &str) -> Result<(), TagValidationError> {
144 if key.is_empty() || key.starts_with(':') {
145 Err(TagValidationError::BeginsWithColon)
146 } else if value.is_empty() || value.ends_with(':') {
147 Err(TagValidationError::EndsWithColon)
148 } else {
149 Ok(())
150 }
151 }
152
153 #[inline]
155 pub fn validate_value(value: &str) -> Result<(), TagValidationError> {
156 if value.is_empty() {
157 Err(TagValidationError::Empty)
158 } else if value.starts_with(':') {
159 Err(TagValidationError::BeginsWithColon)
160 } else if value.ends_with(':') {
161 Err(TagValidationError::EndsWithColon)
162 } else {
163 Ok(())
164 }
165 }
166
167 fn from_value<'a, IntoCow>(value: IntoCow) -> anyhow::Result<Self>
169 where
170 IntoCow: Into<Cow<'a, str>>,
171 {
172 let value = value.into();
173 match Self::validate_value(&value) {
174 Ok(()) => Ok(Self {
175 value: Cow::Owned(value.into_owned()),
176 }),
177 Err(error) => {
178 let invalid = InvalidTag {
179 value: value.as_ref(),
180 error,
181 };
182 anyhow::bail!("{invalid}")
183 }
184 }
185 }
186
187 pub fn new<K, V>(key: K, value: V) -> anyhow::Result<Self>
190 where
191 K: AsRef<str>,
192 V: AsRef<str>,
193 {
194 let key = key.as_ref();
195 let value = value.as_ref();
196
197 Tag::from_value(format!("{key}:{value}"))
198 }
199}
200
201pub struct TagParser<'a> {
203 chunks: core::str::Split<'a, &'static [char]>,
204}
205
206impl<'a> TagParser<'a> {
207 pub fn new(input: &'a str) -> Self {
209 const SEPARATORS: &[char] = &[',', ' '];
210 Self {
211 chunks: input.split(SEPARATORS),
212 }
213 }
214}
215
216impl<'a> Iterator for TagParser<'a> {
217 type Item = Result<&'a str, InvalidTag<'a>>;
218
219 fn next(&mut self) -> Option<Self::Item> {
220 let tag = self.chunks.find(|chunk| !chunk.is_empty())?;
221 Some(
222 Tag::validate_value(tag)
223 .map(|()| tag)
224 .map_err(|error| InvalidTag { value: tag, error }),
225 )
226 }
227}
228
229pub fn parse_tags(str: &str) -> (Vec<Tag>, Option<String>) {
238 let mut tags = vec![];
239 let mut error_message = String::new();
240 for result in TagParser::new(str) {
241 match result {
242 Ok(tag) => tags.push(Tag {
243 value: Cow::Owned(tag.to_owned()),
244 }),
245 Err(err) => {
246 if error_message.is_empty() {
247 error_message += "Errors while parsing tags: ";
248 } else {
249 error_message += ", ";
250 }
251 error_message += &err.to_string();
252 }
253 }
254 }
255
256 let error_message = if error_message.is_empty() {
257 None
258 } else {
259 Some(error_message)
260 };
261 (tags, error_message)
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use proptest::prelude::*;
268
269 proptest! {
270 #[test]
271 fn prop_component_and_serialized_validation_are_consistent(
272 key in any::<String>(),
273 value in any::<String>(),
274 ) {
275 let serialized = format!("{key}:{value}");
276
277 prop_assert_eq!(
278 Tag::validate(&key, &value),
279 Tag::validate_value(&serialized)
280 );
281 }
282
283 #[test]
284 fn prop_constructor_and_validation_are_consistent(
285 key in any::<String>(),
286 value in any::<String>(),
287 ) {
288 let serialized = format!("{key}:{value}");
289 let validation = Tag::validate(&key, &value);
290 let result = Tag::new(&key, &value);
291
292 prop_assert_eq!(result.is_ok(), validation.is_ok());
293 if let Ok(tag) = result {
294 prop_assert_eq!(tag.as_ref(), serialized);
295 }
296 }
297
298 #[test]
299 fn prop_serialized_constructor_and_validation_are_consistent(
300 value in any::<String>(),
301 ) {
302 prop_assert_eq!(
303 Tag::from_value(value.as_str()).is_ok(),
304 Tag::validate_value(&value).is_ok()
305 );
306 }
307 }
308
309 #[test]
310 fn test_is_send() {
311 fn is_send<T: Send>(_t: T) -> bool {
313 true
314 }
315 assert!(is_send(tag!("src_library", "libdatadog")));
316 }
317
318 #[test]
319 fn test_validation() {
320 assert_eq!(Tag::validate("key", "value"), Ok(()));
321 assert_eq!(Tag::validate("key", "value:with:colons"), Ok(()));
322 assert_eq!(
323 Tag::validate("", "value"),
324 Err(TagValidationError::BeginsWithColon)
325 );
326 assert_eq!(
327 Tag::validate(":key", "value"),
328 Err(TagValidationError::BeginsWithColon)
329 );
330 assert_eq!(
331 Tag::validate("key", ""),
332 Err(TagValidationError::EndsWithColon)
333 );
334 assert_eq!(
335 Tag::validate("key", "value:"),
336 Err(TagValidationError::EndsWithColon)
337 );
338
339 assert_eq!(Tag::validate_value("key:value"), Ok(()));
340 assert_eq!(Tag::validate_value("tag"), Ok(()));
341 assert_eq!(Tag::validate_value(""), Err(TagValidationError::Empty));
342 assert_eq!(
343 Tag::validate_value(":value"),
344 Err(TagValidationError::BeginsWithColon)
345 );
346 assert_eq!(
347 Tag::validate_value("key:"),
348 Err(TagValidationError::EndsWithColon)
349 );
350 }
351
352 #[test]
353 fn test_empty_key() {
354 let error = Tag::new("", "woof").expect_err("empty key is not allowed");
355 assert_eq!(error.to_string(), "tag ':woof' begins with a colon");
356 }
357
358 #[test]
359 fn test_empty_value() {
360 let error = Tag::new("key1", "").expect_err("empty value is an error");
361 assert_eq!(error.to_string(), "tag 'key1:' ends with a colon");
362 }
363
364 #[test]
365 fn test_bad_utf8() {
366 let bytes = &[b'a', 0b1111_0111];
372 let key = String::from_utf8_lossy(bytes);
373 let t = Tag::new(key, "value").unwrap();
374 assert_eq!("a\u{FFFD}:value", t.to_string());
375 }
376
377 #[test]
378 fn test_value_has_colon() {
379 let result = Tag::new("env", "staging:east").expect("values can have colons");
380 assert_eq!("env:staging:east", result.to_string());
381
382 let result = tag!("env", "staging:east");
383 assert_eq!("env:staging:east", result.to_string());
384 }
385
386 #[test]
387 fn test_suspicious_tags() {
388 let cases = [
392 ("_begins_with_non-letter".to_string(), "value"),
393 ("the-tag-length-is-over-200-characters".repeat(6), "value"),
394 ];
395
396 for case in cases {
397 let result = Tag::new(case.0, case.1);
398 assert!(result.is_ok())
400 }
401 }
402
403 #[test]
404 fn test_missing_colon_parsing() {
405 let tag = Tag::from_value("tag").unwrap();
406 assert_eq!("tag", tag.to_string());
407 }
408
409 #[test]
410 fn test_leading_colon_parsing() {
411 let _ = Tag::from_value(":tag").expect_err("Cannot start with a colon");
412 }
413
414 #[test]
415 fn test_tailing_colon_parsing() {
416 let _ = Tag::from_value("tag:").expect_err("Cannot end with a colon");
417 }
418
419 #[test]
420 fn test_tag_parser() {
421 let parsed =
422 TagParser::new("key:value, :leading middle:colon trailing: bare").collect::<Vec<_>>();
423 assert_eq!(
424 parsed,
425 vec![
426 Ok("key:value"),
427 Err(InvalidTag {
428 value: ":leading",
429 error: TagValidationError::BeginsWithColon,
430 }),
431 Ok("middle:colon"),
432 Err(InvalidTag {
433 value: "trailing:",
434 error: TagValidationError::EndsWithColon,
435 }),
436 Ok("bare"),
437 ]
438 );
439 }
440
441 #[test]
442 fn test_tag_parser_preserves_parse_tags_errors() {
443 let (tags, error) = parse_tags("valid:value,:leading,trailing:,also:valid");
444 assert_eq!(
445 tags,
446 vec![
447 Tag::new("valid", "value").unwrap(),
448 Tag::new("also", "valid").unwrap(),
449 ]
450 );
451 assert_eq!(
452 error.as_deref(),
453 Some(
454 "Errors while parsing tags: tag ':leading' begins with a colon, tag 'trailing:' ends with a colon"
455 )
456 );
457 }
458
459 #[test]
460 fn test_tags_parsing() {
461 let cases = [
462 ("", vec![]),
463 (",", vec![]),
464 (" , ", vec![]),
465 (
467 "env:staging:east,location:nyc:ny",
468 vec![
469 Tag::new("env", "staging:east").unwrap(),
470 Tag::new("location", "nyc:ny").unwrap(),
471 ],
472 ),
473 ("value", vec![Tag::from_value("value").unwrap()]),
475 (
476 "state:utah,state:idaho",
477 vec![
478 Tag::new("state", "utah").unwrap(),
479 Tag::new("state", "idaho").unwrap(),
480 ],
481 ),
482 (
483 "key1:value1 key2:value2 key3:value3",
484 vec![
485 Tag::new("key1", "value1").unwrap(),
486 Tag::new("key2", "value2").unwrap(),
487 Tag::new("key3", "value3").unwrap(),
488 ],
489 ),
490 (
491 "key1:value1, key2:value2 ,key3:value3 , key4:value4",
493 vec![
494 Tag::new("key1", "value1").unwrap(),
495 Tag::new("key2", "value2").unwrap(),
496 Tag::new("key3", "value3").unwrap(),
497 Tag::new("key4", "value4").unwrap(),
498 ],
499 ),
500 ];
501
502 for case in cases {
503 let expected = case.1;
504 let (actual, error_message) = parse_tags(case.0);
505 assert_eq!(expected, actual);
506 assert!(error_message.is_none());
507 }
508 }
509}