proto_blue_syntax/
recordkey.rs1use regex::Regex;
7use std::fmt;
8use std::str::FromStr;
9
10const RECORD_KEY_MAX_LENGTH: usize = 512;
12
13static RECORD_KEY_REGEX: std::sync::LazyLock<Regex> =
14 std::sync::LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9_~.:\-]{1,512}$").unwrap());
15
16#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
21pub struct RecordKey(String);
22
23#[derive(Debug, Clone, thiserror::Error)]
25#[error("Invalid record key: {reason}")]
26pub struct InvalidRecordKeyError {
27 pub reason: String,
28}
29
30impl RecordKey {
31 pub fn new(s: &str) -> Result<Self, InvalidRecordKeyError> {
33 ensure_valid_record_key(s)?;
34 Ok(Self(s.to_string()))
35 }
36
37 #[must_use]
39 pub fn is_valid(s: &str) -> bool {
40 ensure_valid_record_key(s).is_ok()
41 }
42
43 #[must_use]
45 pub fn as_str(&self) -> &str {
46 &self.0
47 }
48
49 #[must_use]
51 pub fn into_inner(self) -> String {
52 self.0
53 }
54}
55
56fn ensure_valid_record_key(s: &str) -> Result<(), InvalidRecordKeyError> {
57 let err = |reason: &str| InvalidRecordKeyError {
58 reason: reason.to_string(),
59 };
60
61 if s.is_empty() {
62 return Err(err("Record key must not be empty"));
63 }
64
65 if s.len() > RECORD_KEY_MAX_LENGTH {
66 return Err(err(&format!(
67 "Record key too long ({} chars, max {})",
68 s.len(),
69 RECORD_KEY_MAX_LENGTH
70 )));
71 }
72
73 if s == "." || s == ".." {
74 return Err(err("Record key cannot be \".\" or \"..\""));
75 }
76
77 if !RECORD_KEY_REGEX.is_match(s) {
78 return Err(err(
79 "Record key must contain only [a-zA-Z0-9_~.:-] characters",
80 ));
81 }
82
83 Ok(())
84}
85
86impl fmt::Display for RecordKey {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 f.write_str(&self.0)
89 }
90}
91
92impl FromStr for RecordKey {
93 type Err = InvalidRecordKeyError;
94 fn from_str(s: &str) -> Result<Self, Self::Err> {
95 Self::new(s)
96 }
97}
98
99impl AsRef<str> for RecordKey {
100 fn as_ref(&self) -> &str {
101 &self.0
102 }
103}
104
105impl serde::Serialize for RecordKey {
106 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
107 self.0.serialize(serializer)
108 }
109}
110
111impl<'de> serde::Deserialize<'de> for RecordKey {
112 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
113 let s = String::deserialize(deserializer)?;
114 Self::new(&s).map_err(serde::de::Error::custom)
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn valid_record_keys() {
124 let cases = ["self", "3jui7kd54zh2y", "example.com", "a", "a-b_c~d:e"];
125 for rkey in &cases {
126 assert!(RecordKey::new(rkey).is_ok(), "should be valid: {rkey}");
127 }
128 }
129
130 #[test]
131 fn invalid_record_keys() {
132 assert!(RecordKey::new("").is_err(), "empty");
133 assert!(RecordKey::new(".").is_err(), "dot");
134 assert!(RecordKey::new("..").is_err(), "double dot");
135 assert!(RecordKey::new("has space").is_err(), "space");
136 assert!(RecordKey::new("has/slash").is_err(), "slash");
137 assert!(RecordKey::new("has#hash").is_err(), "hash");
138 }
139
140 #[test]
141 fn max_length() {
142 let max = "a".repeat(RECORD_KEY_MAX_LENGTH);
143 assert!(RecordKey::new(&max).is_ok());
144 let too_long = "a".repeat(RECORD_KEY_MAX_LENGTH + 1);
145 assert!(RecordKey::new(&too_long).is_err());
146 }
147}