spec_driven_docs/landing/
path.rs1use camino::{Utf8Path, Utf8PathBuf};
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
13#[serde(try_from = "String", into = "String")]
14pub struct TargetPath(Utf8PathBuf);
15
16#[derive(Debug, Clone, PartialEq, Eq, Error)]
18#[error("{0}")]
19pub struct TargetPathError(String);
20
21impl TargetPath {
22 pub fn new(value: &str) -> Result<Self, TargetPathError> {
31 let refuse = |why: &str| TargetPathError(format!("{value}: {why}"));
32 if value.is_empty() {
33 return Err(refuse("the path is empty"));
34 }
35 if value.contains('\0') {
36 return Err(refuse("the path carries a NUL"));
37 }
38 let path = Utf8Path::new(value);
39 if path.is_absolute() {
40 return Err(refuse("the path is absolute"));
41 }
42 let mut normalized = Utf8PathBuf::new();
43 for component in path.components() {
44 match component {
45 camino::Utf8Component::Normal(part) => normalized.push(part),
46 camino::Utf8Component::CurDir => {}
47 camino::Utf8Component::ParentDir => {
48 return Err(refuse("the path climbs out of the target"));
49 }
50 camino::Utf8Component::RootDir | camino::Utf8Component::Prefix(_) => {
51 return Err(refuse("the path is absolute"));
52 }
53 }
54 }
55 if normalized.as_str().is_empty() {
56 return Err(refuse("the path names no file"));
57 }
58 Ok(Self(normalized))
59 }
60
61 #[must_use]
63 pub fn as_path(&self) -> &Utf8Path {
64 &self.0
65 }
66
67 #[must_use]
69 pub fn as_str(&self) -> &str {
70 self.0.as_str()
71 }
72}
73
74impl TryFrom<String> for TargetPath {
75 type Error = TargetPathError;
76
77 fn try_from(value: String) -> Result<Self, Self::Error> {
78 Self::new(&value)
79 }
80}
81
82impl From<TargetPath> for String {
83 fn from(value: TargetPath) -> Self {
84 value.0.into_string()
85 }
86}
87
88impl std::fmt::Display for TargetPath {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 f.write_str(self.0.as_str())
91 }
92}