1use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
2use std::{fmt, str::FromStr};
3
4const MAX_KEY_BYTES: usize = 128;
5
6#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
20pub struct StableKey(String);
21
22impl<'de> Deserialize<'de> for StableKey {
23 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
24 #[derive(Deserialize)]
26 #[serde(rename = "StableKey")]
27 struct Text(String);
28
29 let Text(value) = Text::deserialize(deserializer)?;
30 validate(&value).map_err(D::Error::custom)?;
31 Ok(Self(value))
32 }
33}
34
35impl StableKey {
36 pub fn parse(value: impl AsRef<str>) -> Result<Self, StableKeyError> {
44 let value = value.as_ref();
45 validate(value)?;
46 Ok(Self(value.to_string()))
47 }
48
49 #[must_use]
51 pub fn as_str(&self) -> &str {
52 &self.0
53 }
54
55 #[must_use]
57 pub fn into_string(self) -> String {
58 self.0
59 }
60}
61
62impl AsRef<str> for StableKey {
63 fn as_ref(&self) -> &str {
64 self.as_str()
65 }
66}
67
68impl fmt::Display for StableKey {
69 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
70 formatter.write_str(self.as_str())
71 }
72}
73
74impl FromStr for StableKey {
75 type Err = StableKeyError;
76
77 fn from_str(value: &str) -> Result<Self, Self::Err> {
78 Self::parse(value)
79 }
80}
81
82#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
87#[error("stable key '{stable_key}' is invalid: {reason}")]
88pub struct StableKeyError {
89 pub stable_key: String,
91 pub reason: &'static str,
93}
94
95pub fn validate(stable_key: &str) -> Result<(), StableKeyError> {
96 if stable_key.is_empty() {
97 return invalid(stable_key, "must not be empty");
98 }
99 if stable_key.len() > MAX_KEY_BYTES {
100 return invalid(stable_key, "must be at most 128 bytes");
101 }
102 let mut invalid_ascii = false;
105 let mut uppercase = false;
106 let mut whitespace = false;
107 let mut separator = false;
108 for byte in stable_key.bytes() {
109 invalid_ascii |= !byte.is_ascii();
110 uppercase |= byte.is_ascii_uppercase();
111 whitespace |= matches!(byte, b'\t'..=b'\r' | b' ');
112 separator |= matches!(byte, b'/' | b'-');
113 }
114 if invalid_ascii {
115 return invalid(stable_key, "must be ASCII");
116 }
117 if uppercase {
118 return invalid(stable_key, "must be lowercase");
119 }
120 if whitespace {
121 return invalid(stable_key, "must not contain whitespace");
122 }
123 if separator {
124 return invalid(stable_key, "must not contain slashes or hyphens");
125 }
126 if stable_key.starts_with('.') || stable_key.ends_with('.') {
127 return invalid(stable_key, "must not start or end with a dot");
128 }
129
130 let Some(version_index) = stable_key
131 .as_bytes()
132 .windows(2)
133 .rposition(|pair| pair == b".v")
134 else {
135 return invalid(stable_key, "must end with .vN");
136 };
137 let version = &stable_key[version_index + 2..];
138 if version.is_empty()
139 || version.starts_with('0')
140 || !version.bytes().all(|byte| byte.is_ascii_digit())
141 {
142 return invalid(stable_key, "version suffix must be nonzero .vN");
143 }
144
145 let prefix = &stable_key[..version_index];
146 if prefix.is_empty() {
147 return invalid(
148 stable_key,
149 "must contain at least one segment before version",
150 );
151 }
152
153 for segment in prefix.as_bytes().split(|byte| *byte == b'.') {
154 if let Err(reason) = validate_segment(segment) {
155 return invalid(stable_key, reason);
156 }
157 }
158
159 Ok(())
160}
161
162pub fn is_key_prefix(value: &str) -> bool {
165 let Some(prefix) = value.strip_suffix('.') else {
166 return false;
167 };
168 value.len() <= MAX_KEY_BYTES - "v1".len()
169 && prefix
170 .as_bytes()
171 .split(|byte| *byte == b'.')
172 .all(|segment| validate_segment(segment).is_ok())
173}
174
175fn validate_segment(segment: &[u8]) -> Result<(), &'static str> {
176 let mut bytes = segment.iter().copied();
177 let Some(first) = bytes.next() else {
178 return Err("must not contain empty segments");
179 };
180 if !first.is_ascii_lowercase() {
181 return Err("segments must start with a lowercase letter");
182 }
183 if !bytes.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_') {
184 return Err("segments may contain only lowercase letters, digits, and underscores");
185 }
186 Ok(())
187}
188
189fn invalid<T>(stable_key: &str, reason: &'static str) -> Result<T, StableKeyError> {
190 Err(StableKeyError {
191 stable_key: stable_key.to_string(),
192 reason,
193 })
194}
195
196#[cfg(test)]
197mod tests;