Skip to main content

nickel_lang_package/index/
path.rs

1//! Path utilities for the package index.
2
3use std::path::{Path, PathBuf};
4
5use nickel_lang_core::cache::normalize_rel_path;
6
7/// A relative path that has no "parent" components and is convertible to UTF-8.
8///
9/// The UTF-8 requirement comes from the fact that these paths need to be expressible
10/// in Nickel package manifests, which are always UTF-8.
11#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, Default)]
12#[serde(transparent)]
13pub struct RelativePath {
14    inner: String,
15}
16
17impl std::fmt::Display for RelativePath {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        self.inner.fmt(f)
20    }
21}
22
23impl RelativePath {
24    /// Is this an empty path?
25    pub fn is_empty(&self) -> bool {
26        self.inner.is_empty()
27    }
28
29    /// Iterate over the path components.
30    pub fn components(&self) -> impl Iterator<Item = &str> + '_ {
31        self.as_ref().components().map(|c| {
32            match c {
33                // unwrap: RelativePath is always UTF-8.
34                std::path::Component::Normal(os_str) => os_str.to_str().unwrap(),
35                // We're a normalized, relative path so we only have normal components.
36                _ => unreachable!(),
37            }
38        })
39    }
40}
41
42impl<'de> serde::Deserialize<'de> for RelativePath {
43    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
44    where
45        D: serde::Deserializer<'de>,
46    {
47        let path = PathBuf::deserialize(deserializer)?;
48        path.try_into().map_err(serde::de::Error::custom)
49    }
50}
51
52impl AsRef<Path> for RelativePath {
53    fn as_ref(&self) -> &Path {
54        self.inner.as_ref()
55    }
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, thiserror::Error)]
59pub enum RelativePathErrorKind {
60    /// The path was an absolute path, where a relative one was expected.
61    #[error("absolute path")]
62    Absolute,
63    /// The path pointed to something outside of the containing directory.
64    #[error("points outside its parent")]
65    OutOfBounds,
66    /// The path contains invalid UTF-8.
67    #[error("invalid UTF-8")]
68    InvalidUtf8,
69}
70
71#[derive(Clone, Debug, PartialEq, Eq, Hash, thiserror::Error)]
72#[error("could not convert {path} to a relative path: {kind}")]
73pub struct RelativePathError {
74    /// The path that could not be converted to a relative path.
75    pub path: PathBuf,
76    /// The reason the conversion failed.
77    pub kind: RelativePathErrorKind,
78}
79
80impl TryFrom<PathBuf> for RelativePath {
81    type Error = RelativePathError;
82
83    fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
84        if path.is_absolute() {
85            Err(RelativePathError {
86                path,
87                kind: RelativePathErrorKind::Absolute,
88            })
89        } else {
90            let normalized = normalize_rel_path(&path);
91            if normalized.components().next() == Some(std::path::Component::ParentDir) {
92                Err(RelativePathError {
93                    path,
94                    kind: RelativePathErrorKind::OutOfBounds,
95                })
96            } else {
97                let inner =
98                    normalized
99                        .into_os_string()
100                        .into_string()
101                        .map_err(|s| RelativePathError {
102                            path: s.into(),
103                            kind: RelativePathErrorKind::InvalidUtf8,
104                        })?;
105                Ok(RelativePath { inner })
106            }
107        }
108    }
109}