Skip to main content

vld/primitives/
path.rs

1use serde_json::Value;
2use std::path::PathBuf;
3
4use crate::error::{value_type_name, IssueCode, VldError};
5use crate::schema::VldSchema;
6
7#[derive(Clone)]
8pub struct ZPath {
9    must_exist: bool,
10    must_be_file: bool,
11    must_be_dir: bool,
12    must_be_absolute: bool,
13    must_be_relative: bool,
14    within_base: Option<PathBuf>,
15    custom_type_error: Option<String>,
16}
17
18impl ZPath {
19    pub fn new() -> Self {
20        Self {
21            must_exist: false,
22            must_be_file: false,
23            must_be_dir: false,
24            must_be_absolute: false,
25            must_be_relative: false,
26            within_base: None,
27            custom_type_error: None,
28        }
29    }
30
31    pub fn type_error(mut self, msg: impl Into<String>) -> Self {
32        self.custom_type_error = Some(msg.into());
33        self
34    }
35
36    pub fn exists(mut self) -> Self {
37        self.must_exist = true;
38        self
39    }
40
41    pub fn file(mut self) -> Self {
42        self.must_be_file = true;
43        self.must_exist = true;
44        self
45    }
46
47    pub fn dir(mut self) -> Self {
48        self.must_be_dir = true;
49        self.must_exist = true;
50        self
51    }
52
53    pub fn absolute(mut self) -> Self {
54        self.must_be_absolute = true;
55        self
56    }
57
58    pub fn relative(mut self) -> Self {
59        self.must_be_relative = true;
60        self
61    }
62
63    pub fn within(mut self, base: impl Into<PathBuf>) -> Self {
64        self.within_base = Some(base.into());
65        self
66    }
67
68    #[cfg(feature = "openapi")]
69    pub fn to_json_schema(&self) -> serde_json::Value {
70        serde_json::json!({
71            "type": "string",
72            "format": "path"
73        })
74    }
75}
76
77impl Default for ZPath {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83impl VldSchema for ZPath {
84    type Output = PathBuf;
85
86    fn parse_value(&self, value: &Value) -> Result<PathBuf, VldError> {
87        let s = value.as_str().ok_or_else(|| {
88            let msg = self.custom_type_error.clone().unwrap_or_else(|| {
89                format!("Expected path string, received {}", value_type_name(value))
90            });
91            VldError::single_with_value(
92                IssueCode::InvalidType {
93                    expected: "string (path)".to_string(),
94                    received: value_type_name(value),
95                },
96                msg,
97                value,
98            )
99        })?;
100        let p = PathBuf::from(s);
101
102        if self.must_be_absolute && !p.is_absolute() {
103            return Err(VldError::single_with_value(
104                IssueCode::Custom {
105                    code: "path_not_absolute".to_string(),
106                },
107                "Path must be absolute",
108                value,
109            ));
110        }
111        if self.must_be_relative && p.is_absolute() {
112            return Err(VldError::single_with_value(
113                IssueCode::Custom {
114                    code: "path_not_relative".to_string(),
115                },
116                "Path must be relative",
117                value,
118            ));
119        }
120        if self.must_exist && !p.exists() {
121            return Err(VldError::single_with_value(
122                IssueCode::IoError,
123                format!("Path does not exist: {}", p.display()),
124                value,
125            ));
126        }
127        if self.must_be_file && !p.is_file() {
128            return Err(VldError::single_with_value(
129                IssueCode::IoError,
130                format!("Path is not a file: {}", p.display()),
131                value,
132            ));
133        }
134        if self.must_be_dir && !p.is_dir() {
135            return Err(VldError::single_with_value(
136                IssueCode::IoError,
137                format!("Path is not a directory: {}", p.display()),
138                value,
139            ));
140        }
141        if let Some(base) = &self.within_base {
142            let joined = if p.is_absolute() {
143                p.clone()
144            } else {
145                base.join(&p)
146            };
147            let joined_canon = joined.canonicalize().unwrap_or(joined);
148            let base_canon = base.canonicalize().unwrap_or_else(|_| base.clone());
149            if !joined_canon.starts_with(&base_canon) {
150                return Err(VldError::single_with_value(
151                    IssueCode::Custom {
152                        code: "path_outside_base".to_string(),
153                    },
154                    format!("Path must be within base directory: {}", base.display()),
155                    value,
156                ));
157            }
158        }
159        Ok(p)
160    }
161}