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