rakata_formats/gff/
label.rs1use std::fmt::{Display, Formatter};
2use std::str::FromStr;
3use thiserror::Error;
4
5pub const MAX_GFF_LABEL_LEN: usize = 16;
7
8#[derive(Debug, Clone, PartialEq, Eq, Error)]
10pub enum GffLabelError {
11 #[error("label length {len} exceeds maximum {max}")]
13 TooLong {
14 len: usize,
16 max: usize,
18 },
19 #[error("invalid GFF label character '{ch}'")]
21 InvalidChar {
22 ch: char,
24 },
25}
26
27#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
38pub struct GffLabel {
39 bytes: [u8; MAX_GFF_LABEL_LEN],
40 len: u8,
41}
42
43impl GffLabel {
44 pub fn new(value: impl AsRef<str>) -> Result<Self, GffLabelError> {
48 let raw = value.as_ref();
49 if raw.len() > MAX_GFF_LABEL_LEN {
50 return Err(GffLabelError::TooLong {
51 len: raw.len(),
52 max: MAX_GFF_LABEL_LEN,
53 });
54 }
55 let mut bytes = [0u8; MAX_GFF_LABEL_LEN];
56 for (i, ch) in raw.chars().enumerate() {
57 if !(ch.is_ascii_alphanumeric() || ch == '_' || ch == ' ') {
58 return Err(GffLabelError::InvalidChar { ch });
59 }
60 bytes[i] = u8::try_from(u32::from(ch)).expect("ASCII char is guaranteed to fit in u8");
62 }
63 let len = u8::try_from(raw.len()).expect("len already bounded by MAX_GFF_LABEL_LEN");
64 Ok(Self { bytes, len })
65 }
66
67 pub fn as_str(&self) -> &str {
69 std::str::from_utf8(&self.bytes[..usize::from(self.len)])
72 .expect("all bytes are validated ASCII during construction")
73 }
74
75 #[allow(clippy::as_conversions)]
77 pub const fn len(&self) -> usize {
78 self.len as usize
79 }
80
81 pub const fn is_empty(&self) -> bool {
83 self.len == 0
84 }
85}
86
87impl std::fmt::Debug for GffLabel {
88 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
89 write!(f, "GffLabel({:?})", self.as_str())
90 }
91}
92
93impl Display for GffLabel {
94 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
95 f.write_str(self.as_str())
96 }
97}
98
99impl FromStr for GffLabel {
100 type Err = GffLabelError;
101
102 fn from_str(s: &str) -> Result<Self, Self::Err> {
103 Self::new(s)
104 }
105}
106
107impl TryFrom<&str> for GffLabel {
108 type Error = GffLabelError;
109
110 fn try_from(value: &str) -> Result<Self, Self::Error> {
111 Self::new(value)
112 }
113}
114
115impl TryFrom<String> for GffLabel {
116 type Error = GffLabelError;
117
118 fn try_from(value: String) -> Result<Self, Self::Error> {
119 Self::new(value)
120 }
121}
122
123impl From<GffLabel> for String {
124 fn from(val: GffLabel) -> Self {
125 val.as_str().to_owned()
126 }
127}
128
129impl AsRef<str> for GffLabel {
130 fn as_ref(&self) -> &str {
131 self.as_str()
132 }
133}
134
135impl PartialEq<str> for GffLabel {
136 fn eq(&self, other: &str) -> bool {
137 self.as_str() == other
138 }
139}
140
141impl PartialEq<&str> for GffLabel {
142 fn eq(&self, other: &&str) -> bool {
143 self.as_str() == *other
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn accepts_valid_label() {
153 let parsed = GffLabel::new("Equip_ItemList").expect("valid label");
154 assert_eq!(parsed.as_str(), "Equip_ItemList");
155 }
156
157 #[test]
158 fn rejects_too_long_label() {
159 let err = GffLabel::new("this_name_is_longer_than_sixteen").expect_err("must fail");
160 assert!(matches!(err, GffLabelError::TooLong { .. }));
161 }
162
163 #[test]
164 fn rejects_invalid_characters() {
165 let err = GffLabel::new("bad!name").expect_err("must fail");
166 assert_eq!(err, GffLabelError::InvalidChar { ch: '!' });
167 }
168
169 #[test]
170 fn is_copy() {
171 fn takes_copy<T: Copy>(_: T) {}
172 takes_copy(GffLabel::new("Test").unwrap());
173 }
174
175 #[test]
176 fn roundtrip_through_string() {
177 let original = GffLabel::new("test_label").expect("valid");
178 let s: String = original.into();
179 assert_eq!(s, "test_label");
180 let back: GffLabel = s.try_into().expect("valid");
181 assert_eq!(back, original);
182 }
183}