Skip to main content

elicitation/verification/types/
pathbufs.rs

1//! PathBuf contract types with runtime filesystem validation.
2
3use super::ValidationError;
4use crate::{ElicitClient, ElicitResult, Elicitation, Prompt};
5use elicitation_macros::instrumented_impl;
6use std::path::PathBuf;
7
8// PathBufExists - Paths that exist on the filesystem
9/// A PathBuf that is guaranteed to exist on the filesystem (runtime check).
10///
11/// **Note:** This is a runtime validation, not compile-time.
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub struct PathBufExists(PathBuf);
14
15#[cfg_attr(not(kani), instrumented_impl)]
16impl PathBufExists {
17    /// Create a new PathBufExists, validating the path exists.
18    pub fn new(path: PathBuf) -> Result<Self, ValidationError> {
19        if path.exists() {
20            Ok(Self(path))
21        } else {
22            Err(ValidationError::PathDoesNotExist(
23                path.display().to_string(),
24            ))
25        }
26    }
27
28    /// Get the inner PathBuf.
29    pub fn get(&self) -> &PathBuf {
30        &self.0
31    }
32
33    /// Unwrap into the inner PathBuf.
34    pub fn into_inner(self) -> PathBuf {
35        self.0
36    }
37}
38
39#[cfg_attr(not(kani), instrumented_impl)]
40impl Prompt for PathBufExists {
41    fn prompt() -> Option<&'static str> {
42        Some("Please provide a path that exists on the filesystem:")
43    }
44}
45
46#[cfg_attr(not(kani), instrumented_impl)]
47impl Elicitation for PathBufExists {
48    type Style = <PathBuf as Elicitation>::Style;
49
50    #[tracing::instrument(skip(client))]
51    async fn elicit(client: &ElicitClient) -> ElicitResult<Self> {
52        tracing::debug!("Eliciting PathBufExists");
53        loop {
54            let path = PathBuf::elicit(client).await?;
55            match Self::new(path) {
56                Ok(valid) => {
57                    tracing::debug!(path = ?valid.0, "Valid existing path");
58                    return Ok(valid);
59                }
60                Err(e) => {
61                    tracing::warn!(error = %e, "Path does not exist, re-prompting");
62                    continue;
63                }
64            }
65        }
66    }
67}
68
69// PathBufReadable - Paths that are readable
70/// A PathBuf that is guaranteed to be readable (runtime check).
71///
72/// **Note:** This is a runtime validation checking metadata access.
73#[derive(Debug, Clone, PartialEq, Eq, Hash)]
74pub struct PathBufReadable(PathBuf);
75
76#[cfg_attr(not(kani), instrumented_impl)]
77impl PathBufReadable {
78    /// Create a new PathBufReadable, validating the path is readable.
79    pub fn new(path: PathBuf) -> Result<Self, ValidationError> {
80        // Try to read metadata as a proxy for readability
81        match path.metadata() {
82            Ok(_) => Ok(Self(path)),
83            Err(_) => Err(ValidationError::PathNotReadable(path.display().to_string())),
84        }
85    }
86
87    /// Get the inner PathBuf.
88    pub fn get(&self) -> &PathBuf {
89        &self.0
90    }
91
92    /// Unwrap into the inner PathBuf.
93    pub fn into_inner(self) -> PathBuf {
94        self.0
95    }
96}
97
98#[cfg_attr(not(kani), instrumented_impl)]
99impl Prompt for PathBufReadable {
100    fn prompt() -> Option<&'static str> {
101        Some("Please provide a readable path:")
102    }
103}
104
105#[cfg_attr(not(kani), instrumented_impl)]
106impl Elicitation for PathBufReadable {
107    type Style = <PathBuf as Elicitation>::Style;
108
109    #[tracing::instrument(skip(client))]
110    async fn elicit(client: &ElicitClient) -> ElicitResult<Self> {
111        tracing::debug!("Eliciting PathBufReadable");
112        loop {
113            let path = PathBuf::elicit(client).await?;
114            match Self::new(path) {
115                Ok(valid) => {
116                    tracing::debug!(path = ?valid.0, "Valid readable path");
117                    return Ok(valid);
118                }
119                Err(e) => {
120                    tracing::warn!(error = %e, "Path not readable, re-prompting");
121                    continue;
122                }
123            }
124        }
125    }
126}
127
128// PathBufIsDir - Paths that are directories
129/// A PathBuf that is guaranteed to be a directory (runtime check).
130///
131/// **Note:** Path must exist for this check to work.
132#[derive(Debug, Clone, PartialEq, Eq, Hash)]
133pub struct PathBufIsDir(PathBuf);
134
135#[cfg_attr(not(kani), instrumented_impl)]
136impl PathBufIsDir {
137    /// Create a new PathBufIsDir, validating the path is a directory.
138    pub fn new(path: PathBuf) -> Result<Self, ValidationError> {
139        if path.is_dir() {
140            Ok(Self(path))
141        } else if path.exists() {
142            Err(ValidationError::PathNotDirectory(
143                path.display().to_string(),
144            ))
145        } else {
146            Err(ValidationError::PathDoesNotExist(
147                path.display().to_string(),
148            ))
149        }
150    }
151
152    /// Get the inner PathBuf.
153    pub fn get(&self) -> &PathBuf {
154        &self.0
155    }
156
157    /// Unwrap into the inner PathBuf.
158    pub fn into_inner(self) -> PathBuf {
159        self.0
160    }
161}
162
163#[cfg_attr(not(kani), instrumented_impl)]
164impl Prompt for PathBufIsDir {
165    fn prompt() -> Option<&'static str> {
166        Some("Please provide a directory path:")
167    }
168}
169
170#[cfg_attr(not(kani), instrumented_impl)]
171impl Elicitation for PathBufIsDir {
172    type Style = <PathBuf as Elicitation>::Style;
173
174    #[tracing::instrument(skip(client))]
175    async fn elicit(client: &ElicitClient) -> ElicitResult<Self> {
176        tracing::debug!("Eliciting PathBufIsDir");
177        loop {
178            let path = PathBuf::elicit(client).await?;
179            match Self::new(path) {
180                Ok(valid) => {
181                    tracing::debug!(path = ?valid.0, "Valid directory path");
182                    return Ok(valid);
183                }
184                Err(e) => {
185                    tracing::warn!(error = %e, "Path not directory, re-prompting");
186                    continue;
187                }
188            }
189        }
190    }
191}
192
193// PathBufIsFile - Paths that are files
194/// A PathBuf that is guaranteed to be a file (runtime check).
195///
196/// **Note:** Path must exist for this check to work.
197#[derive(Debug, Clone, PartialEq, Eq, Hash)]
198pub struct PathBufIsFile(PathBuf);
199
200#[cfg_attr(not(kani), instrumented_impl)]
201impl PathBufIsFile {
202    /// Create a new PathBufIsFile, validating the path is a file.
203    pub fn new(path: PathBuf) -> Result<Self, ValidationError> {
204        if path.is_file() {
205            Ok(Self(path))
206        } else if path.exists() {
207            Err(ValidationError::PathNotFile(path.display().to_string()))
208        } else {
209            Err(ValidationError::PathDoesNotExist(
210                path.display().to_string(),
211            ))
212        }
213    }
214
215    /// Get the inner PathBuf.
216    pub fn get(&self) -> &PathBuf {
217        &self.0
218    }
219
220    /// Unwrap into the inner PathBuf.
221    pub fn into_inner(self) -> PathBuf {
222        self.0
223    }
224}
225
226#[cfg_attr(not(kani), instrumented_impl)]
227impl Prompt for PathBufIsFile {
228    fn prompt() -> Option<&'static str> {
229        Some("Please provide a file path:")
230    }
231}
232
233#[cfg_attr(not(kani), instrumented_impl)]
234impl Elicitation for PathBufIsFile {
235    type Style = <PathBuf as Elicitation>::Style;
236
237    #[tracing::instrument(skip(client))]
238    async fn elicit(client: &ElicitClient) -> ElicitResult<Self> {
239        tracing::debug!("Eliciting PathBufIsFile");
240        loop {
241            let path = PathBuf::elicit(client).await?;
242            match Self::new(path) {
243                Ok(valid) => {
244                    tracing::debug!(path = ?valid.0, "Valid file path");
245                    return Ok(valid);
246                }
247                Err(e) => {
248                    tracing::warn!(error = %e, "Path not file, re-prompting");
249                    continue;
250                }
251            }
252        }
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    use std::env;
260
261    #[test]
262    fn test_path_exists_valid() {
263        // Use Cargo.toml (guaranteed to exist in project root)
264        let mut path = env::current_dir().unwrap();
265        path.push("Cargo.toml");
266        let result = PathBufExists::new(path);
267        assert!(result.is_ok());
268    }
269
270    #[test]
271    fn test_path_exists_invalid() {
272        let path = PathBuf::from("/this/path/does/not/exist/hopefully");
273        let result = PathBufExists::new(path);
274        assert!(result.is_err());
275    }
276
277    #[test]
278    fn test_path_readable_valid() {
279        let mut path = env::current_dir().unwrap();
280        path.push("Cargo.toml");
281        let result = PathBufReadable::new(path);
282        assert!(result.is_ok());
283    }
284
285    #[test]
286    fn test_path_is_dir_valid() {
287        // Use src directory
288        let mut path = env::current_dir().unwrap();
289        path.push("src");
290        let result = PathBufIsDir::new(path);
291        assert!(result.is_ok());
292    }
293
294    #[test]
295    fn test_path_is_dir_file() {
296        let mut path = env::current_dir().unwrap();
297        path.push("Cargo.toml");
298        let result = PathBufIsDir::new(path);
299        assert!(result.is_err());
300    }
301
302    #[test]
303    fn test_path_is_file_valid() {
304        let mut path = env::current_dir().unwrap();
305        path.push("Cargo.toml");
306        let result = PathBufIsFile::new(path);
307        assert!(result.is_ok());
308    }
309
310    #[test]
311    fn test_path_is_file_dir() {
312        let mut path = env::current_dir().unwrap();
313        path.push("src");
314        let result = PathBufIsFile::new(path);
315        assert!(result.is_err());
316    }
317
318    #[test]
319    fn test_path_into_inner() {
320        let mut original = env::current_dir().unwrap();
321        original.push("Cargo.toml");
322        let exists = PathBufExists::new(original.clone()).unwrap();
323        assert_eq!(exists.into_inner(), original);
324    }
325}