use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
pub enum ApplicationTargetError {
#[error("application path must not be empty")]
EmptyPath,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "ApplicationTargetWire", into = "ApplicationTargetWire")]
pub struct ApplicationTarget {
path: String,
display_name: String,
}
impl ApplicationTarget {
pub fn new(
path: impl Into<String>,
display_name: impl Into<String>,
) -> Result<Self, ApplicationTargetError> {
let path = path.into().trim().to_string();
if path.is_empty() {
return Err(ApplicationTargetError::EmptyPath);
}
let requested_name = display_name.into();
let display_name = if requested_name.trim().is_empty() {
std::path::Path::new(&path)
.file_stem()
.and_then(std::ffi::OsStr::to_str)
.filter(|name| !name.is_empty())
.unwrap_or(path.as_str())
.to_string()
} else {
requested_name.trim().to_string()
};
Ok(Self { path, display_name })
}
#[must_use]
pub fn path(&self) -> &str {
&self.path
}
#[must_use]
pub fn display_name(&self) -> &str {
&self.display_name
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct ApplicationTargetWire {
path: String,
#[serde(default)]
display_name: String,
}
impl TryFrom<ApplicationTargetWire> for ApplicationTarget {
type Error = ApplicationTargetError;
fn try_from(target: ApplicationTargetWire) -> Result<Self, Self::Error> {
Self::new(target.path, target.display_name)
}
}
impl From<ApplicationTarget> for ApplicationTargetWire {
fn from(target: ApplicationTarget) -> Self {
Self {
path: target.path,
display_name: target.display_name,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validates_and_derives_display_name() {
let target = ApplicationTarget::new("/Applications/Safari.app", "")
.unwrap_or_else(|error| panic!("valid target failed: {error}"));
assert_eq!(target.path(), "/Applications/Safari.app");
assert_eq!(target.display_name(), "Safari");
assert_eq!(
ApplicationTarget::new(" ", "Safari"),
Err(ApplicationTargetError::EmptyPath)
);
}
#[test]
fn roundtrips_as_a_human_readable_target_table() {
let target = ApplicationTarget::new("https://example.test", "Example")
.unwrap_or_else(|error| panic!("valid target failed: {error}"));
let encoded = toml::to_string(&target)
.unwrap_or_else(|error| panic!("target serialization failed: {error}"));
assert!(encoded.contains("path = \"https://example.test\""));
assert_eq!(toml::from_str::<ApplicationTarget>(&encoded), Ok(target));
}
}