Skip to main content

strop_workspace/
resource.rs

1//! One resolved resource path on one filesystem: the identity routing,
2//! diagnostics, bindings and navigation share (0042). Supersedes
3//! strop-lsp's `DocPath`; the serialized field accepts the legacy
4//! `target` name so records written before the move still decode.
5
6use std::path::PathBuf;
7
8use crate::addr::RemoteEndpoint;
9use crate::filesystem::Filesystem;
10
11/// One resolved path on one filesystem namespace. Always resolved: an
12/// unresolved user entry (a `~` home query) is a
13/// [`crate::addr::RemoteLocation`], never a `ResourceLocation`.
14#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
15pub struct ResourceLocation {
16    #[serde(alias = "target")]
17    pub filesystem: Filesystem,
18    #[serde(with = "strop_core::path_serde")]
19    pub path: PathBuf,
20}
21
22impl ResourceLocation {
23    pub fn local(path: PathBuf) -> Self {
24        Self {
25            filesystem: Filesystem::Local,
26            path,
27        }
28    }
29
30    pub fn remote(endpoint: RemoteEndpoint, path: PathBuf) -> Self {
31        Self {
32            filesystem: Filesystem::Remote(endpoint),
33            path,
34        }
35    }
36
37    /// Modeline/trace form: the bare path locally, `endpoint + path`
38    /// for a remote resource — never an ambiguous local-looking path.
39    pub fn label(&self) -> String {
40        match &self.filesystem {
41            Filesystem::Local => self.path.display().to_string(),
42            other => format!("{}{}", other.label(), self.path.display()),
43        }
44    }
45
46    /// The local path, only when this resource is on the local disk.
47    /// There is no other way to read the path as a local one.
48    pub fn local_path(&self) -> Option<&std::path::Path> {
49        match &self.filesystem {
50            Filesystem::Local => Some(&self.path),
51            _ => None,
52        }
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn decodes_the_legacy_docpath_shape() {
62        // Written by strop-lsp's DocPath (0.19.1): field name `target`.
63        let legacy = serde_json::json!({
64            "target": "Local",
65            "path": "/workspace/a.rs",
66        });
67        let location: ResourceLocation = serde_json::from_value(legacy).unwrap();
68        assert_eq!(location.filesystem, Filesystem::Local);
69        assert_eq!(location.path, PathBuf::from("/workspace/a.rs"));
70        let endpoint = RemoteEndpoint::parse("ssh://user@example.com:2222").unwrap();
71        let legacy = serde_json::json!({
72            "target": { "Remote": endpoint },
73            "path": "/var/log/app.log",
74        });
75        let location: ResourceLocation = serde_json::from_value(legacy).unwrap();
76        assert_eq!(location.filesystem, Filesystem::Remote(endpoint));
77    }
78
79    #[test]
80    fn remote_resources_never_expose_a_local_path() {
81        let endpoint = RemoteEndpoint::parse("ssh://example.com").unwrap();
82        let location = ResourceLocation::remote(endpoint, PathBuf::from("/etc/hostname"));
83        assert_eq!(location.local_path(), None);
84        assert!(location.label().starts_with("ssh://example.com"));
85    }
86}