openlogi_core/binding/
application_target.rs1use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
8pub enum ApplicationTargetError {
9 #[error("application path must not be empty")]
11 EmptyPath,
12}
13
14#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
16#[serde(try_from = "ApplicationTargetWire", into = "ApplicationTargetWire")]
17pub struct ApplicationTarget {
18 path: String,
19 display_name: String,
20}
21
22impl ApplicationTarget {
23 pub fn new(
25 path: impl Into<String>,
26 display_name: impl Into<String>,
27 ) -> Result<Self, ApplicationTargetError> {
28 let path = path.into().trim().to_string();
29 if path.is_empty() {
30 return Err(ApplicationTargetError::EmptyPath);
31 }
32 let requested_name = display_name.into();
33 let display_name = if requested_name.trim().is_empty() {
34 std::path::Path::new(&path)
35 .file_stem()
36 .and_then(std::ffi::OsStr::to_str)
37 .filter(|name| !name.is_empty())
38 .unwrap_or(path.as_str())
39 .to_string()
40 } else {
41 requested_name.trim().to_string()
42 };
43 Ok(Self { path, display_name })
44 }
45
46 #[must_use]
49 pub fn path(&self) -> &str {
50 &self.path
51 }
52
53 #[must_use]
55 pub fn display_name(&self) -> &str {
56 &self.display_name
57 }
58}
59
60#[derive(Clone, Debug, Serialize, Deserialize)]
61#[serde(deny_unknown_fields)]
62struct ApplicationTargetWire {
63 path: String,
64 #[serde(default)]
65 display_name: String,
66}
67
68impl TryFrom<ApplicationTargetWire> for ApplicationTarget {
69 type Error = ApplicationTargetError;
70
71 fn try_from(target: ApplicationTargetWire) -> Result<Self, Self::Error> {
72 Self::new(target.path, target.display_name)
73 }
74}
75
76impl From<ApplicationTarget> for ApplicationTargetWire {
77 fn from(target: ApplicationTarget) -> Self {
78 Self {
79 path: target.path,
80 display_name: target.display_name,
81 }
82 }
83}
84
85#[cfg(test)]
86#[allow(clippy::expect_used, reason = "expect/unwrap are idiomatic in tests")]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn validates_and_derives_display_name() {
92 let target =
93 ApplicationTarget::new("/Applications/Safari.app", "").expect("valid target failed");
94 assert_eq!(target.path(), "/Applications/Safari.app");
95 assert_eq!(target.display_name(), "Safari");
96 assert_eq!(
97 ApplicationTarget::new(" ", "Safari"),
98 Err(ApplicationTargetError::EmptyPath)
99 );
100 }
101
102 #[test]
103 fn roundtrips_as_a_human_readable_target_table() {
104 let target =
105 ApplicationTarget::new("https://example.test", "Example").expect("valid target failed");
106 let encoded = toml::to_string(&target).expect("target serialization failed");
107 assert!(encoded.contains("path = \"https://example.test\""));
108 assert_eq!(toml::from_str::<ApplicationTarget>(&encoded), Ok(target));
109 }
110}