1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
use std::fmt::Debug;
use std::path::{Path, PathBuf};

use derive_more::{AsRef, Deref};
use resolve_path::PathResolveExt;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

/// This type is a wrapper around a normal [`PathBuf`] that is resolved upon construction. This
/// happens through the [`resolve_path`] crate.
///
/// Additionally, this wrapper preserves the original path as well, and uses the original when
/// serialized using [`serde`]. This makes this wrapper a safe tool when dealing with user provided
/// path. For example either through command line arguments or configuration values.
///
/// Preserving the original allows us to avoid modifying the user configuration when serializing
/// the configuration back to the disk.
#[derive(Deref, AsRef)]
pub struct ResolvedPathBuf {
    #[as_ref]
    #[deref]
    resolved: PathBuf,
    /// If the resolved path is the same as the original we don't store a copy
    /// of the original. This can happen if the original path is already an
    /// absolute path.
    original: Option<PathBuf>,
}

impl TryFrom<PathBuf> for ResolvedPathBuf {
    type Error = std::io::Error;

    fn try_from(original: PathBuf) -> Result<Self, Self::Error> {
        let resolved = original.try_resolve()?.to_path_buf();
        let original = (resolved != original).then_some(original);
        Ok(Self { resolved, original })
    }
}

impl TryFrom<&str> for ResolvedPathBuf {
    type Error = std::io::Error;

    fn try_from(path: &str) -> Result<Self, Self::Error> {
        let original: PathBuf = path.into();
        let resolved = original.try_resolve()?.to_path_buf();
        let original = (resolved != original).then_some(original);
        Ok(Self { resolved, original })
    }
}

impl AsRef<Path> for ResolvedPathBuf {
    fn as_ref(&self) -> &Path {
        self.resolved.as_path()
    }
}

impl Debug for ResolvedPathBuf {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.resolved.fmt(f)
    }
}

impl Serialize for ResolvedPathBuf {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        self.original
            .as_ref()
            .unwrap_or(&self.resolved)
            .serialize(serializer)
    }
}

impl<'de> Deserialize<'de> for ResolvedPathBuf {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let original: PathBuf = PathBuf::deserialize(deserializer)?;
        ResolvedPathBuf::try_from(original)
            .map_err(|e| serde::de::Error::custom(format!("Failed to resolve path: {e}")))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_as_ref() {
        let p = ResolvedPathBuf::try_from(PathBuf::from(r"~/x")).unwrap();
        let x: &PathBuf = p.as_ref();
        assert_eq!(&p.resolved, x);
        assert!(x.is_absolute());
    }

    #[test]
    fn test_deref() {
        let p = ResolvedPathBuf::try_from(PathBuf::from(r"~/x")).unwrap();
        let x: &PathBuf = &p;
        assert_eq!(&p.resolved, x);
        assert!(x.is_absolute());
    }

    #[test]
    fn serde_serialize() {
        let o = PathBuf::from(r"~/x");
        let r = ResolvedPathBuf::try_from(o.clone()).unwrap();

        let o_json = serde_json::to_vec(&o).unwrap();
        let r_json = serde_json::to_vec(&r).unwrap();

        assert_eq!(o_json, r_json);
    }

    #[test]
    fn serde_deserialize() {
        let o = PathBuf::from(r"~/x");
        let o_json = serde_json::to_vec(&o).unwrap();
        let r: ResolvedPathBuf = serde_json::from_slice(&o_json).unwrap();
        assert_eq!(r.original, Some(o));
        assert!(r.is_absolute());
    }
}