nix_uri/flakeref/
forge.rs

1use std::fmt::Display;
2
3use nom::{
4    IResult, Parser,
5    branch::alt,
6    bytes::complete::take_till1,
7    character::complete::char,
8    combinator::{cut, opt, value},
9    error::context,
10    sequence::{preceded, separated_pair, terminated},
11};
12use serde::{Deserialize, Serialize};
13
14use crate::{IErr, error::tag};
15
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17pub enum GitForgePlatform {
18    GitHub,
19    GitLab,
20    SourceHut,
21}
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
23pub struct GitForge {
24    pub platform: GitForgePlatform,
25    pub owner: String,
26    pub repo: String,
27    pub ref_or_rev: Option<String>,
28}
29
30impl GitForgePlatform {
31    /// `nom`s the gitforge + `:`
32    /// `"<github|gitlab|sourceforge>:foobar..."` -> `(foobar..., GitForge)`
33    pub fn parse(input: &str) -> IResult<&str, Self, IErr<&str>> {
34        alt((
35            value(Self::GitHub, tag("github")),
36            value(Self::GitLab, tag("gitlab")),
37            value(Self::SourceHut, tag("sourcehut")),
38        ))
39        .parse(input)
40    }
41    pub fn parse_terminated(input: &str) -> IResult<&str, Self, IErr<&str>> {
42        terminated(Self::parse, char(':')).parse(input)
43    }
44}
45
46impl GitForge {
47    /// <owner>/<repo>[/?#]
48    // TODO: return up a `NixUIError::MissingTypeParameter
49    fn parse_owner_repo(input: &str) -> IResult<&str, (&str, &str), IErr<&str>> {
50        context(
51            "owner and repo",
52            cut(separated_pair(
53                context("owner", take_till1(|c| c == '/')),
54                char('/'),
55                context("repo", take_till1(|c| c == '/' || c == '?' || c == '#')),
56            )),
57        )
58        .parse(input)
59    }
60
61    /// `/[foobar]<?#>...` -> `(<?#>...), Option<foobar>)`
62    fn parse_rev_ref(input: &str) -> IResult<&str, Option<&str>, IErr<&str>> {
63        preceded(char('/'), opt(take_till1(|c| c == '?' || c == '#'))).parse(input)
64    }
65    // TODO?: Apply gitlab/hub/sourcehut rule-checks
66    // TODO: #158
67    // TODO: #163
68    /// <owner>/<repo>[/[ref-or-rev]] -> (owner: &str, repo: &str, ref_or_rev: Option<&str>)
69    pub(crate) fn parse_owner_repo_ref(
70        input: &str,
71    ) -> IResult<&str, (&str, &str, Option<&str>), IErr<&str>> {
72        let (input, (owner, repo)) = Self::parse_owner_repo(input)?;
73        // drop the `/` if it exists
74        let (input, maybe_refrev) = opt(Self::parse_rev_ref).parse(input)?;
75        // if the remaining is empty, that's the ref/rev
76
77        Ok((input, (owner, repo, maybe_refrev.flatten())))
78    }
79    pub fn parse(input: &str) -> IResult<&str, Self, IErr<&str>> {
80        let (rest, platform) = terminated(GitForgePlatform::parse, char(':')).parse(input)?;
81        let (rest, forge_path) = Self::parse_owner_repo_ref(rest)?;
82        let res = Self {
83            platform,
84            owner: forge_path.0.to_string(),
85            repo: forge_path.1.to_string(),
86            ref_or_rev: forge_path.2.map(str::to_string),
87        };
88        Ok((rest, res))
89    }
90}
91
92impl Display for GitForgePlatform {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        write!(
95            f,
96            "{}",
97            match self {
98                Self::GitHub => "github",
99                Self::GitLab => "gitlab",
100                Self::SourceHut => "sourcehut",
101            }
102        )
103    }
104}
105
106#[cfg(test)]
107mod inc_parse_platform {
108    use super::*;
109
110    #[test]
111    fn platform() {
112        let remain = ":nixos/nixpkgs";
113
114        let uri = "github:nixos/nixpkgs";
115
116        let (rest, platform) = GitForgePlatform::parse(uri).unwrap();
117        assert_eq!(rest, remain);
118        assert_eq!(platform, GitForgePlatform::GitHub);
119
120        let (rest, platform) = GitForgePlatform::parse_terminated(uri).unwrap();
121        assert_eq!(rest, &remain[1..]);
122        assert_eq!(platform, GitForgePlatform::GitHub);
123
124        let uri = "gitlab:nixos/nixpkgs";
125
126        let (rest, platform) = GitForgePlatform::parse(uri).unwrap();
127        assert_eq!(rest, remain);
128        assert_eq!(platform, GitForgePlatform::GitLab);
129
130        let uri = "sourcehut:nixos/nixpkgs";
131
132        let (rest, platform) = GitForgePlatform::parse(uri).unwrap();
133        assert_eq!(rest, remain);
134        assert_eq!(platform, GitForgePlatform::SourceHut);
135        // TODO?: fuzz test where `:` is preceded by bad string
136    }
137}
138#[cfg(test)]
139mod err_msgs {
140    use cool_asserts::assert_matches;
141    use nom::{Finish, error::ErrorKind};
142
143    use super::*;
144    use crate::error::{BaseErrorKind, ErrorTree, Expectation, StackContext};
145
146    #[test]
147    fn just_owner() {
148        let input = "owner";
149        let input_slash = "owner/";
150
151        let err = GitForge::parse_owner_repo_ref(input).finish().unwrap_err();
152        // panic!("{:?}", err);
153        assert_matches!(
154            err,
155            ErrorTree::Stack {
156                base, //: Box(ErrorTree::Base {location, kind}),
157                contexts,
158            } => {
159                assert_matches!(*base, ErrorTree::Base {
160                    location: "",
161                    kind: BaseErrorKind::Expected(Expectation::Char('/'))
162                });
163                assert_eq!(contexts, [
164                    ("owner", StackContext::Context("owner and repo")),
165                ]);
166            }
167        );
168        let err_slash = GitForge::parse_owner_repo_ref(input_slash)
169            .finish()
170            .unwrap_err();
171        assert_matches!(
172            err_slash,
173            ErrorTree::Stack {
174                base, //: Box(ErrorTree::Base {location, kind}),
175                contexts,
176            } => {
177                assert_matches!(*base, ErrorTree::Base {
178                    location: "",
179                    kind: BaseErrorKind::Kind(ErrorKind::TakeTill1)
180                });
181                assert_eq!(contexts, [
182                    ("", StackContext::Context("repo")),
183                    ("owner/", StackContext::Context("owner and repo")),
184                ]);
185            }
186        );
187
188        // assert_eq!(input, expected  `/` is missing);
189        // assert_eq!(input, expected repo-string is missing);
190    }
191    #[test]
192    #[ignore = "bad github ownerstring not yet impld"]
193    fn git_owner() {
194        let _input = "bad-owner/";
195
196        // let err = GitForge::parse_owner_repo_ref(input, GitForgePlatform::GitHub).unwrap_err();
197        // assert_eq!(input, invalid github owner format);
198    }
199    #[test]
200    #[ignore = "bad github repostring not yet impld"]
201    fn git_repo() {
202        let _input = "owner/bad-string";
203
204        // let err = GitForge::parse_owner_repo_ref(input, GitForgePlatform::GitHub).unwrap_err();
205        // assert_eq!(input, invalid github owner format);
206    }
207    #[test]
208    #[ignore = "bad mercurial ownerstring not yet impld"]
209    fn merc_owner() {
210        let _input = "bad-owner/";
211
212        // let err = GitForge::parse_owner_repo_ref(input, GitForgePlatform::Mercurial).unwrap_err();
213        // assert_eq!(input, invalid github owner format);
214    }
215    #[test]
216    #[ignore = "bad mercurial repostring not yet impld"]
217    fn merc_repo() {
218        let _input = "owner/bad-string";
219
220        // let err = GitForge::parse_owner_repo_ref(input, GitForgePlatform::Mercurial).unwrap_err();
221        // assert_eq!(input, invalid github owner format);
222    }
223}
224#[cfg(test)]
225mod inc_parse {
226    use super::*;
227
228    #[test]
229    fn plain() {
230        let input = "owner/repo";
231        let (rest, res) = GitForge::parse_owner_repo_ref(input).unwrap();
232        let expected = ("owner", "repo", None);
233        assert_eq!(rest, "");
234        assert_eq!(expected, res);
235    }
236
237    #[test]
238    fn param_terminated() {
239        let input = "owner/repo?🤡";
240        let (rest, res) = GitForge::parse_owner_repo_ref(input).unwrap();
241        let expected = ("owner", "repo", None);
242        assert_eq!(rest, "?🤡");
243        assert_eq!(expected, res);
244        assert_eq!(rest, "?🤡");
245
246        let input = "owner/repo#🤡";
247        let (rest, res) = GitForge::parse_owner_repo_ref(input).unwrap();
248        let expected = ("owner", "repo", None);
249        assert_eq!(expected, res);
250        assert_eq!(rest, "#🤡");
251
252        let input = "owner/repo?#🤡";
253        let (rest, res) = GitForge::parse_owner_repo_ref(input).unwrap();
254        let expected = ("owner", "repo", None);
255        assert_eq!(expected, res);
256        assert_eq!(rest, "?#🤡");
257    }
258
259    #[test]
260    fn attr_terminated() {
261        let input = "owner/repo#fizz.bar";
262        let (rest, res) = GitForge::parse_owner_repo_ref(input).unwrap();
263        let expected = ("owner", "repo", None);
264        assert_eq!(rest, "#fizz.bar");
265        assert_eq!(expected, res);
266    }
267
268    #[test]
269    fn rev_param_terminated() {
270        let input = "owner/repo/rev?foo=bar";
271        let (rest, res) = GitForge::parse_owner_repo_ref(input).unwrap();
272        let expected = ("owner", "repo", Some("rev"));
273        assert_eq!(rest, "?foo=bar");
274        assert_eq!(expected, res);
275    }
276
277    #[test]
278    fn rev_attr_terminated() {
279        let input = "owner/repo/rev#fizz.bar";
280        let (rest, res) = GitForge::parse_owner_repo_ref(input).unwrap();
281        let expected = ("owner", "repo", Some("rev"));
282        assert_eq!(rest, "#fizz.bar");
283        assert_eq!(expected, res);
284    }
285}