neodes_codec 0.1.3

Library to read and write data from DSN files as described in the NeoDes norm.
Documentation
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::str::FromStr;

use crate::line::value::CharType::{Forbidden, NonValidating, Validating};

/// Value part of a NeoDes line
/// i.e. `value` in `S21.G00.12.345,'value'`
///
/// Enforces character validation
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
#[cfg_attr(
	feature = "serde",
	derive(serde_with::DeserializeFromStr, serde_with::SerializeDisplay)
)]
pub struct NeodesValue(String);

impl NeodesValue {
	/// Extracts a string slice containing the entire `NeodesValue`.
	///
	/// # Examples
	///
	/// ```
	/// # use std::error::Error;
	/// # fn main() -> Result<(), Box<dyn Error>> {
	///
	/// use std::str::FromStr;
	/// use neodes_codec::line::NeodesValue;
	///
	/// let  input = "X";
	/// let  neodes_value = NeodesValue::from_str(input)?;
	/// assert_eq!(neodes_value.as_str(), input);
	///
	/// # Ok(())
	/// # }
	/// ```
	#[inline]
	pub fn as_str(&self) -> &str {
		self.0.as_str()
	}
}

impl FromStr for NeodesValue {
	type Err = NeodesValueError;

	/// # Examples
	///
	/// ```
	/// # use std::error::Error;
	/// # fn main() -> Result<(), Box<dyn Error>> {
	///
	/// use std::str::FromStr;
	///
	/// use neodes_codec::line::NeodesValue;
	/// use neodes_codec::line::NeodesValueError::{ForbiddenCharacter, MissingAlphanumericCharacter};
	///
	/// let value = NeodesValue::from_str("e")?;
	/// assert_eq!(value.to_string(), "e");
	///
	/// let value = NeodesValue::from_str("é")?;
	/// assert_eq!(value.to_string(), "é");
	///
	/// assert_eq!(NeodesValue::from_str("%"), Err(ForbiddenCharacter));
	/// assert_eq!(NeodesValue::from_str("😔"), Err(ForbiddenCharacter));
	/// assert_eq!(
	///     NeodesValue::from_str("''"),
	///     Err(MissingAlphanumericCharacter)
	/// );
	///
	/// # Ok(())
	/// # }
	///
	/// ```
	#[inline]
	fn from_str(input: &str) -> Result<Self, Self::Err> {
		let mut validated = false;
		for c in input.chars() {
			match CharType::from(c) {
				Validating => validated = true,
				Forbidden => {
					return Err(NeodesValueError::ForbiddenCharacter);
				}
				_ => {}
			}
		}

		if !validated {
			return Err(NeodesValueError::MissingAlphanumericCharacter);
		}

		Ok(Self(input.to_string()))
	}
}

impl Display for NeodesValue {
	#[inline]
	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
		write!(f, "{}", self.0)
	}
}

enum CharType {
	Validating,
	NonValidating,
	Forbidden,
}

impl From<char> for CharType {
	#[inline]
	fn from(c: char) -> Self {
		match c {
			'0'..='9'
			| 'A'..='Z'
			| 'a'..='z'
			| 'À'..='Å'
			| 'Ç'..='Ï'
			| 'Ñ'..='Ö'
			| 'Ù'..='Ý'
			| 'à'..='å'
			| 'ç'..='ï'
			| 'ñ'..='ö'
			| 'ù'..='ý'
			| 'ÿ' => Validating,

			' ' | '"' | '&'..='/' | ':' | '=' | '@' | '_' | '`' | '«' | '°' | '»' => {
				NonValidating
			}
			_ => Forbidden,
		}
	}
}

/// Error returned in cas of an invalid NeoDes value
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub enum NeodesValueError {
	/// A forbidden character was found
	ForbiddenCharacter,

	/// No character validating the value (such as a digit or a letter) was
	/// found
	MissingAlphanumericCharacter,
}

impl Error for NeodesValueError {}

impl Display for NeodesValueError {
	#[inline]
	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
		match self {
			NeodesValueError::ForbiddenCharacter => {
				write!(f, "Forbidden character")
			}
			NeodesValueError::MissingAlphanumericCharacter => {
				write!(f, "Value must contain at least one validating character")
			}
		}
	}
}