mod id;
mod value;
pub use id::*;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
pub use value::{NeodesValue, NeodesValueError};
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
#[cfg_attr(
feature = "serde",
derive(serde_with::DeserializeFromStr, serde_with::SerializeDisplay)
)]
pub struct NeodesLine {
id: FieldId,
value: NeodesValue,
}
impl NeodesLine {
#[inline]
pub fn new(id: FieldId, value: NeodesValue) -> Self {
Self { id, value }
}
#[inline]
pub fn field_id(&self) -> FieldId {
self.id
}
#[inline]
pub fn value(&self) -> &NeodesValue {
&self.value
}
}
const MAX_NEODES_LINE_LENGTH: usize = 255;
impl FromStr for NeodesLine {
type Err = NeodesLineParseError;
#[inline]
fn from_str(mut source: &str) -> Result<Self, Self::Err> {
if source.is_empty() {
return Err(NeodesLineParseError::EmptyLine);
}
if source.chars().count() > MAX_NEODES_LINE_LENGTH {
return Err(NeodesLineParseError::TooLong);
}
if source.ends_with('\r') {
source = &source[..source.len() - 1];
}
let (field_id_src, value_src): (&str, &str) =
source.split_once(',').ok_or(NeodesLineParseError::Format)?;
let field_id = FieldId::from_str(field_id_src)?;
if value_src.len() < 2
|| value_src.chars().next().is_none_or(|t| t != '\'')
|| value_src.chars().last().is_none_or(|t| t != '\'')
{
return Err(NeodesLineParseError::Format);
}
let value =
NeodesValue::from_str(&value_src[1..value_src.len() - 1]).map_err(|value_error| {
NeodesLineParseError::Value {
field_id,
value_error,
}
})?;
Ok(NeodesLine {
id: field_id,
value,
})
}
}
impl Display for NeodesLine {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{},'{}'", self.id, self.value)
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub enum NeodesLineParseError {
EmptyLine,
TooLong,
Format,
Id(FieldIdParseError),
Value {
field_id: FieldId,
value_error: NeodesValueError,
},
}
impl From<FieldIdParseError> for NeodesLineParseError {
#[inline]
fn from(value: FieldIdParseError) -> Self {
NeodesLineParseError::Id(value)
}
}
impl Error for NeodesLineParseError {}
impl Display for NeodesLineParseError {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
NeodesLineParseError::EmptyLine => {
write!(f, "Line must not be empty")
}
NeodesLineParseError::Format => {
write!(f, "Line must be formatted as \"SWW.GXX.YY.ZZZ,'<value>'\"")
}
NeodesLineParseError::TooLong => {
write!(f, "Line must be 256 characters long or shorter")
}
NeodesLineParseError::Id(id_error) => {
write!(
f,
"Line id must respect format \"S99.G99.99.999\" --> {}",
id_error
)
}
NeodesLineParseError::Value {
field_id: _,
value_error,
} => {
write!(f, "Error parsing value --> {}", value_error)
}
}
}
}
#[cfg(test)]
mod tests {
use crate::line::NeodesLineParseError::EmptyLine;
use crate::line::{
FieldId, NeodesLine, NeodesLineParseError, NeodesValue, NeodesValueError, ShortBlockId,
ShortFieldId, ShortGroupId, ShortStrucId,
};
use parameterized::parameterized;
use std::str::FromStr;
const FIELD_ID: FieldId = FieldId::new(
ShortStrucId::from_u8_lossy(12),
ShortGroupId::from_u8_lossy(34),
ShortBlockId::from_u8_lossy(56),
ShortFieldId::from_u16_lossy(789),
);
#[parameterized(input = {
"S21.G0",
"S21.G00.01.123",
"S21.G00.01.123,",
"S21.G00.01.123,'",
"S21.G00.01.123,x'",
"S21.G00.01.123,'x",
}
)]
fn returns_parsing_errors(input: &str) {
assert_eq!(
NeodesLine::from_str(input),
Err(NeodesLineParseError::Format)
);
}
#[test]
fn error_on_empty_line() {
assert_eq!(NeodesLine::from_str(""), Err(EmptyLine))
}
#[test]
fn error_if_too_long() {
assert_eq!(
NeodesLine::from_str(
"S12.G34.56.789,'00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'"
),
Err(NeodesLineParseError::TooLong)
);
}
#[test]
fn accents_count_as_one() {
assert!(
NeodesLine::from_str("S12.G34.56.789,'ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé'")
.is_ok()
);
}
#[test]
fn return_is_removed() {
assert_eq!(
NeodesLine::from_str("S21.G00.01.023,'X'\r"),
NeodesLine::from_str("S21.G00.01.023,'X'")
)
}
#[test]
fn can_parse_str() {
let input = "S12.G34.56.789,'dslhéfbsld'hjhjsgbsbgfgb'";
assert_eq!(
NeodesLine::from_str(input),
Ok(NeodesLine {
id: FIELD_ID,
value: NeodesValue::from_str("dslhéfbsld'hjhjsgbsbgfgb").unwrap(),
})
)
}
#[test]
fn cannot_parse_str_with_emoji() {
let input = "S12.G34.56.789,'dslhfbsld👎hjhjsgbsbgfgb'";
assert_eq!(
NeodesLine::from_str(input),
Err(NeodesLineParseError::Value {
field_id: FIELD_ID,
value_error: NeodesValueError::ForbiddenCharacter
})
)
}
#[test]
#[cfg(feature = "serde")]
fn can_serialize() {
let value = NeodesLine {
id: FIELD_ID,
value: NeodesValue::from_str("dslhfbsld'hjhjsgbsbgfgb").unwrap(),
};
let result: NeodesLine =
serde_json::from_str("\"S12.G34.56.789,'dslhfbsld'hjhjsgbsbgfgb'\"").unwrap();
assert_eq!(result, value)
}
#[test]
#[cfg(feature = "serde")]
fn can_deserialize() {
let value = NeodesLine {
id: FIELD_ID,
value: NeodesValue::from_str("dslhfbsld'hjhjsgbsbgfgb").unwrap(),
};
let result = serde_json::to_string(&value).unwrap();
assert_eq!(result, "\"S12.G34.56.789,'dslhfbsld'hjhjsgbsbgfgb'\"")
}
}