Skip to main content

neodes_codec/line/
mod.rs

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/// NeoDes Line
11/// Encodes a NeoDes line represented as `SWW.GXX.YY.ZZZ,'value'`
12#[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	/// The id of the NeoDes line  (`SWW.GXX.YY.ZZZ`)
19	id: FieldId,
20
21	/// The value filled in the NeoDes line (`value`)
22	value: NeodesValue,
23}
24
25impl NeodesLine {
26	/// Create a NeodesLine from an id and a value
27	///
28	/// # Examples
29	///
30	/// ```
31	/// # use std::error::Error;
32	/// # fn main() -> Result<(), Box<dyn Error>> {
33	/// use std::str::FromStr;
34	/// use neodes_codec::line::{FieldId, NeodesLine, NeodesValue};
35	///
36	/// let id = FieldId::from_str("S21.G00.13.123")?;
37	/// let value = NeodesValue::from_str("X")?;
38	/// assert_eq!(NeodesLine::new(id, value).to_string(), "S21.G00.13.123,'X'");
39	///
40	/// # Ok(())
41	/// # }
42	/// ```
43	#[inline]
44	pub fn new(id: FieldId, value: NeodesValue) -> Self {
45		Self { id, value }
46	}
47
48	/// Field ID
49	/// This is the first part of the line (`SWW.GXX.YY.ZZZ`)
50	#[inline]
51	pub fn field_id(&self) -> FieldId {
52		self.id
53	}
54
55	/// The value filled in the NeoDes line (`value`)
56	#[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	/// # Examples
68	///
69	/// ```
70	/// # use std::error::Error;
71	/// # fn main() -> Result<(), Box<dyn Error>> {
72	/// use neodes_codec::line::{FieldId, NeodesLine, NeodesLineParseError, NeodesValue, ShortBlockId, ShortFieldId, ShortGroupId, ShortStrucId};
73	/// use std::str::FromStr;
74	///
75	/// assert_eq!(
76	///     NeodesLine::from_str("S21.G00.01.123,'X'"),
77	///     Ok(NeodesLine::new(
78	///         FieldId::new(
79	///             ShortStrucId::from_u8_lossy(21),
80	///             ShortGroupId::from_u8_lossy(0),
81	///             ShortBlockId::from_u8_lossy(1),
82	///             ShortFieldId::from_u16_lossy(123)
83	///         ),
84	///         NeodesValue::from_str("X")?
85	///     ))
86	/// );
87	/// assert_eq!(NeodesLine::from_str("S21.G00.01.123'X'"), Err(NeodesLineParseError::Format));
88	///
89	/// # Ok(())
90	/// # }
91	/// ```
92	#[inline]
93	fn from_str(mut source: &str) -> Result<Self, Self::Err> {
94		// Check length
95		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		// Remove \r at the end
104		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		// Parse ID
112		let field_id = FieldId::from_str(field_id_src)?;
113
114		// Parse value
115		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/// Error returned when trying to parse a line that doesn't respect the standard
145/// format : `SWW.GXX.YY.ZZZ,'value'`
146#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
147pub enum NeodesLineParseError {
148	/// Line is empty
149	EmptyLine,
150
151	/// Line length is greater than the maximum authorized length, including
152	/// terminating characters \r or \n
153	TooLong,
154
155	/// Either comma or quotes are misplaced, or the line is too short to be
156	/// valid
157	Format,
158
159	/// An error occurred while parsing the id
160	Id(FieldIdParseError),
161
162	/// An error occurred while parsing the value. The field ID was parsed though.
163	Value {
164		/// Field ID parsed before the value was.
165		field_id: FieldId,
166
167		/// Error that occurred while parsing value
168		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}