joat_git_url/
lib.rs

1// Copyright (c) 2020-3 Richard Cook
2//
3// Permission is hereby granted, free of charge, to any person obtaining
4// a copy of this software and associated documentation files (the
5// "Software"), to deal in the Software without restriction, including
6// without limitation the rights to use, copy, modify, merge, publish,
7// distribute, sublicense, and/or sell copies of the Software, and to
8// permit persons to whom the Software is furnished to do so, subject to
9// the following conditions:
10//
11// The above copyright notice and this permission notice shall be
12// included in all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21//
22#![warn(clippy::all)]
23#![warn(clippy::cargo)]
24//#![warn(clippy::expect_used)]
25#![warn(clippy::nursery)]
26//#![warn(clippy::panic_in_result_fn)]
27#![warn(clippy::pedantic)]
28#![allow(clippy::derive_partial_eq_without_eq)]
29#![allow(clippy::enum_glob_use)]
30#![allow(clippy::match_wildcard_for_single_variants)]
31#![allow(clippy::missing_errors_doc)]
32#![allow(clippy::module_name_repetitions)]
33#![allow(clippy::option_if_let_else)]
34use std::error::Error as StdError;
35use std::fmt::{Display, Formatter, Result as FmtResult};
36use std::str::FromStr;
37
38#[derive(Debug)]
39pub struct ParseGitUrlError(String);
40
41impl Display for ParseGitUrlError {
42    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
43        f.write_str(&self.0)
44    }
45}
46
47impl StdError for ParseGitUrlError {}
48
49#[derive(Clone)]
50pub struct GitUrl {
51    host: String,
52    path: String,
53}
54
55impl GitUrl {
56    const HTTP_PREFIX: &'static str = "http://";
57    const HTTPS_PREFIX: &'static str = "https://";
58
59    #[allow(dead_code)]
60    #[must_use]
61    pub fn pop(&self) -> Option<Self> {
62        let mut temp = self.clone();
63        if temp.pop_mut() {
64            Some(temp)
65        } else {
66            None
67        }
68    }
69
70    #[allow(dead_code)]
71    pub fn pop_mut(&mut self) -> bool {
72        Self::pop_helper(&mut self.path)
73    }
74
75    #[allow(dead_code)]
76    #[must_use]
77    pub fn join(&self, child_path: &str) -> Option<Self> {
78        let mut temp = self.clone();
79        if temp.join_mut(child_path) {
80            Some(temp)
81        } else {
82            None
83        }
84    }
85
86    #[allow(dead_code)]
87    pub fn join_mut(&mut self, child_path: &str) -> bool {
88        let mut path = self.path.clone();
89        for part in child_path.split('/') {
90            if part.is_empty() {
91                return false;
92            } else if part == ".." {
93                if !Self::pop_helper(&mut path) {
94                    return false;
95                }
96            } else if part != "." {
97                if !path.is_empty() {
98                    path += "/";
99                }
100                path += part;
101            }
102        }
103        self.path = path;
104        true
105    }
106
107    fn pop_helper(path: &mut String) -> bool {
108        if path.is_empty() {
109            false
110        } else {
111            match path.rfind('/') {
112                Some(pos) => path.truncate(pos),
113                None => path.clear(),
114            }
115            true
116        }
117    }
118}
119
120impl FromStr for GitUrl {
121    type Err = ParseGitUrlError;
122
123    #[allow(clippy::manual_strip)]
124    fn from_str(s: &str) -> Result<Self, Self::Err> {
125        let opt = if s.starts_with(Self::HTTP_PREFIX) {
126            s[Self::HTTP_PREFIX.len()..].find('/').map(|p| Self {
127                host: s[..Self::HTTP_PREFIX.len() + p].to_string(),
128                path: s[Self::HTTP_PREFIX.len() + p + 1..].to_string(),
129            })
130        } else if s.starts_with(Self::HTTPS_PREFIX) {
131            s[Self::HTTPS_PREFIX.len()..].find('/').map(|p| Self {
132                host: s[..Self::HTTPS_PREFIX.len() + p].to_string(),
133                path: s[Self::HTTPS_PREFIX.len() + p + 1..].to_string(),
134            })
135        } else {
136            s.find(':').map(|p| Self {
137                host: s[..p].to_string(),
138                path: s[p + 1..].to_string(),
139            })
140        };
141        opt.ok_or_else(|| ParseGitUrlError(String::from(s)))
142    }
143}
144
145impl Display for GitUrl {
146    fn fmt(&self, f: &mut Formatter) -> FmtResult {
147        write!(
148            f,
149            "{}",
150            match self.path.len() {
151                0 => self.host.to_string(),
152                _ => self.host.to_string() + ":" + &self.path,
153            }
154        )
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::{GitUrl, ParseGitUrlError};
161    use std::result::Result as StdResult;
162
163    #[test]
164    fn test_pop_https() -> StdResult<(), ParseGitUrlError> {
165        let x0 = "https://github.com/user/foo/bar/quux.git".parse::<GitUrl>()?;
166        assert_eq!(x0.host, "https://github.com");
167        assert_eq!(x0.path, "user/foo/bar/quux.git");
168
169        let x1 = "http://github.com/user/foo/bar/quux.git".parse::<GitUrl>()?;
170        assert_eq!(x1.host, "http://github.com");
171        assert_eq!(x1.path, "user/foo/bar/quux.git");
172
173        let x2 = "git@github.com:user/foo/bar/quux.git".parse::<GitUrl>()?;
174        assert_eq!(x2.host, "git@github.com");
175        assert_eq!(x2.path, "user/foo/bar/quux.git");
176
177        Ok(())
178    }
179
180    #[test]
181    fn test_pop() -> StdResult<(), ParseGitUrlError> {
182        let x0 = "git@github.com:user/foo/bar/quux.git".parse::<GitUrl>()?;
183
184        assert_eq!(x0.host, "git@github.com");
185        assert_eq!(x0.path, "user/foo/bar/quux.git");
186        assert_eq!(x0.to_string(), "git@github.com:user/foo/bar/quux.git");
187
188        let x1 = x0.pop().expect("pop failed");
189        assert_eq!(x1.host, "git@github.com");
190        assert_eq!(x1.path, "user/foo/bar");
191        assert_eq!(x1.to_string(), "git@github.com:user/foo/bar");
192
193        let x2 = x1.pop().expect("pop failed");
194        assert_eq!(x2.host, "git@github.com");
195        assert_eq!(x2.path, "user/foo");
196        assert_eq!(x2.to_string(), "git@github.com:user/foo");
197
198        let x3 = x2.pop().expect("pop failed");
199        assert_eq!(x3.host, "git@github.com");
200        assert_eq!(x3.path, "user");
201        assert_eq!(x3.to_string(), "git@github.com:user");
202
203        let x4 = x3.pop().expect("pop failed");
204        assert_eq!(x4.host, "git@github.com");
205        assert_eq!(x4.path, "");
206        assert_eq!(x4.to_string(), "git@github.com");
207
208        assert!(x4.pop().is_none());
209
210        Ok(())
211    }
212
213    #[test]
214    fn test_pop_mut() -> StdResult<(), ParseGitUrlError> {
215        let mut git_url = "git@github.com:user/foo/bar/quux.git".parse::<GitUrl>()?;
216
217        assert_eq!(git_url.host, "git@github.com");
218        assert_eq!(git_url.path, "user/foo/bar/quux.git");
219        assert_eq!(git_url.to_string(), "git@github.com:user/foo/bar/quux.git");
220
221        assert!(git_url.pop_mut());
222        assert_eq!(git_url.host, "git@github.com");
223        assert_eq!(git_url.path, "user/foo/bar");
224        assert_eq!(git_url.to_string(), "git@github.com:user/foo/bar");
225
226        assert!(git_url.pop_mut());
227        assert_eq!(git_url.host, "git@github.com");
228        assert_eq!(git_url.path, "user/foo");
229        assert_eq!(git_url.to_string(), "git@github.com:user/foo");
230
231        assert!(git_url.pop_mut());
232        assert_eq!(git_url.host, "git@github.com");
233        assert_eq!(git_url.path, "user");
234        assert_eq!(git_url.to_string(), "git@github.com:user");
235
236        assert!(git_url.pop_mut());
237        assert_eq!(git_url.host, "git@github.com");
238        assert_eq!(git_url.path, "");
239        assert_eq!(git_url.to_string(), "git@github.com");
240
241        assert!(!git_url.pop_mut());
242        assert_eq!(git_url.host, "git@github.com");
243        assert_eq!(git_url.path, "");
244        assert_eq!(git_url.to_string(), "git@github.com");
245
246        Ok(())
247    }
248
249    #[test]
250    fn test_join() -> StdResult<(), ParseGitUrlError> {
251        let git_url = "git@github.com:user/foo/bar/quux.git".parse::<GitUrl>()?;
252
253        assert_eq!(
254            git_url.join("aaa").expect("join failed").to_string(),
255            "git@github.com:user/foo/bar/quux.git/aaa"
256        );
257
258        assert_eq!(
259            git_url.join("aaa/bbb").expect("join failed").to_string(),
260            "git@github.com:user/foo/bar/quux.git/aaa/bbb"
261        );
262
263        assert_eq!(
264            git_url.join(".").expect("join failed").to_string(),
265            "git@github.com:user/foo/bar/quux.git"
266        );
267
268        assert_eq!(
269            git_url.join("..").expect("join failed").to_string(),
270            "git@github.com:user/foo/bar"
271        );
272
273        assert_eq!(
274            git_url.join("../aaa").expect("join failed").to_string(),
275            "git@github.com:user/foo/bar/aaa"
276        );
277
278        assert_eq!(
279            git_url.join("../aaa/bbb").expect("join failed").to_string(),
280            "git@github.com:user/foo/bar/aaa/bbb"
281        );
282
283        assert_eq!(
284            git_url
285                .join("../../../aaa/bbb")
286                .expect("join failed")
287                .to_string(),
288            "git@github.com:user/aaa/bbb"
289        );
290
291        assert_eq!(
292            git_url
293                .join("../../../../aaa/bbb")
294                .expect("join failed")
295                .to_string(),
296            "git@github.com:aaa/bbb"
297        );
298
299        assert!(git_url.join("/aaa").is_none());
300
301        Ok(())
302    }
303
304    #[test]
305    fn test_join_mut() -> StdResult<(), ParseGitUrlError> {
306        {
307            let mut git_url = "git@github.com:user/foo/bar/quux.git".parse::<GitUrl>()?;
308            assert!(git_url.join_mut("aaa"));
309            assert_eq!(
310                git_url.to_string(),
311                "git@github.com:user/foo/bar/quux.git/aaa"
312            );
313        }
314
315        {
316            let mut git_url = "git@github.com:user/foo/bar/quux.git".parse::<GitUrl>()?;
317            assert!(git_url.join_mut("aaa/bbb"));
318            assert_eq!(
319                git_url.to_string(),
320                "git@github.com:user/foo/bar/quux.git/aaa/bbb"
321            );
322        }
323
324        {
325            let mut git_url = "git@github.com:user/foo/bar/quux.git".parse::<GitUrl>()?;
326            assert!(git_url.join_mut("."));
327            assert_eq!(git_url.to_string(), "git@github.com:user/foo/bar/quux.git");
328        }
329
330        {
331            let mut git_url = "git@github.com:user/foo/bar/quux.git".parse::<GitUrl>()?;
332            assert!(git_url.join_mut(".."));
333            assert_eq!(git_url.to_string(), "git@github.com:user/foo/bar");
334        }
335
336        {
337            let mut git_url = "git@github.com:user/foo/bar/quux.git".parse::<GitUrl>()?;
338            assert!(git_url.join_mut("../aaa"));
339            assert_eq!(git_url.to_string(), "git@github.com:user/foo/bar/aaa");
340        }
341
342        {
343            let mut git_url = "git@github.com:user/foo/bar/quux.git".parse::<GitUrl>()?;
344            assert!(git_url.join_mut("../aaa/bbb"));
345            assert_eq!(git_url.to_string(), "git@github.com:user/foo/bar/aaa/bbb");
346        }
347
348        {
349            let mut git_url = "git@github.com:user/foo/bar/quux.git".parse::<GitUrl>()?;
350            assert!(git_url.join_mut("../../../aaa/bbb"));
351            assert_eq!(git_url.to_string(), "git@github.com:user/aaa/bbb");
352        }
353
354        {
355            let mut git_url = "git@github.com:user/foo/bar/quux.git".parse::<GitUrl>()?;
356            assert!(git_url.join_mut("../../../../aaa/bbb"));
357            assert_eq!(git_url.to_string(), "git@github.com:aaa/bbb");
358        }
359
360        {
361            let mut git_url = "git@github.com:user/foo/bar/quux.git".parse::<GitUrl>()?;
362            assert!(!git_url.join_mut("/aaa"));
363        }
364
365        Ok(())
366    }
367}