1use crate::{assert_exists, assert_is_dir, assert_not_dir, ClioPath, Error, Result};
7use clap::builder::TypedValueParser;
8use clap::error::ErrorKind;
9use std::ffi::OsStr;
10use std::marker::PhantomData;
11
12#[derive(Copy, Clone, Debug)]
14pub struct OsStrParser<T> {
15 exists: Option<bool>,
16 is_dir: Option<bool>,
17 is_file: Option<bool>,
18 is_tty: Option<bool>,
19 atomic: bool,
20 default_name: Option<&'static str>,
21 phantom: PhantomData<T>,
22}
23
24impl<T> OsStrParser<T> {
25 pub(crate) fn new() -> Self {
26 OsStrParser {
27 exists: None,
28 is_dir: None,
29 is_file: None,
30 is_tty: None,
31 default_name: None,
32 atomic: false,
33 phantom: PhantomData,
34 }
35 }
36
37 pub fn exists(mut self) -> Self {
39 self.exists = Some(true);
40 self
41 }
42
43 pub fn is_dir(mut self) -> Self {
45 self.is_dir = Some(true);
46 self.is_file = None;
47 self
48 }
49
50 pub fn is_file(mut self) -> Self {
52 self.is_dir = None;
53 self.is_file = Some(true);
54 self
55 }
56
57 pub fn not_tty(mut self) -> Self {
59 self.is_tty = Some(false);
60 self
61 }
62
63 pub fn atomic(mut self) -> Self {
66 self.atomic = true;
67 self
68 }
69
70 pub fn default_name(mut self, name: &'static str) -> Self {
72 self.default_name = Some(name);
73 self
74 }
75
76 fn validate(&self, value: &OsStr) -> Result<ClioPath> {
77 let mut path = ClioPath::new(value)?;
78 path.atomic = self.atomic;
79 if path.is_local() {
80 if let Some(name) = self.default_name {
81 if path.is_dir() || path.ends_with_slash() {
82 path.push(name)
83 }
84 }
85 if self.is_dir == Some(true) && path.exists() {
86 assert_is_dir(&path)?;
87 }
88 if self.is_file == Some(true) {
89 assert_not_dir(&path)?;
90 }
91 if self.exists == Some(true) {
92 assert_exists(&path)?;
93 }
94 } else if self.is_dir == Some(true) {
95 return Err(Error::not_dir_error());
96 } else if self.is_tty == Some(false) && path.is_tty() {
97 return Err(Error::other(
98 "blocked reading from stdin because it is a tty",
99 ));
100 }
101 Ok(path)
102 }
103}
104
105impl<T> TypedValueParser for OsStrParser<T>
106where
107 for<'a> T: TryFrom<ClioPath, Error = crate::Error>,
108 T: Clone + Sync + Send + 'static,
109{
110 type Value = T;
111
112 fn parse_ref(
113 &self,
114 cmd: &clap::Command,
115 arg: Option<&clap::Arg>,
116 value: &OsStr,
117 ) -> core::result::Result<Self::Value, clap::Error> {
118 self.validate(value).and_then(T::try_from).map_err(|orig| {
119 cmd.clone().error(
120 ErrorKind::InvalidValue,
121 if let Some(arg) = arg {
122 format!(
123 "Invalid value for {}: Could not open {:?}: {}",
124 arg, value, orig
125 )
126 } else {
127 format!("Could not open {:?}: {}", value, orig)
128 },
129 )
130 })
131 }
132}
133
134impl TypedValueParser for OsStrParser<ClioPath> {
135 type Value = ClioPath;
136 fn parse_ref(
137 &self,
138 cmd: &clap::Command,
139 arg: Option<&clap::Arg>,
140 value: &OsStr,
141 ) -> core::result::Result<Self::Value, clap::Error> {
142 self.validate(value).map_err(|orig| {
143 cmd.clone().error(
144 ErrorKind::InvalidValue,
145 if let Some(arg) = arg {
146 format!(
147 "Invalid value for {}: Invalid path {:?}: {}",
148 arg, value, orig
149 )
150 } else {
151 format!("Invalid path {:?}: {}", value, orig)
152 },
153 )
154 })
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use std::fs::{create_dir, write};
162 use tempfile::{tempdir, TempDir};
163
164 fn temp() -> TempDir {
165 let tmp = tempdir().expect("could not make tmp dir");
166 create_dir(&tmp.path().join("dir")).expect("could not create dir");
167 write(&tmp.path().join("file"), "contents").expect("could not create dir");
168 tmp
169 }
170
171 #[test]
172 fn test_path_exists() {
173 let tmp = temp();
174 let validator = OsStrParser::<ClioPath>::new().exists();
175 validator
176 .validate(tmp.path().join("file").as_os_str())
177 .unwrap();
178 validator
179 .validate(tmp.path().join("dir").as_os_str())
180 .unwrap();
181 validator
182 .validate(tmp.path().join("dir/").as_os_str())
183 .unwrap();
184
185 assert!(validator
186 .validate(tmp.path().join("dir/missing").as_os_str())
187 .is_err());
188 }
189
190 #[test]
191 fn test_path_is_file() {
192 let tmp = temp();
193 let validator = OsStrParser::<ClioPath>::new().is_file();
194 validator
195 .validate(tmp.path().join("file").as_os_str())
196 .unwrap();
197 validator
198 .validate(tmp.path().join("dir/missing").as_os_str())
199 .unwrap();
200 validator.validate(OsStr::new("-")).unwrap();
201 assert!(validator
202 .validate(tmp.path().join("dir/").as_os_str())
203 .is_err());
204 assert!(validator
205 .validate(tmp.path().join("missing-dir/").as_os_str())
206 .is_err());
207 }
208
209 #[test]
210 fn test_path_is_existing_file() {
211 let tmp = temp();
212 let validator = OsStrParser::<ClioPath>::new().exists().is_file();
213 validator
214 .validate(tmp.path().join("file").as_os_str())
215 .unwrap();
216 assert!(validator
217 .validate(tmp.path().join("dir/missing").as_os_str())
218 .is_err());
219 assert!(validator
220 .validate(tmp.path().join("dir/").as_os_str())
221 .is_err());
222 }
223
224 #[test]
225 fn test_path_is_dir() {
226 let tmp = temp();
227 let validator = OsStrParser::<ClioPath>::new().is_dir();
228 validator
229 .validate(tmp.path().join("dir").as_os_str())
230 .unwrap();
231 validator
232 .validate(tmp.path().join("dir/missing").as_os_str())
233 .unwrap();
234 assert!(validator
235 .validate(tmp.path().join("file").as_os_str())
236 .is_err());
237 assert!(validator.validate(OsStr::new("-")).is_err());
238 }
239
240 #[test]
241 fn test_default_name() {
242 let tmp = temp();
243 let validator = OsStrParser::<ClioPath>::new().default_name("default.txt");
244 assert_eq!(
245 validator
246 .validate(tmp.path().join("dir").as_os_str())
247 .unwrap()
248 .file_name()
249 .unwrap(),
250 "default.txt"
251 );
252 assert_eq!(
253 validator
254 .validate(tmp.path().join("dir/file").as_os_str())
255 .unwrap()
256 .file_name()
257 .unwrap(),
258 "file"
259 );
260 assert_eq!(
261 validator
262 .validate(tmp.path().join("missing-dir/").as_os_str())
263 .unwrap()
264 .file_name()
265 .unwrap(),
266 "default.txt"
267 );
268 }
269}