nix_compat/flakeref/
mod.rs

1// Implements a parser and formatter for Nix flake references.
2// It defines the `FlakeRef` enum which represents different types of flake sources
3// (such as Git repositories, GitHub repos, local paths, etc.), along with functionality
4// to parse URLs into `FlakeRef` instances and convert them back to URIs.
5use std::{collections::HashMap, path::PathBuf};
6use url::Url;
7
8#[derive(Debug)]
9#[non_exhaustive]
10pub enum FlakeRef {
11    File {
12        last_modified: Option<u64>,
13        nar_hash: Option<String>,
14        rev: Option<String>,
15        rev_count: Option<u64>,
16        url: Url,
17    },
18    Git {
19        all_refs: bool,
20        export_ignore: bool,
21        keytype: Option<String>,
22        public_key: Option<String>,
23        public_keys: Option<Vec<String>>,
24        r#ref: Option<String>,
25        rev: Option<String>,
26        shallow: bool,
27        submodules: bool,
28        url: Url,
29        verify_commit: bool,
30    },
31    GitHub {
32        owner: String,
33        repo: String,
34        host: Option<String>,
35        keytype: Option<String>,
36        public_key: Option<String>,
37        public_keys: Option<Vec<String>>,
38        r#ref: Option<String>,
39        rev: Option<String>,
40    },
41    GitLab {
42        owner: String,
43        repo: String,
44        host: Option<String>,
45        keytype: Option<String>,
46        public_key: Option<String>,
47        public_keys: Option<Vec<String>>,
48        r#ref: Option<String>,
49        rev: Option<String>,
50    },
51    Indirect {
52        id: String,
53        r#ref: Option<String>,
54        rev: Option<String>,
55    },
56    Mercurial {
57        r#ref: Option<String>,
58        rev: Option<String>,
59    },
60    Path {
61        last_modified: Option<u64>,
62        nar_hash: Option<String>,
63        path: PathBuf,
64        rev: Option<String>,
65        rev_count: Option<u64>,
66    },
67    SourceHut {
68        owner: String,
69        repo: String,
70        host: Option<String>,
71        keytype: Option<String>,
72        public_key: Option<String>,
73        public_keys: Option<Vec<String>>,
74        r#ref: Option<String>,
75        rev: Option<String>,
76    },
77    Tarball {
78        last_modified: Option<u64>,
79        nar_hash: Option<String>,
80        rev: Option<String>,
81        rev_count: Option<u64>,
82        url: Url,
83    },
84}
85
86#[derive(Debug, Default)]
87pub struct FlakeRefOutput {
88    pub out_path: String,
89    pub nar_hash: String,
90    pub last_modified: Option<i64>,
91    pub last_modified_date: Option<String>,
92    pub rev_count: Option<i64>,
93    pub rev: Option<String>,
94    pub short_rev: Option<String>,
95    pub submodules: Option<bool>,
96}
97
98impl FlakeRefOutput {
99    pub fn into_kv_tuples(self) -> Vec<(String, String)> {
100        let mut vec = vec![
101            ("outPath".into(), self.out_path),
102            ("narHash".into(), self.nar_hash),
103        ];
104
105        if let Some(lm) = self.last_modified {
106            vec.push(("lastModified".into(), lm.to_string()));
107        }
108        if let Some(lmd) = self.last_modified_date {
109            vec.push(("lastModifiedDate".into(), lmd));
110        }
111        if let Some(rc) = self.rev_count {
112            vec.push(("revCount".into(), rc.to_string()));
113        }
114        if let Some(rev) = self.rev {
115            vec.push(("rev".into(), rev));
116        }
117        if let Some(sr) = self.short_rev {
118            vec.push(("shortRev".into(), sr));
119        }
120        if let Some(sub) = self.submodules {
121            vec.push(("submodules".into(), sub.to_string()));
122        }
123
124        vec
125    }
126}
127
128#[derive(Debug, thiserror::Error)]
129pub enum FlakeRefError {
130    #[error("failed to parse URL: {0}")]
131    UrlParseError(#[from] url::ParseError),
132    #[error("unsupported input type: {0}")]
133    UnsupportedType(String),
134}
135
136// Implement FromStr for FlakeRef to allow parsing from a string
137impl std::str::FromStr for FlakeRef {
138    type Err = FlakeRefError;
139
140    fn from_str(s: &str) -> Result<Self, Self::Err> {
141        // Parse initial URL
142        let mut url = Url::parse(s)?;
143        let mut new_protocol = None;
144
145        // Determine fetch type from scheme
146        let fetch_type = if let Some((type_part, protocol)) = url.scheme().split_once('+') {
147            new_protocol = Some(protocol.to_string());
148            match type_part {
149                "path" => FetchType::Path,
150                "file" => FetchType::File,
151                "tarball" => FetchType::Tarball,
152                "git" => FetchType::Git,
153                "github" => FetchType::GitHub,
154                "gitlab" => FetchType::GitLab,
155                "sourcehut" => FetchType::SourceHut,
156                "indirect" => FetchType::Indirect,
157                _ => return Err(FlakeRefError::UnsupportedType(type_part.to_string())),
158            }
159        } else {
160            match url.scheme() {
161                // Direct schemes
162                "path" => FetchType::Path,
163                "github" => FetchType::GitHub,
164                "gitlab" => FetchType::GitLab,
165                "sourcehut" => FetchType::SourceHut,
166                "git" => FetchType::Git,
167                // Check for tarball file extensions
168                _ if is_tarball_extension(url.path()) => FetchType::Tarball,
169                // Default to File for other schemes
170                _ => FetchType::File,
171            }
172        };
173
174        // We need to convert the URL to string, strip the prefix there, and then
175        // parse it back as url, as Url::set_scheme() rejects some of the transitions we want to do.
176        if let Some(protocol) = new_protocol {
177            let mut url_str = url.to_string();
178            url_str.replace_range(..url.scheme().len(), &protocol);
179            url = Url::parse(&url_str)?;
180        }
181
182        // Extract query parameters
183        let query_pairs = extract_query_pairs(&url);
184
185        // Process URL based on fetch type
186        Ok(match fetch_type {
187            FetchType::File => {
188                let params = extract_common_file_params(&query_pairs);
189                FlakeRef::File {
190                    url,
191                    nar_hash: params.nar_hash,
192                    rev: params.rev,
193                    rev_count: params.rev_count,
194                    last_modified: params.last_modified,
195                }
196            }
197            FetchType::Tarball => {
198                let params = extract_common_file_params(&query_pairs);
199                FlakeRef::Tarball {
200                    url,
201                    nar_hash: params.nar_hash,
202                    rev: params.rev,
203                    rev_count: params.rev_count,
204                    last_modified: params.last_modified,
205                }
206            }
207            FetchType::Indirect => FlakeRef::Indirect {
208                id: url.path().to_string(),
209                r#ref: query_pairs.get("ref").cloned(),
210                rev: query_pairs.get("rev").cloned(),
211            },
212            FetchType::Git => {
213                let params = extract_git_params(&query_pairs);
214                FlakeRef::Git {
215                    url,
216                    r#ref: params.r#ref,
217                    rev: params.rev,
218                    keytype: params.keytype,
219                    public_key: params.public_key,
220                    public_keys: params.public_keys,
221                    shallow: params.shallow,
222                    submodules: params.submodules,
223                    export_ignore: params.export_ignore,
224                    all_refs: params.all_refs,
225                    verify_commit: params.verify_commit,
226                }
227            }
228            FetchType::Path => {
229                let params = extract_common_file_params(&query_pairs);
230                FlakeRef::Path {
231                    path: PathBuf::from(url.path()),
232                    rev: params.rev,
233                    nar_hash: params.nar_hash,
234                    rev_count: params.rev_count,
235                    last_modified: params.last_modified,
236                }
237            }
238            FetchType::GitHub => {
239                create_repo_host_args(&url, &query_pairs, |params| FlakeRef::GitHub {
240                    owner: params.owner,
241                    repo: params.repo,
242                    r#ref: params.r#ref,
243                    rev: params.rev,
244                    host: params.host,
245                    keytype: params.keytype,
246                    public_key: params.public_key,
247                    public_keys: params.public_keys,
248                })?
249            }
250            FetchType::GitLab => {
251                create_repo_host_args(&url, &query_pairs, |params| FlakeRef::GitLab {
252                    owner: params.owner,
253                    repo: params.repo,
254                    r#ref: params.r#ref,
255                    rev: params.rev,
256                    host: params.host,
257                    keytype: params.keytype,
258                    public_key: params.public_key,
259                    public_keys: params.public_keys,
260                })?
261            }
262            FetchType::SourceHut => {
263                create_repo_host_args(&url, &query_pairs, |params| FlakeRef::SourceHut {
264                    owner: params.owner,
265                    repo: params.repo,
266                    r#ref: params.r#ref,
267                    rev: params.rev,
268                    host: params.host,
269                    keytype: params.keytype,
270                    public_key: params.public_key,
271                    public_keys: params.public_keys,
272                })?
273            }
274        })
275    }
276}
277
278// Common parameter structs
279#[derive(Debug, Default, Clone)]
280struct FileParams {
281    nar_hash: Option<String>,
282    rev: Option<String>,
283    rev_count: Option<u64>,
284    last_modified: Option<u64>,
285}
286
287#[derive(Debug, Default)]
288struct GitParams {
289    r#ref: Option<String>,
290    rev: Option<String>,
291    keytype: Option<String>,
292    public_key: Option<String>,
293    public_keys: Option<Vec<String>>,
294    submodules: bool,
295    shallow: bool,
296    export_ignore: bool,
297    all_refs: bool,
298    verify_commit: bool,
299}
300
301#[derive(Debug, Default)]
302struct RepoHostParams {
303    owner: String,
304    repo: String,
305    host: Option<String>,
306    r#ref: Option<String>,
307    rev: Option<String>,
308    keytype: Option<String>,
309    public_key: Option<String>,
310    public_keys: Option<Vec<String>>,
311}
312
313// Helper enum for fetch types
314enum FetchType {
315    File,
316    Git,
317    GitHub,
318    GitLab,
319    Indirect,
320    Path,
321    SourceHut,
322    Tarball,
323}
324
325// Helper functions for query parameters
326fn extract_query_pairs(url: &Url) -> HashMap<String, String> {
327    url.query_pairs()
328        .map(|(k, v)| (k.to_string(), v.to_string()))
329        .collect()
330}
331
332fn get_param(query_pairs: &HashMap<String, String>, key: &str) -> Option<u64> {
333    query_pairs.get(key).and_then(|s| s.parse().ok())
334}
335
336fn get_bool_param(query_pairs: &HashMap<String, String>, key: &str) -> bool {
337    query_pairs
338        .get(key)
339        .map(|v| v == "1" || v.to_lowercase() == "true")
340        .unwrap_or(false)
341}
342
343// Parameter extractors
344fn extract_common_file_params(query_pairs: &HashMap<String, String>) -> FileParams {
345    FileParams {
346        nar_hash: query_pairs.get("narHash").cloned(),
347        rev: query_pairs.get("rev").cloned(),
348        rev_count: get_param(query_pairs, "revCount"),
349        last_modified: get_param(query_pairs, "lastModified"),
350    }
351}
352
353fn extract_git_params(query_pairs: &HashMap<String, String>) -> GitParams {
354    GitParams {
355        r#ref: query_pairs.get("ref").cloned(),
356        rev: query_pairs.get("rev").cloned(),
357        keytype: query_pairs.get("keytype").cloned(),
358        public_key: query_pairs.get("publicKey").cloned(),
359        public_keys: query_pairs
360            .get("publicKeys")
361            .map(|s| s.split(',').map(String::from).collect()),
362        submodules: get_bool_param(query_pairs, "submodules"),
363        shallow: get_bool_param(query_pairs, "shallow"),
364        export_ignore: get_bool_param(query_pairs, "exportIgnore"),
365        all_refs: get_bool_param(query_pairs, "allRefs"),
366        verify_commit: get_bool_param(query_pairs, "verifyCommit"),
367    }
368}
369
370fn extract_repo_params(
371    url: &Url,
372    query_pairs: &HashMap<String, String>,
373) -> Result<RepoHostParams, FlakeRefError> {
374    let (owner, repo, path_ref) = parse_path_segments(url)?;
375
376    // Check for branch/tag conflicts
377    if path_ref.is_some() && query_pairs.contains_key("ref") {
378        return Err(FlakeRefError::UnsupportedType(
379            "URL contains multiple branch/tag names".to_string(),
380        ));
381    }
382
383    let r#ref = path_ref.or_else(|| query_pairs.get("ref").cloned());
384
385    Ok(RepoHostParams {
386        owner,
387        repo,
388        r#ref,
389        rev: query_pairs.get("rev").cloned(),
390        host: query_pairs.get("host").cloned(),
391        keytype: query_pairs.get("keytype").cloned(),
392        public_key: query_pairs.get("publicKey").cloned(),
393        public_keys: query_pairs
394            .get("publicKeys")
395            .map(|s| s.split(',').map(String::from).collect()),
396    })
397}
398
399// URL parsing helpers
400fn parse_path_segments(url: &Url) -> Result<(String, String, Option<String>), FlakeRefError> {
401    let path_segments: Vec<&str> = url.path().trim_start_matches('/').splitn(3, '/').collect();
402
403    if path_segments.len() < 2 {
404        return Err(FlakeRefError::UnsupportedType(
405            "URLs must contain owner and repo".to_string(),
406        ));
407    }
408
409    Ok((
410        path_segments[0].to_string(),
411        path_segments[1].to_string(),
412        path_segments.get(2).map(|&s| s.to_string()),
413    ))
414}
415
416// Helper function for tarball detection
417fn is_tarball_extension(path: &str) -> bool {
418    const TARBALL_EXTENSIONS: [&str; 7] = [
419        ".zip", ".tar", ".tgz", ".tar.gz", ".tar.xz", ".tar.bz2", ".tar.zst",
420    ];
421
422    TARBALL_EXTENSIONS.iter().any(|ext| path.ends_with(ext))
423}
424
425fn create_repo_host_args<F>(
426    url: &Url,
427    query_pairs: &HashMap<String, String>,
428    creator: F,
429) -> Result<FlakeRef, FlakeRefError>
430where
431    F: FnOnce(RepoHostParams) -> FlakeRef,
432{
433    let params = extract_repo_params(url, query_pairs)?;
434    Ok(creator(params))
435}
436
437// Helper functions for appending query parameters
438fn append_param<T: ToString>(url: &mut Url, key: &str, value: &Option<T>) {
439    if let Some(val) = value {
440        url.query_pairs_mut().append_pair(key, &val.to_string());
441    }
442}
443
444fn append_bool_param(url: &mut Url, key: &str, value: bool) {
445    if value {
446        url.query_pairs_mut().append_pair(key, "1");
447    }
448}
449
450fn append_params(url: &mut Url, params: &[(&str, Option<String>)]) {
451    for &(key, ref value) in params {
452        append_param(url, key, value);
453    }
454}
455
456fn append_public_keys_param(url: &mut Url, public_keys: &Option<Vec<String>>) {
457    if let Some(keys) = public_keys {
458        url.query_pairs_mut()
459            .append_pair("publicKeys", &keys.join(","));
460    }
461}
462
463fn append_common_file_params(url: &mut Url, params: &FileParams) {
464    append_params(
465        url,
466        &[
467            ("narHash", params.nar_hash.clone()),
468            ("rev", params.rev.clone()),
469        ],
470    );
471    append_param(url, "revCount", &params.rev_count);
472    append_param(url, "lastModified", &params.last_modified);
473}
474
475fn append_git_params(url: &mut Url, params: &GitParams) {
476    append_params(
477        url,
478        &[
479            ("ref", params.r#ref.clone()),
480            ("rev", params.rev.clone()),
481            ("keytype", params.keytype.clone()),
482            ("publicKey", params.public_key.clone()),
483        ],
484    );
485    append_public_keys_param(url, &params.public_keys);
486    append_bool_param(url, "shallow", params.shallow);
487    append_bool_param(url, "submodules", params.submodules);
488    append_bool_param(url, "exportIgnore", params.export_ignore);
489    append_bool_param(url, "allRefs", params.all_refs);
490    append_bool_param(url, "verifyCommit", params.verify_commit);
491}
492
493fn append_repo_host_params(url: &mut Url, params: &RepoHostParams) {
494    append_params(
495        url,
496        &[
497            ("ref", params.r#ref.clone()),
498            ("rev", params.rev.clone()),
499            ("keytype", params.keytype.clone()),
500            ("publicKey", params.public_key.clone()),
501        ],
502    );
503    append_public_keys_param(url, &params.public_keys);
504}
505
506// Implementation of to_uri method for FlakeRef
507impl FlakeRef {
508    pub fn to_uri(&self) -> Url {
509        match self {
510            FlakeRef::File {
511                url,
512                nar_hash,
513                rev,
514                rev_count,
515                last_modified,
516            } => {
517                let mut url = url.clone();
518                let params = FileParams {
519                    nar_hash: nar_hash.clone(),
520                    rev: rev.clone(),
521                    rev_count: *rev_count,
522                    last_modified: *last_modified,
523                };
524                append_common_file_params(&mut url, &params);
525                url
526            }
527            FlakeRef::Git {
528                url,
529                r#ref,
530                rev,
531                keytype,
532                public_key,
533                public_keys,
534                shallow,
535                submodules,
536                export_ignore,
537                all_refs,
538                verify_commit,
539            } => {
540                let mut url = url.clone();
541                let params = GitParams {
542                    r#ref: r#ref.clone(),
543                    rev: rev.clone(),
544                    keytype: keytype.clone(),
545                    public_key: public_key.clone(),
546                    public_keys: public_keys.clone(),
547                    shallow: *shallow,
548                    submodules: *submodules,
549                    export_ignore: *export_ignore,
550                    all_refs: *all_refs,
551                    verify_commit: *verify_commit,
552                };
553                append_git_params(&mut url, &params);
554                Url::parse(&format!("git+{}", url.as_str())).unwrap()
555            }
556            FlakeRef::GitHub {
557                owner,
558                repo,
559                host,
560                keytype,
561                public_key,
562                public_keys,
563                r#ref,
564                rev,
565            }
566            | FlakeRef::GitLab {
567                owner,
568                repo,
569                host,
570                keytype,
571                public_key,
572                public_keys,
573                r#ref,
574                rev,
575            }
576            | FlakeRef::SourceHut {
577                owner,
578                repo,
579                host,
580                keytype,
581                public_key,
582                public_keys,
583                r#ref,
584                rev,
585            } => {
586                let scheme = match self {
587                    FlakeRef::GitHub { .. } => "github",
588                    FlakeRef::GitLab { .. } => "gitlab",
589                    FlakeRef::SourceHut { .. } => "sourcehut",
590                    _ => unreachable!(),
591                };
592
593                let mut url = Url::parse(&format!("{}://{}/{}", scheme, owner, repo)).unwrap();
594                if let Some(h) = host {
595                    url.set_host(Some(h)).unwrap();
596                }
597
598                let params = RepoHostParams {
599                    owner: owner.clone(),
600                    repo: repo.clone(),
601                    host: host.clone(),
602                    r#ref: r#ref.clone(),
603                    rev: rev.clone(),
604                    keytype: keytype.clone(),
605                    public_key: public_key.clone(),
606                    public_keys: public_keys.clone(),
607                };
608                append_repo_host_params(&mut url, &params);
609                url
610            }
611            FlakeRef::Indirect { id, r#ref, rev } => {
612                let mut url = Url::parse(&format!("indirect://{}", id)).unwrap();
613                append_params(&mut url, &[("ref", r#ref.clone()), ("rev", rev.clone())]);
614                url
615            }
616            FlakeRef::Path {
617                path,
618                rev,
619                nar_hash,
620                rev_count,
621                last_modified,
622            } => {
623                let mut url = Url::parse(&format!("path://{}", path.display())).unwrap();
624                let params = FileParams {
625                    nar_hash: nar_hash.clone(),
626                    rev: rev.clone(),
627                    rev_count: *rev_count,
628                    last_modified: *last_modified,
629                };
630                append_common_file_params(&mut url, &params);
631                url
632            }
633            FlakeRef::Tarball {
634                url,
635                nar_hash,
636                rev,
637                rev_count,
638                last_modified,
639            } => {
640                let mut url = url.clone();
641                let params = FileParams {
642                    nar_hash: nar_hash.clone(),
643                    rev: rev.clone(),
644                    rev_count: *rev_count,
645                    last_modified: *last_modified,
646                };
647                append_common_file_params(&mut url, &params);
648                url
649            }
650            FlakeRef::Mercurial { r#ref, rev } => {
651                let mut url = Url::parse("hg://").unwrap();
652                append_params(&mut url, &[("ref", r#ref.clone()), ("rev", rev.clone())]);
653                url
654            }
655        }
656    }
657}
658
659#[cfg(test)]
660mod tests {
661    use super::*;
662
663    #[test]
664    fn test_git_urls() {
665        let input = "git+https://github.com/lichess-org/fishnet?submodules=1";
666        pretty_assertions::assert_matches!(
667            input.parse::<FlakeRef>(),
668            Ok(FlakeRef::Git {
669                submodules: true,
670                shallow: false,
671                export_ignore: false,
672                all_refs: false,
673                verify_commit: false,
674                ..
675            })
676        );
677
678        let input = "git+file:///home/user/project?ref=fa1e2d23a22";
679        match input.parse::<FlakeRef>() {
680            Ok(FlakeRef::Git { r#ref, rev, .. }) => {
681                assert_eq!(r#ref, Some("fa1e2d23a22".to_string()));
682                assert_eq!(rev, None);
683            }
684            _ => panic!("Expected Git input type"),
685        }
686
687        let input = "git+git://github.com/someuser/my-repo?rev=v1.2.3";
688        match input.parse::<FlakeRef>() {
689            Ok(FlakeRef::Git { rev, .. }) => {
690                assert_eq!(rev, Some("v1.2.3".to_string()));
691            }
692            _ => panic!("Expected Git input type"),
693        }
694    }
695
696    #[test]
697    fn test_github_urls() {
698        let input = "github:snowfallorg/lib?ref=v2.1.1";
699        match input.parse::<FlakeRef>() {
700            Ok(FlakeRef::GitHub { r#ref, rev, .. }) => {
701                assert_eq!(r#ref, Some("v2.1.1".to_string()));
702                assert_eq!(rev, None);
703            }
704            _ => panic!("Expected GitHub input type"),
705        }
706
707        let input = "github:aarowill/base16-alacritty";
708        match input.parse::<FlakeRef>() {
709            Ok(FlakeRef::GitHub { r#ref, rev, .. }) => {
710                assert_eq!(r#ref, None);
711                assert_eq!(rev, None);
712            }
713            _ => panic!("Expected GitHub input type"),
714        }
715
716        let input = "github:a/b/c?ref=yyy";
717        match input.parse::<FlakeRef>() {
718            Ok(_) => panic!("Expected error for multiple identifiers"),
719            Err(FlakeRefError::UnsupportedType(_)) => (),
720            _ => panic!("Expected UnsupportedType error"),
721        }
722
723        let input = "github:a";
724        match input.parse::<FlakeRef>() {
725            Ok(_) => panic!("Expected error for missing repo"),
726            Err(FlakeRefError::UnsupportedType(_)) => (),
727            _ => panic!("Expected UnsupportedType error"),
728        }
729
730        let input = "github:a/b/master/extra";
731        match input.parse::<FlakeRef>() {
732            Ok(FlakeRef::GitHub { r#ref, rev, .. }) => {
733                assert_eq!(r#ref, Some("master/extra".to_string()));
734                assert_eq!(rev, None);
735            }
736            _ => panic!("Expected GitHub input type"),
737        }
738
739        let input = "github:a/b";
740        match input.parse::<FlakeRef>() {
741            Ok(FlakeRef::GitHub { r#ref, .. }) => {
742                assert_eq!(r#ref, None);
743            }
744            _ => panic!("Expected GitHub input type"),
745        }
746    }
747
748    #[test]
749    fn test_file_urls() {
750        let input = "https://www.shutterstock.com/image-photo/young-potato-isolated-on-white-260nw-630239534.jpg";
751        pretty_assertions::assert_matches!(
752            input.parse::<FlakeRef>(),
753            Ok(FlakeRef::File {
754                url,
755                nar_hash: None,
756                rev: None,
757                rev_count: None,
758                last_modified: None,
759            }) if url.to_string() == input
760        );
761    }
762
763    #[test]
764    fn test_path_urls() {
765        let input = "path:./go";
766        pretty_assertions::assert_matches!(
767            input.parse::<FlakeRef>(),
768            Ok(FlakeRef::Path {
769                path,
770                rev: None,
771                nar_hash: None,
772                rev_count: None,
773                last_modified: None,
774            }) if path.to_str().unwrap() == "./go"
775        );
776
777        let input = "~/Downloads/a.zip";
778        match input.parse::<FlakeRef>() {
779            Ok(_) => panic!("Expected error for invalid URL format"),
780            Err(FlakeRefError::UrlParseError(_)) => (),
781            _ => panic!("Expected UrlParseError error"),
782        }
783    }
784}