1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::{DataError, Result};
6
7fn validate_identifier(value: &str) -> Result<()> {
8 if value.is_empty() {
9 return Err(DataError::InvalidIdentifier {
10 value: value.to_string(),
11 reason: "identifier is empty",
12 });
13 }
14 if value.len() > 128 {
15 return Err(DataError::InvalidIdentifier {
16 value: value.to_string(),
17 reason: "identifier is longer than 128 bytes",
18 });
19 }
20 if !value
21 .bytes()
22 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.'))
23 {
24 return Err(DataError::InvalidIdentifier {
25 value: value.to_string(),
26 reason: "identifier contains unsupported characters",
27 });
28 }
29 Ok(())
30}
31
32macro_rules! define_id {
33 ($name:ident) => {
34 #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
35 #[serde(try_from = "String", into = "String")]
36 pub struct $name(String);
37
38 impl $name {
39 pub fn new(value: impl Into<String>) -> Result<Self> {
40 let value = value.into();
41 validate_identifier(&value)?;
42 Ok(Self(value))
43 }
44
45 pub fn as_str(&self) -> &str {
46 &self.0
47 }
48 }
49
50 impl TryFrom<String> for $name {
51 type Error = DataError;
52
53 fn try_from(value: String) -> Result<Self> {
54 Self::new(value)
55 }
56 }
57
58 impl From<$name> for String {
59 fn from(value: $name) -> Self {
60 value.0
61 }
62 }
63
64 impl fmt::Display for $name {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 self.0.fmt(f)
67 }
68 }
69 };
70}
71
72define_id!(SampleId);
73define_id!(SourceId);
74define_id!(RepresentationId);
75define_id!(TypeId);
76define_id!(ObservationId);
77define_id!(TargetId);
78define_id!(GroupId);
79define_id!(OriginId);
80define_id!(RepetitionId);
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn accepts_data_ids() {
88 assert!(SourceId::new("nir.source_1").is_ok());
89 }
90
91 #[test]
92 fn rejects_graph_style_colon_for_data_ids() {
93 assert!(SourceId::new("source:nir").is_err());
94 }
95}