Skip to main content

zenops_safe_relative_path/
single_path_component.rs

1use std::fmt;
2
3use crate::{SafeRelativePath, error::Error};
4use serde::de;
5use smol_str::{SmolStr, ToSmolStr};
6
7/// A path that is exactly one segment — no separators, no traversal.
8///
9/// Narrower than [`SafeRelativePath`]: where `SafeRelativePath` allows any
10/// number of components as long as none of them are `..`,
11/// `SinglePathComponent` permits exactly one. Reach for it when a value
12/// has to be a single name in a flat namespace — a package key, a
13/// configuration map key, a directory entry — and you want the type
14/// system to enforce that.
15///
16/// [`Deref`]s to [`SafeRelativePath`], so a `SinglePathComponent` can be
17/// handed to anything that takes `&SafeRelativePath` without conversion.
18///
19/// # Example
20///
21/// ```
22/// use zenops_safe_relative_path::SinglePathComponent;
23///
24/// assert!(SinglePathComponent::try_new("zsh").is_ok());
25///
26/// // More than one component — rejected.
27/// assert!(SinglePathComponent::try_new("zsh/init.sh").is_err());
28/// // Traversal — also rejected.
29/// assert!(SinglePathComponent::try_new("..").is_err());
30/// ```
31///
32/// [`Deref`]: std::ops::Deref
33#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
34pub struct SinglePathComponent(SmolStr);
35
36impl SinglePathComponent {
37    /// Try to wrap a string as a [`SinglePathComponent`].
38    ///
39    /// Fails on anything containing `/`, anything with `..` traversal, and
40    /// the empty string.
41    pub fn try_new(v: &str) -> Result<Self, Error> {
42        let path = SafeRelativePath::from_relative_path(v)?;
43        let first = path.0.components().map(|c| c.as_str()).next();
44        if first == Some(v) {
45            Ok(Self(v.to_smolstr()))
46        } else {
47            Err(Error::NotASinglePathComponent(v.to_string()))
48        }
49    }
50
51    /// View this component as a [`SafeRelativePath`].
52    ///
53    /// `SinglePathComponent` already [`Deref`]s to [`SafeRelativePath`], so
54    /// most call sites don't need this directly — it's exposed for places
55    /// where an explicit conversion reads more clearly than a reborrow.
56    ///
57    /// [`Deref`]: std::ops::Deref
58    pub fn as_safe_relative_path(&self) -> &SafeRelativePath {
59        unsafe { SafeRelativePath::new_unchecked_from_str(self.0.as_str()) }
60    }
61}
62
63impl AsRef<SafeRelativePath> for SinglePathComponent {
64    fn as_ref(&self) -> &SafeRelativePath {
65        self.as_safe_relative_path()
66    }
67}
68
69impl fmt::Display for SinglePathComponent {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        fmt::Display::fmt(&self.0, f)
72    }
73}
74
75impl std::ops::Deref for SinglePathComponent {
76    type Target = SafeRelativePath;
77
78    fn deref(&self) -> &Self::Target {
79        self.as_safe_relative_path()
80    }
81}
82
83#[cfg(feature = "schemars")]
84impl schemars::JsonSchema for SinglePathComponent {
85    fn schema_name() -> std::borrow::Cow<'static, str> {
86        "SinglePathComponent".into()
87    }
88
89    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
90        schemars::json_schema!({
91            "type": "string",
92            "description": "A single path component — no separators, no `..` traversal.",
93            "pattern": "^[^/]+$",
94        })
95    }
96}
97
98impl<'de> de::Deserialize<'de> for SinglePathComponent {
99    fn deserialize<D: de::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
100        struct Visitor;
101
102        impl<'de> de::Visitor<'de> for Visitor {
103            type Value = SinglePathComponent;
104
105            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
106                write!(f, "single path component")
107            }
108
109            fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
110                SinglePathComponent::try_new(v).map_err(de::Error::custom)
111            }
112        }
113
114        d.deserialize_any(Visitor)
115    }
116}