1mod id;
2mod value;
3
4pub use id::*;
5use std::error::Error;
6use std::fmt::{Display, Formatter};
7use std::str::FromStr;
8pub use value::{NeodesValue, NeodesValueError};
9
10#[derive(Clone, Eq, PartialEq, Hash, Debug)]
13#[cfg_attr(
14 feature = "serde",
15 derive(serde_with::DeserializeFromStr, serde_with::SerializeDisplay)
16)]
17pub struct NeodesLine {
18 id: FieldId,
20
21 value: NeodesValue,
23}
24
25impl NeodesLine {
26 #[inline]
44 pub fn new(id: FieldId, value: NeodesValue) -> Self {
45 Self { id, value }
46 }
47
48 #[inline]
51 pub fn field_id(&self) -> FieldId {
52 self.id
53 }
54
55 #[inline]
57 pub fn value(&self) -> &NeodesValue {
58 &self.value
59 }
60}
61
62const MAX_NEODES_LINE_LENGTH: usize = 255;
63
64impl FromStr for NeodesLine {
65 type Err = NeodesLineParseError;
66
67 #[inline]
93 fn from_str(mut source: &str) -> Result<Self, Self::Err> {
94 if source.is_empty() {
96 return Err(NeodesLineParseError::EmptyLine);
97 }
98
99 if source.chars().count() > MAX_NEODES_LINE_LENGTH {
100 return Err(NeodesLineParseError::TooLong);
101 }
102
103 if source.ends_with('\r') {
105 source = &source[..source.len() - 1];
106 }
107
108 let (field_id_src, value_src): (&str, &str) =
109 source.split_once(',').ok_or(NeodesLineParseError::Format)?;
110
111 let field_id = FieldId::from_str(field_id_src)?;
113
114 if value_src.len() < 2
116 || value_src.chars().next().is_none_or(|t| t != '\'')
117 || value_src.chars().last().is_none_or(|t| t != '\'')
118 {
119 return Err(NeodesLineParseError::Format);
120 }
121
122 let value =
123 NeodesValue::from_str(&value_src[1..value_src.len() - 1]).map_err(|value_error| {
124 NeodesLineParseError::Value {
125 field_id,
126 value_error,
127 }
128 })?;
129
130 Ok(NeodesLine {
131 id: field_id,
132 value,
133 })
134 }
135}
136
137impl Display for NeodesLine {
138 #[inline]
139 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
140 write!(f, "{},'{}'", self.id, self.value)
141 }
142}
143
144#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
147pub enum NeodesLineParseError {
148 EmptyLine,
150
151 TooLong,
154
155 Format,
158
159 Id(FieldIdParseError),
161
162 Value {
164 field_id: FieldId,
166
167 value_error: NeodesValueError,
169 },
170}
171
172impl From<FieldIdParseError> for NeodesLineParseError {
173 #[inline]
174 fn from(value: FieldIdParseError) -> Self {
175 NeodesLineParseError::Id(value)
176 }
177}
178
179impl Error for NeodesLineParseError {}
180
181impl Display for NeodesLineParseError {
182 #[inline]
183 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
184 match self {
185 NeodesLineParseError::EmptyLine => {
186 write!(f, "Line must not be empty")
187 }
188 NeodesLineParseError::Format => {
189 write!(f, "Line must be formatted as \"SWW.GXX.YY.ZZZ,'<value>'\"")
190 }
191 NeodesLineParseError::TooLong => {
192 write!(f, "Line must be 256 characters long or shorter")
193 }
194 NeodesLineParseError::Id(id_error) => {
195 write!(
196 f,
197 "Line id must respect format \"S99.G99.99.999\" --> {}",
198 id_error
199 )
200 }
201 NeodesLineParseError::Value {
202 field_id: _,
203 value_error,
204 } => {
205 write!(f, "Error parsing value --> {}", value_error)
206 }
207 }
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use crate::line::NeodesLineParseError::EmptyLine;
214 use crate::line::{
215 FieldId, NeodesLine, NeodesLineParseError, NeodesValue, NeodesValueError, ShortBlockId,
216 ShortFieldId, ShortGroupId, ShortStrucId,
217 };
218 use parameterized::parameterized;
219 use std::str::FromStr;
220
221 const FIELD_ID: FieldId = FieldId::new(
222 ShortStrucId::from_u8_lossy(12),
223 ShortGroupId::from_u8_lossy(34),
224 ShortBlockId::from_u8_lossy(56),
225 ShortFieldId::from_u16_lossy(789),
226 );
227
228 #[parameterized(input = {
229 "S21.G0",
230 "S21.G00.01.123",
231 "S21.G00.01.123,",
232 "S21.G00.01.123,'",
233 "S21.G00.01.123,x'",
234 "S21.G00.01.123,'x",
235 }
236 )]
237 fn returns_parsing_errors(input: &str) {
238 assert_eq!(
239 NeodesLine::from_str(input),
240 Err(NeodesLineParseError::Format)
241 );
242 }
243
244 #[test]
245 fn error_on_empty_line() {
246 assert_eq!(NeodesLine::from_str(""), Err(EmptyLine))
247 }
248
249 #[test]
250 fn error_if_too_long() {
251 assert_eq!(
252 NeodesLine::from_str(
253 "S12.G34.56.789,'00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'"
254 ),
255 Err(NeodesLineParseError::TooLong)
256 );
257 }
258
259 #[test]
260 fn accents_count_as_one() {
261 assert!(
262 NeodesLine::from_str("S12.G34.56.789,'ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé'")
263 .is_ok()
264 );
265 }
266
267 #[test]
268 fn return_is_removed() {
269 assert_eq!(
270 NeodesLine::from_str("S21.G00.01.023,'X'\r"),
271 NeodesLine::from_str("S21.G00.01.023,'X'")
272 )
273 }
274
275 #[test]
276 fn can_parse_str() {
277 let input = "S12.G34.56.789,'dslhéfbsld'hjhjsgbsbgfgb'";
278
279 assert_eq!(
280 NeodesLine::from_str(input),
281 Ok(NeodesLine {
282 id: FIELD_ID,
283 value: NeodesValue::from_str("dslhéfbsld'hjhjsgbsbgfgb").unwrap(),
284 })
285 )
286 }
287
288 #[test]
289 fn cannot_parse_str_with_emoji() {
290 let input = "S12.G34.56.789,'dslhfbsld👎hjhjsgbsbgfgb'";
291
292 assert_eq!(
293 NeodesLine::from_str(input),
294 Err(NeodesLineParseError::Value {
295 field_id: FIELD_ID,
296 value_error: NeodesValueError::ForbiddenCharacter
297 })
298 )
299 }
300
301 #[test]
302 #[cfg(feature = "serde")]
303 fn can_serialize() {
304 let value = NeodesLine {
305 id: FIELD_ID,
306 value: NeodesValue::from_str("dslhfbsld'hjhjsgbsbgfgb").unwrap(),
307 };
308 let result: NeodesLine =
309 serde_json::from_str("\"S12.G34.56.789,'dslhfbsld'hjhjsgbsbgfgb'\"").unwrap();
310 assert_eq!(result, value)
311 }
312
313 #[test]
314 #[cfg(feature = "serde")]
315 fn can_deserialize() {
316 let value = NeodesLine {
317 id: FIELD_ID,
318 value: NeodesValue::from_str("dslhfbsld'hjhjsgbsbgfgb").unwrap(),
319 };
320 let result = serde_json::to_string(&value).unwrap();
321 assert_eq!(result, "\"S12.G34.56.789,'dslhfbsld'hjhjsgbsbgfgb'\"")
322 }
323}