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}