use std::fmt::Display;
use std::fmt::Formatter;
use std::str::FromStr;
use serde::Deserialize;
use serde::Serialize;
use serde::Serializer;
use super::portable_name::is_portable;
use crate::error::ParseProjectIdentifierError;
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize)]
#[serde(try_from = "String")]
pub struct ProjectIdentifier(String);
impl ProjectIdentifier {
pub fn get(&self) -> &str {
&self.0
}
}
impl Display for ProjectIdentifier {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl Serialize for ProjectIdentifier {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.0)
}
}
impl FromStr for ProjectIdentifier {
type Err = ParseProjectIdentifierError;
fn from_str(identifier: &str) -> Result<Self, Self::Err> {
Self::try_from(identifier.to_owned())
}
}
impl TryFrom<&str> for ProjectIdentifier {
type Error = ParseProjectIdentifierError;
fn try_from(identifier: &str) -> Result<Self, Self::Error> {
Self::try_from(identifier.to_owned())
}
}
impl TryFrom<String> for ProjectIdentifier {
type Error = ParseProjectIdentifierError;
fn try_from(identifier: String) -> Result<Self, Self::Error> {
if is_portable(&identifier) {
Ok(Self(identifier))
} else {
Err(ParseProjectIdentifierError::MalformedIdentifier { identifier })
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::missing_panics_doc)]
use super::*;
#[derive(Debug, Deserialize)]
struct Document {
id: ProjectIdentifier,
}
#[test]
fn deserialize_with_a_malformed_identifier_returns_an_error() {
let result = toml::from_str::<Document>(r#"id = "../example""#);
assert!(result.is_err());
}
#[test]
fn deserialize_with_a_portable_identifier_returns_the_identifier() {
let document: Document = toml::from_str(r#"id = "example""#).unwrap();
assert_eq!(document.id.get(), "example");
}
#[test]
fn from_str_with_a_malformed_identifier_returns_an_error() {
let error = "C:".parse::<ProjectIdentifier>().unwrap_err();
assert!(matches!(
error,
ParseProjectIdentifierError::MalformedIdentifier { .. }
));
}
#[test]
fn from_str_with_a_portable_identifier_returns_the_identifier() {
let identifier: ProjectIdentifier = "example".parse().unwrap();
assert_eq!(identifier.get(), "example");
}
#[test]
fn serialize_writes_a_string() {
let identifier: ProjectIdentifier = "example".parse().unwrap();
serde_test::assert_ser_tokens(&identifier, &[serde_test::Token::Str("example")]);
}
#[test]
fn to_string_of_the_error_escapes_the_value() {
let error = "aux\u{1b}[2J".parse::<ProjectIdentifier>().unwrap_err();
assert_eq!(
error.to_string(),
"the project identifier \"aux\\u{1b}[2J\" is malformed, because an identifier holds 1 to 250 ASCII letters, digits, and hyphens, and does not name a device on Windows"
);
}
#[test]
fn try_from_a_str_with_a_malformed_identifier_returns_an_error() {
let error = ProjectIdentifier::try_from("/example").unwrap_err();
assert!(matches!(
error,
ParseProjectIdentifierError::MalformedIdentifier { .. }
));
}
#[test]
fn try_from_a_string_with_a_malformed_identifier_returns_an_error() {
let error = ProjectIdentifier::try_from(String::from("example project")).unwrap_err();
assert!(matches!(
error,
ParseProjectIdentifierError::MalformedIdentifier { .. }
));
}
}