1use std::fmt::Display;
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::{NixUriError, UnsupportedReason};
6
7pub(crate) mod encoding;
8mod fr_type;
9pub use fr_type::FlakeRefType;
10pub(crate) mod location_params;
11pub(crate) use location_params::LocationParamKeys;
12pub use location_params::LocationParameters;
13mod transport_layer;
14pub use transport_layer::TransportLayer;
15mod forge;
16pub use forge::{GitForge, GitForgePlatform};
17mod resource_url;
18pub use resource_url::{ResourceType, ResourceUrl};
19#[cfg(test)]
20mod proptest;
21pub(crate) mod validators;
22
23#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
36#[non_exhaustive]
37pub enum RefLocation {
38 #[default]
40 PathComponent,
41 QueryParameter,
43}
44
45#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
47#[cfg_attr(test, serde(deny_unknown_fields))]
48#[non_exhaustive]
49pub struct FlakeRef {
50 pub(crate) kind: FlakeRefType,
56 fragment: Option<String>,
57 params: Box<LocationParameters>,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
70#[non_exhaustive]
71pub struct ForgeIdentity {
72 pub platform: GitForgePlatform,
73 pub owner: String,
74 pub repo: String,
75 pub domain: String,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82#[non_exhaustive]
83pub enum RefKind {
84 None,
86 Ref,
88 Rev,
90 Both,
93}
94
95impl FlakeRef {
96 pub fn new(kind: FlakeRefType) -> Self {
100 Self {
101 kind,
102 ..Self::default()
103 }
104 }
105
106 pub fn kind(&self) -> &FlakeRefType {
109 &self.kind
110 }
111
112 pub fn kind_mut(&mut self) -> &mut FlakeRefType {
117 &mut self.kind
118 }
119
120 pub fn id(&self) -> Option<&str> {
123 self.kind().id()
124 }
125
126 pub fn owner(&self) -> Option<&str> {
131 self.kind().owner()
132 }
133
134 pub fn repo(&self) -> Option<&str> {
137 self.kind().repo()
138 }
139
140 pub fn domain(&self) -> Option<&str> {
150 if matches!(self.kind(), FlakeRefType::GitForge(_)) {
151 if let Some(host) = self.params.host_value() {
152 return Some(host);
153 }
154 }
155 self.kind().domain()
156 }
157
158 pub fn forge_identity(&self) -> Option<ForgeIdentity> {
169 match self.kind() {
170 FlakeRefType::GitForge(forge) => {
171 let canonical = match forge.platform {
172 GitForgePlatform::GitHub => "github.com",
173 GitForgePlatform::GitLab => "gitlab.com",
174 GitForgePlatform::SourceHut => "git.sr.ht",
175 };
176 let domain = self
177 .params
178 .host_value()
179 .map_or_else(|| canonical.to_string(), str::to_owned);
180 Some(ForgeIdentity {
181 platform: forge.platform.clone(),
182 owner: forge.owner.clone(),
183 repo: forge.repo.clone(),
184 domain,
185 })
186 }
187 _ => None,
188 }
189 }
190
191 pub fn ref_(&self) -> Option<&str> {
193 match self.kind() {
194 FlakeRefType::GitForge(GitForge { ref_, .. }) | FlakeRefType::Indirect { ref_, .. } => {
195 ref_.as_deref()
196 }
197 FlakeRefType::Resource(res) => res.ref_.as_deref(),
198 FlakeRefType::Path { .. } => None,
199 }
200 }
201
202 pub fn rev(&self) -> Option<&str> {
204 match self.kind() {
205 FlakeRefType::GitForge(GitForge { rev, .. })
206 | FlakeRefType::Indirect { rev, .. }
207 | FlakeRefType::Path { rev, .. } => rev.as_deref(),
208 FlakeRefType::Resource(res) => res.rev.as_deref(),
209 }
210 }
211
212 pub fn ref_or_rev(&self) -> Option<&str> {
217 if self.is_pinned_to_rev() {
218 self.rev()
219 } else {
220 self.ref_().or_else(|| self.rev())
221 }
222 }
223
224 pub fn ref_kind(&self) -> RefKind {
228 match (self.ref_().is_some(), self.rev().is_some()) {
229 (false, false) => RefKind::None,
230 (true, false) => RefKind::Ref,
231 (false, true) => RefKind::Rev,
232 (true, true) => RefKind::Both,
233 }
234 }
235
236 pub fn is_pinned_to_rev(&self) -> bool {
240 matches!(
241 self.kind(),
242 FlakeRefType::GitForge(GitForge { rev: Some(_), .. })
243 | FlakeRefType::Indirect { rev: Some(_), .. }
244 | FlakeRefType::Path { rev: Some(_), .. }
245 ) || matches!(
246 self.kind(),
247 FlakeRefType::Resource(res) if res.rev.is_some()
248 )
249 }
250
251 pub fn ref_source_location(&self) -> RefLocation {
255 match self.kind() {
256 FlakeRefType::GitForge(forge) => forge.location,
257 FlakeRefType::Indirect { location, .. } => *location,
258 FlakeRefType::Resource(res) => res.ref_location,
259 FlakeRefType::Path { .. } => RefLocation::QueryParameter,
260 }
261 }
262
263 pub fn params(&self) -> &LocationParameters {
265 &self.params
266 }
267
268 pub fn fragment(&self) -> Option<&str> {
272 self.fragment.as_deref()
273 }
274
275 pub fn set_ref(&mut self, new_ref: Option<String>) {
282 let writing_some = new_ref.is_some();
283 self.kind_mut().set_ref(new_ref);
284 if writing_some && matches!(self.kind(), FlakeRefType::Resource(_)) {
285 self.kind_mut()
286 .set_ref_location(RefLocation::QueryParameter);
287 }
288 }
289
290 pub fn set_rev(&mut self, new_rev: Option<String>) {
293 let writing_some = new_rev.is_some();
294 self.kind_mut().set_rev(new_rev);
295 if writing_some && matches!(self.kind(), FlakeRefType::Resource(_)) {
296 self.kind_mut()
297 .set_ref_location(RefLocation::QueryParameter);
298 }
299 }
300
301 pub fn set_fragment(&mut self, fragment: Option<String>) {
303 self.fragment = fragment;
304 }
305
306 pub fn set_ref_location(&mut self, loc: RefLocation) {
311 self.kind_mut().set_ref_location(loc);
312 }
313
314 pub fn set_dir(&mut self, dir: Option<String>) {
316 self.params.set_dir(dir);
317 }
318
319 pub fn set_host(&mut self, host: Option<String>) {
321 self.params.set_host(host);
322 }
323
324 pub fn set_shallow(&mut self, shallow: bool) {
329 self.params.set_shallow(Some(shallow));
330 }
331
332 pub fn set_submodules(&mut self, submodules: bool) {
335 self.params.set_submodules(Some(submodules));
336 }
337
338 pub fn set_nar_hash(&mut self, hash: Option<String>) {
340 self.params.set_nar_hash(hash);
341 }
342
343 pub fn set_last_modified(&mut self, ts: Option<String>) {
345 self.params.set_last_modified(ts);
346 }
347
348 pub fn set_rev_count(&mut self, count: Option<String>) {
350 self.params.set_rev_count(count);
351 }
352
353 pub(crate) fn replace_params(&mut self, params: LocationParameters) {
359 *self.params = params;
360 }
361
362 pub fn with_ref(mut self, r: Option<String>) -> Self {
367 self.set_ref(r);
368 self
369 }
370
371 pub fn with_rev(mut self, r: Option<String>) -> Self {
375 self.set_rev(r);
376 self
377 }
378
379 pub fn try_with_ref(self, new_ref: Option<String>) -> Result<Self, NixUriError> {
394 if new_ref.is_some() && !self.kind().allows_ref() {
395 return Err(NixUriError::Unsupported(UnsupportedReason::Field {
396 field: "ref".into(),
397 only_supported_by: "github, gitlab, sourcehut, flake (indirect), git+, hg+".into(),
398 }));
399 }
400 Ok(self.with_ref(new_ref))
401 }
402
403 pub fn try_with_rev(self, new_rev: Option<String>) -> Result<Self, NixUriError> {
410 Ok(self.with_rev(new_rev))
411 }
412
413 pub fn with_fragment(mut self, fragment: Option<String>) -> Self {
415 self.set_fragment(fragment);
416 self
417 }
418
419 pub fn with_kind(mut self, kind: FlakeRefType) -> Self {
421 *self.kind_mut() = kind;
422 self
423 }
424
425 pub fn with_params(mut self, params: LocationParameters) -> Self {
428 self.params = Box::new(params);
429 self
430 }
431
432 pub fn without_pin(mut self) -> Self {
436 self.set_rev(None);
437 self
438 }
439
440 pub fn pin_to_rev(mut self, rev: String) -> Self {
453 self.set_ref(None);
454 self.set_rev(Some(rev));
455 self
456 }
457
458 pub fn into_uri(self) -> String {
462 self.to_string()
463 }
464
465 pub fn to_canonical_string(&self) -> String {
495 use std::fmt::Write;
496
497 let mut out = String::new();
498
499 match self.kind() {
500 FlakeRefType::GitForge(forge) => {
501 let owner_out = encoding::encode_path_segment(&forge.owner);
502 write!(&mut out, "{}:{}/{}", forge.platform, owner_out, forge.repo).unwrap();
503 if let Some(value) = forge.rev.as_deref().or(forge.ref_.as_deref()) {
509 write!(&mut out, "/{value}").unwrap();
510 }
511 let mut entries: Vec<(&str, &str)> = Vec::new();
512 if let Some(host) = self.params.host_value() {
513 entries.push(("host", host));
514 }
515 if let Some(nar) = self.params.nar_hash_value() {
516 entries.push(("narHash", nar));
517 }
518 entries.sort_by(|a, b| a.0.cmp(b.0));
519 write_canonical_query(&mut out, &entries);
520 }
521 FlakeRefType::Resource(res) if matches!(res.res_type, ResourceType::Git) => {
522 write_resource_base(&mut out, res);
523 let mut entries: Vec<(&str, &str)> = Vec::new();
524 if let Some(r) = res.ref_.as_deref() {
525 entries.push(("ref", r));
526 }
527 if let Some(v) = res.rev.as_deref() {
528 entries.push(("rev", v));
529 }
530 if self.params.shallow_truthy() {
531 entries.push(("shallow", "1"));
532 }
533 if self.params.lfs == Some(true) {
534 entries.push(("lfs", "1"));
535 }
536 if self.params.submodules_truthy() {
537 entries.push(("submodules", "1"));
538 }
539 if self.params.export_ignore == Some(true) {
540 entries.push(("exportIgnore", "1"));
541 }
542 if self.params.verify_commit == Some(true) {
543 entries.push(("verifyCommit", "1"));
544 }
545 if let Some(kt) = self.params.keytype.as_deref() {
546 entries.push(("keytype", kt));
547 }
548 if let Some(pk) = self.params.public_key.as_deref() {
549 entries.push(("publicKey", pk));
550 }
551 if let Some(pks) = self.params.public_keys.as_deref() {
552 entries.push(("publicKeys", pks));
553 }
554 entries.sort_by(|a, b| a.0.cmp(b.0));
555 write_canonical_query(&mut out, &entries);
556 }
557 FlakeRefType::Resource(res) if matches!(res.res_type, ResourceType::Mercurial) => {
558 write_resource_base(&mut out, res);
559 let mut entries: Vec<(&str, &str)> = Vec::new();
560 if let Some(r) = res.ref_.as_deref() {
561 entries.push(("ref", r));
562 }
563 if let Some(v) = res.rev.as_deref() {
564 entries.push(("rev", v));
565 }
566 entries.sort_by(|a, b| a.0.cmp(b.0));
567 write_canonical_query(&mut out, &entries);
568 }
569 _ => {
570 return self.to_string();
574 }
575 }
576
577 if let Some(fragment) = &self.fragment {
578 write!(&mut out, "#{}", encoding::encode_fragment(fragment)).unwrap();
579 }
580 out
581 }
582}
583
584fn write_resource_base(out: &mut String, res: &ResourceUrl) {
585 use std::fmt::Write;
586 write!(out, "{}", res.res_type).unwrap();
590 if let Some(transport) = &res.transport_type {
591 write!(out, "+{}", transport).unwrap();
592 }
593 write!(out, "://{}", res.location).unwrap();
594}
595
596fn write_canonical_query(out: &mut String, entries: &[(&str, &str)]) {
597 use std::fmt::Write;
598 if entries.is_empty() {
599 return;
600 }
601 out.push('?');
602 for (i, (key, value)) in entries.iter().enumerate() {
603 if i > 0 {
604 out.push('&');
605 }
606 write!(
607 out,
608 "{key}={value}",
609 key = encoding::encode_query(key),
610 value = encoding::encode_query(value)
611 )
612 .unwrap();
613 }
614}
615
616impl Display for FlakeRef {
617 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
618 write!(f, "{}", self.kind())?;
619
620 let mut entries: Vec<(&str, &str)> = self.params.entries();
626 if matches!(self.ref_source_location(), RefLocation::QueryParameter) {
627 let (ref_, rev) = match self.kind() {
633 FlakeRefType::GitForge(GitForge { ref_, rev, .. })
634 | FlakeRefType::Indirect { ref_, rev, .. } => (ref_.as_deref(), rev.as_deref()),
635 FlakeRefType::Resource(res) => (res.ref_.as_deref(), res.rev.as_deref()),
636 FlakeRefType::Path { rev, .. } => (None, rev.as_deref()),
637 };
638 if let Some(r) = ref_ {
639 entries.push(("ref", r));
640 }
641 if let Some(v) = rev {
642 entries.push(("rev", v));
643 }
644 }
645 entries.sort_by(|a, b| a.0.cmp(b.0));
646 if !entries.is_empty() {
647 write!(f, "?")?;
648 for (i, (key, value)) in entries.iter().enumerate() {
649 if i > 0 {
650 write!(f, "&")?;
651 }
652 write!(
653 f,
654 "{key}={value}",
655 key = encoding::encode_query(key),
656 value = encoding::encode_query(value)
657 )?;
658 }
659 }
660 if let Some(fragment) = &self.fragment {
661 write!(f, "#{}", encoding::encode_fragment(fragment))?;
662 }
663 Ok(())
664 }
665}
666
667impl TryFrom<&str> for FlakeRef {
668 type Error = NixUriError;
669
670 fn try_from(value: &str) -> Result<Self, Self::Error> {
671 use crate::parser::parse_nix_uri;
672 parse_nix_uri(value)
673 }
674}
675
676impl std::str::FromStr for FlakeRef {
677 type Err = NixUriError;
678
679 fn from_str(s: &str) -> Result<Self, Self::Err> {
680 use crate::parser::parse_nix_uri;
681 parse_nix_uri(s)
682 }
683}
684
685#[cfg(test)]
686mod tests {
687
688 use cool_asserts::assert_matches;
689 use resource_url::{ResourceType, ResourceUrl};
690 use winnow::Parser;
691
692 use super::*;
693 use crate::{
694 NixUriResult,
695 parser::{parse_nix_uri, parse_params, route_location_params},
696 };
697
698 #[test]
699 fn parse_simple_uri() {
700 let uri = "github:nixos/nixpkgs";
701 let expected = FlakeRef::default().with_kind(FlakeRefType::GitForge(GitForge {
702 platform: GitForgePlatform::GitHub,
703 owner: "nixos".into(),
704 repo: "nixpkgs".into(),
705 ref_: None,
706 rev: None,
707 location: RefLocation::PathComponent,
708 }));
709
710 let parsed: FlakeRef = uri.try_into().unwrap();
711 assert_eq!(expected, parsed);
712 }
713
714 #[test]
715 fn parse_simple_uri_parsed() {
716 let uri = "github:zellij-org/zellij";
717 let expected = FlakeRef::default().with_kind(FlakeRefType::GitForge(GitForge {
718 platform: GitForgePlatform::GitHub,
719 owner: "zellij-org".into(),
720 repo: "zellij".into(),
721 ref_: None,
722 rev: None,
723 location: RefLocation::PathComponent,
724 }));
725
726 let parsed: FlakeRef = uri.parse().unwrap();
727 assert_eq!(expected, parsed);
728 }
729
730 #[test]
731 fn parse_simple_uri_no_params() {
732 let uri = "github:zellij-org/zellij";
733 let parsed = parse_params.parse_peek(uri).unwrap().1;
734 assert_eq!(("github:zellij-org/zellij", None), parsed);
735 }
736
737 #[test]
738 fn parse_simple_uri_attr_with_params() {
739 let uri = "github:zellij-org/zellij?dir=assets";
740 let mut location_params = LocationParameters::default();
741 location_params.dir(Some("assets".into()));
742 let (head, raw_values) = parse_params.parse_peek(uri).unwrap().1;
743 assert_eq!("github:zellij-org/zellij", head);
744 let (params, ref_rev) = route_location_params(raw_values.unwrap()).unwrap();
745 assert_eq!(location_params, params);
746 assert!(ref_rev.r#ref.is_none() && ref_rev.rev.is_none());
747 }
748
749 #[test]
750 fn parse_simple_uri_ref() {
751 let uri = "github:zellij-org/zellij?ref=main";
752 let flake_ref = FlakeRef::default().with_kind(FlakeRefType::GitForge(GitForge {
753 platform: GitForgePlatform::GitHub,
754 owner: "zellij-org".into(),
755 repo: "zellij".into(),
756 ref_: Some("main".into()),
757 rev: None,
758 location: RefLocation::QueryParameter,
759 }));
760
761 let parsed = parse_nix_uri(uri).unwrap();
762 assert_eq!(flake_ref, parsed);
763 }
764
765 #[test]
766 fn parse_simple_uri_rev() {
767 let uri = "github:zellij-org/zellij?rev=b2df4e4e80e04cbb33a350f87717f4bd6140d298";
768 let flake_ref = FlakeRef::default().with_kind(FlakeRefType::GitForge(GitForge {
769 platform: GitForgePlatform::GitHub,
770 owner: "zellij-org".into(),
771 repo: "zellij".into(),
772 ref_: None,
773 rev: Some("b2df4e4e80e04cbb33a350f87717f4bd6140d298".into()),
774 location: RefLocation::QueryParameter,
775 }));
776
777 let parsed = parse_nix_uri(uri).unwrap();
778 assert_eq!(flake_ref, parsed);
779 }
780
781 #[test]
782 fn parse_simple_uri_ref_or_rev() {
783 let uri = "github:zellij-org/zellij/main";
784 let flake_ref = FlakeRef::default().with_kind(FlakeRefType::GitForge(GitForge {
785 platform: GitForgePlatform::GitHub,
786 owner: "zellij-org".into(),
787 repo: "zellij".into(),
788 ref_: Some("main".into()),
789 rev: None,
790 location: RefLocation::PathComponent,
791 }));
792
793 let parsed = parse_nix_uri(uri).unwrap();
794 assert_eq!(flake_ref, parsed);
795 }
796
797 #[test]
798 fn parse_simple_uri_ref_or_rev_attr() {
799 let uri = "github:zellij-org/zellij/main?dir=assets";
800 let mut params = LocationParameters::default();
801 params.dir(Some("assets".into()));
802 let flake_ref = FlakeRef::default()
803 .with_kind(FlakeRefType::GitForge(GitForge {
804 platform: GitForgePlatform::GitHub,
805 owner: "zellij-org".into(),
806 repo: "zellij".into(),
807 ref_: Some("main".into()),
808 rev: None,
809 location: RefLocation::PathComponent,
810 }))
811 .with_params(params);
812
813 let parsed = parse_nix_uri(uri).unwrap();
814 assert_eq!(flake_ref, parsed);
815 }
816
817 #[test]
818 fn parse_simple_uri_attr() {
819 let uri = "github:zellij-org/zellij?dir=assets";
820 let mut params = LocationParameters::default();
821 params.dir(Some("assets".into()));
822 let flake_ref = FlakeRef::default()
823 .with_kind(FlakeRefType::GitForge(GitForge {
824 platform: GitForgePlatform::GitHub,
825 owner: "zellij-org".into(),
826 repo: "zellij".into(),
827 ref_: None,
828 rev: None,
829 location: RefLocation::PathComponent,
830 }))
831 .with_params(params);
832
833 let parsed = parse_nix_uri(uri).unwrap();
834 assert_eq!(flake_ref, parsed);
835 }
836 #[test]
837 fn parse_simple_uri_attr_nom_alt() {
838 let uri = "github:zellij-org/zellij/?dir=assets";
839 let mut params = LocationParameters::default();
840 params.dir(Some("assets".into()));
841 let flake_ref = FlakeRef::default()
842 .with_kind(FlakeRefType::GitForge(GitForge {
843 platform: GitForgePlatform::GitHub,
844 owner: "zellij-org".into(),
845 repo: "zellij".into(),
846 ref_: None,
847 rev: None,
848 location: RefLocation::PathComponent,
849 }))
850 .with_params(params);
851
852 let parsed = parse_nix_uri(uri).unwrap();
853 assert_eq!(flake_ref, parsed);
854 }
855 #[test]
856 fn parse_simple_uri_params_nom_alt() {
857 let uri = "github:zellij-org/zellij/?dir=assets&narHash=fakeHash256";
858 let mut params = LocationParameters::default();
859 params.dir(Some("assets".into()));
860 params.nar_hash(Some("fakeHash256".into()));
861 let flake_ref = FlakeRef::default()
862 .with_kind(FlakeRefType::GitForge(GitForge {
863 platform: GitForgePlatform::GitHub,
864 owner: "zellij-org".into(),
865 repo: "zellij".into(),
866 ref_: None,
867 rev: None,
868 location: RefLocation::PathComponent,
869 }))
870 .with_params(params);
871
872 let parsed = parse_nix_uri(uri).unwrap();
873 assert_eq!(flake_ref, parsed);
874 }
875
876 #[test]
877 fn parse_simple_path_nom() {
878 let uri = "path:/home/kenji/.config/dotfiles/";
879 let flake_ref = FlakeRef::default().with_kind(FlakeRefType::Path {
880 path: "/home/kenji/.config/dotfiles/".into(),
881 rev: None,
882 });
883
884 let parsed = parse_nix_uri(uri).unwrap();
885 assert_eq!(flake_ref, parsed, "{}", uri);
886 }
887
888 #[test]
889 fn parse_simple_path_params_nom() {
890 let uri = "path:/home/kenji/.config/dotfiles/?dir=assets";
891 let mut params = LocationParameters::default();
892 params.dir(Some("assets".into()));
893 let flake_ref = FlakeRef::default()
894 .with_kind(FlakeRefType::Path {
895 path: "/home/kenji/.config/dotfiles/".into(),
896 rev: None,
897 })
898 .with_params(params);
899
900 let parsed = parse_nix_uri(uri).unwrap();
901 assert_eq!(flake_ref, parsed, "{}", uri);
902 }
903
904 #[test]
905 fn parse_gitlab_simple() {
906 let uri = "gitlab:veloren/veloren";
907 let flake_ref = FlakeRef::default().with_kind(FlakeRefType::GitForge(GitForge {
908 platform: GitForgePlatform::GitLab,
909 owner: "veloren".into(),
910 repo: "veloren".into(),
911 ref_: None,
912 rev: None,
913 location: RefLocation::PathComponent,
914 }));
915
916 let parsed = parse_nix_uri(uri).unwrap();
917 assert_eq!(flake_ref, parsed);
918 }
919
920 #[test]
921 fn parse_gitlab_simple_ref_or_rev() {
922 let uri = "gitlab:veloren/veloren/master";
923 let flake_ref = FlakeRef::default().with_kind(FlakeRefType::GitForge(GitForge {
924 platform: GitForgePlatform::GitLab,
925 owner: "veloren".into(),
926 repo: "veloren".into(),
927 ref_: Some("master".into()),
928 rev: None,
929 location: RefLocation::PathComponent,
930 }));
931
932 let parsed = parse_nix_uri(uri).unwrap();
933 assert_eq!(flake_ref, parsed);
934 }
935
936 #[test]
937 fn parse_gitlab_simple_ref_or_rev_alt() {
938 let uri = "gitlab:veloren/veloren/19742bb9300fb0be9fdc92f30766c95230a8a371";
939 let flake_ref = FlakeRef::default().with_kind(FlakeRefType::GitForge(GitForge {
940 platform: GitForgePlatform::GitLab,
941 owner: "veloren".into(),
942 repo: "veloren".into(),
943 ref_: None,
944 rev: Some("19742bb9300fb0be9fdc92f30766c95230a8a371".into()),
945 location: RefLocation::PathComponent,
946 }));
947
948 let parsed = parse_nix_uri(uri).unwrap();
949 assert_eq!(flake_ref, parsed);
950 }
951
952 #[test]
953 fn parse_gitlab_nested_subgroup() {
954 let uri = "gitlab:veloren%2Fdev/rfcs";
955 let parsed = parse_nix_uri(uri).unwrap();
956 let flake_ref = FlakeRef::default().with_kind(FlakeRefType::GitForge(GitForge {
957 platform: GitForgePlatform::GitLab,
958 owner: "veloren/dev".into(),
959 repo: "rfcs".into(),
960 ref_: None,
961 rev: None,
962 location: RefLocation::PathComponent,
963 }));
964 assert_eq!(flake_ref, parsed);
965 assert_eq!(parsed.to_string(), uri);
967 }
968
969 #[test]
970 fn parse_gitlab_simple_host_param() {
971 let uri = "gitlab:openldap/openldap?host=git.openldap.org";
972 let mut params = LocationParameters::default();
973 params.host(Some("git.openldap.org".into()));
974 let flake_ref = FlakeRef::default()
975 .with_kind(FlakeRefType::GitForge(GitForge {
976 platform: GitForgePlatform::GitLab,
977 owner: "openldap".into(),
978 repo: "openldap".into(),
979 ref_: None,
980 rev: None,
981 location: RefLocation::PathComponent,
982 }))
983 .with_params(params);
984
985 let parsed = parse_nix_uri(uri).unwrap();
986 assert_eq!(flake_ref, parsed);
987 }
988
989 #[test]
990 fn parse_git_and_https_simple() {
991 let uri = "git+https://git.somehost.tld/user/path";
992 let expected = FlakeRef::default().with_kind(FlakeRefType::Resource(ResourceUrl {
993 res_type: ResourceType::Git,
994 location: "git.somehost.tld/user/path".into(),
995 transport_type: Some(TransportLayer::Https),
996 ref_: None,
997 rev: None,
998 ref_location: RefLocation::PathComponent,
999 }));
1000
1001 let parsed: FlakeRef = uri.try_into().unwrap();
1002 assert_eq!(expected, parsed);
1003 }
1004
1005 #[test]
1006 fn parse_git_and_https_params() {
1007 let uri = "git+https://git.somehost.tld/user/path?ref=branch&rev=fdc8ef970de2b4634e1b3dca296e1ed918459a9e";
1008 let parsed: FlakeRef = uri.try_into().unwrap();
1009 assert_eq!(parsed.to_string(), uri);
1010 }
1011
1012 #[test]
1013 fn parse_git_and_file_params() {
1014 let uri = "git+file:///nix/nixpkgs?ref=upstream/nixpkgs-unstable";
1015 let parsed: FlakeRef = uri.try_into().unwrap();
1016 assert_eq!(parsed.to_string(), uri);
1017 }
1018
1019 #[test]
1020 fn parse_git_and_file_simple() {
1021 let uri = "git+file:///nix/nixpkgs";
1022 let expected = FlakeRef::default().with_kind(FlakeRefType::Resource(ResourceUrl {
1023 res_type: ResourceType::Git,
1024 location: "/nix/nixpkgs".into(),
1025 transport_type: Some(TransportLayer::File),
1026 ref_: None,
1027 rev: None,
1028 ref_location: RefLocation::PathComponent,
1029 }));
1030
1031 let parsed: FlakeRef = uri.try_into().unwrap();
1032 assert_eq!(expected, parsed);
1033 }
1034
1035 #[test]
1036 fn parse_git_and_file_branch_query_routes_to_arbitrary() {
1037 let uri = "git+file:///home/user/forked-flake?branch=feat/myNewFeature";
1042 let parsed: FlakeRef = uri.parse().expect("unrecognised key must parse");
1043 assert!(
1044 parsed
1045 .params()
1046 .entries()
1047 .iter()
1048 .any(|(k, v)| *k == "branch" && *v == "feat/myNewFeature"),
1049 "branch=feat/myNewFeature must land in arbitrary",
1050 );
1051 }
1052
1053 #[test]
1054 fn parse_github_simple_tag_non_alphabetic_params() {
1055 let uri = "github:smunix/MyST-Parser?ref=fix.hls-docutils";
1056 let expected = FlakeRef::default().with_kind(FlakeRefType::GitForge(GitForge {
1057 platform: GitForgePlatform::GitHub,
1058 owner: "smunix".into(),
1059 repo: "MyST-Parser".into(),
1060 ref_: Some("fix.hls-docutils".to_owned()),
1061 rev: None,
1062 location: RefLocation::QueryParameter,
1063 }));
1064
1065 let parsed: FlakeRef = uri.try_into().unwrap();
1066 assert_eq!(expected, parsed);
1067 }
1068
1069 #[test]
1070 fn parse_github_simple_tag() {
1071 let uri = "github:cachix/devenv/v0.5";
1072 let expected = FlakeRef::default().with_kind(FlakeRefType::GitForge(GitForge {
1073 platform: GitForgePlatform::GitHub,
1074 owner: "cachix".into(),
1075 repo: "devenv".into(),
1076 ref_: Some("v0.5".into()),
1077 rev: None,
1078 location: RefLocation::PathComponent,
1079 }));
1080
1081 let parsed: FlakeRef = uri.try_into().unwrap();
1082 assert_eq!(expected, parsed);
1083 }
1084
1085 #[test]
1086 fn parse_gitlab_with_host_params_alt() {
1087 let uri = "gitlab:fpottier/menhir/20201216?host=gitlab.inria.fr";
1088 let mut params = LocationParameters::default();
1089 params.set_host(Some("gitlab.inria.fr".into()));
1090 let expected = FlakeRef::default()
1091 .with_kind(FlakeRefType::GitForge(GitForge {
1092 platform: GitForgePlatform::GitLab,
1093 owner: "fpottier".to_owned(),
1094 repo: "menhir".to_owned(),
1095 ref_: Some("20201216".to_owned()),
1096 rev: None,
1097 location: RefLocation::PathComponent,
1098 }))
1099 .with_params(params);
1100
1101 let parsed: FlakeRef = uri.try_into().unwrap();
1102 assert_eq!(expected, parsed);
1103 }
1104
1105 #[test]
1106 fn parse_git_and_https_params_submodules() {
1107 let uri = "git+https://www.github.com/ocaml/ocaml-lsp?submodules=1";
1108 let mut params = LocationParameters::default();
1109 params.set_submodules(Some(true));
1110 let expected = FlakeRef::default()
1111 .with_kind(FlakeRefType::Resource(ResourceUrl {
1112 res_type: ResourceType::Git,
1113 location: "www.github.com/ocaml/ocaml-lsp".to_owned(),
1114 transport_type: Some(TransportLayer::Https),
1115 ref_: None,
1116 rev: None,
1117 ref_location: RefLocation::PathComponent,
1118 }))
1119 .with_params(params);
1120
1121 let parsed: FlakeRef = uri.try_into().unwrap();
1122 assert_eq!(expected, parsed);
1123 }
1124
1125 #[test]
1126 fn parse_marcurial_and_https_simpe_uri() {
1127 let uri = "hg+https://www.github.com/ocaml/ocaml-lsp";
1128 let expected = FlakeRef::default().with_kind(FlakeRefType::Resource(ResourceUrl {
1129 res_type: ResourceType::Mercurial,
1130 location: "www.github.com/ocaml/ocaml-lsp".to_owned(),
1131 transport_type: Some(TransportLayer::Https),
1132 ref_: None,
1133 rev: None,
1134 ref_location: RefLocation::PathComponent,
1135 }));
1136
1137 let parsed: FlakeRef = uri.try_into().unwrap();
1138 assert_eq!(expected, parsed);
1139 }
1140
1141 #[test]
1142 #[should_panic(expected = "Unsupported(UriType { ty: \"gt+https\" })")]
1143 fn parse_git_and_https_params_submodules_wrong_type() {
1144 let uri = "gt+https://www.github.com/ocaml/ocaml-lsp?submodules=1";
1145 let mut params = LocationParameters::default();
1146 params.set_submodules(Some(true));
1147 let expected = FlakeRef::default()
1148 .with_kind(FlakeRefType::Resource(ResourceUrl {
1149 res_type: ResourceType::Git,
1150 location: "www.github.com/ocaml/ocaml-lsp".to_owned(),
1151 transport_type: Some(TransportLayer::Https),
1152 ref_: None,
1153 rev: None,
1154 ref_location: RefLocation::PathComponent,
1155 }))
1156 .with_params(params);
1157
1158 let parsed: FlakeRef = uri.try_into().unwrap();
1159 assert_eq!(expected, parsed);
1160 }
1161
1162 #[test]
1164 fn parse_git_and_file_shallow() {
1165 let uri = "git+file:/path/to/repo?shallow=1";
1166 let mut params = LocationParameters::default();
1167 params.set_shallow(Some(true));
1168 let expected = FlakeRef::default()
1169 .with_kind(FlakeRefType::Resource(ResourceUrl {
1170 res_type: ResourceType::Git,
1171 location: "/path/to/repo".to_owned(),
1172 transport_type: Some(TransportLayer::File),
1173 ref_: None,
1174 rev: None,
1175 ref_location: RefLocation::PathComponent,
1176 }))
1177 .with_params(params);
1178
1179 let parsed: FlakeRef = uri.try_into().unwrap();
1180 assert_eq!(expected, parsed);
1181 }
1182
1183 #[test]
1184 fn parse_simple_path_uri_indirect() {
1185 let uri = "path:../.";
1186 let expected = FlakeRef::default().with_kind(FlakeRefType::Path {
1187 path: "../.".to_owned(),
1188 rev: None,
1189 });
1190 let parsed: FlakeRef = uri.try_into().unwrap();
1191 assert_eq!(expected, parsed);
1192 }
1193
1194 #[test]
1195 fn parse_path_uri_empty_body_rejected() {
1196 for uri in ["path:", "path: ", "path: "] {
1202 let result: Result<FlakeRef, _> = uri.try_into();
1203 assert!(
1204 matches!(result, Err(NixUriError::InvalidUrl(_))),
1205 "expected InvalidUrl for {uri:?}, got {result:?}"
1206 );
1207 }
1208 }
1209
1210 #[test]
1211 fn parse_simple_path_uri_indirect_local() {
1212 let uri = "path:.";
1213 let expected = FlakeRef::default().with_kind(FlakeRefType::Path {
1214 path: ".".to_owned(),
1215 rev: None,
1216 });
1217 let parsed: FlakeRef = uri.try_into().unwrap();
1218 assert_eq!(expected, parsed);
1219 }
1220
1221 #[test]
1222 fn parse_simple_uri_sourcehut() {
1223 let uri = "sourcehut:~misterio/nix-colors";
1224 let expected = FlakeRef::default().with_kind(FlakeRefType::GitForge(GitForge {
1225 platform: GitForgePlatform::SourceHut,
1226 owner: "~misterio".to_owned(),
1227 repo: "nix-colors".to_owned(),
1228 ref_: None,
1229 rev: None,
1230 location: RefLocation::PathComponent,
1231 }));
1232
1233 let parsed: FlakeRef = uri.try_into().unwrap();
1234 assert_eq!(expected, parsed);
1235 }
1236
1237 #[test]
1238 fn parse_simple_uri_sourcehut_rev() {
1239 let uri = "sourcehut:~misterio/nix-colors/main";
1240 let expected = FlakeRef::default().with_kind(FlakeRefType::GitForge(GitForge {
1241 platform: GitForgePlatform::SourceHut,
1242 owner: "~misterio".to_owned(),
1243 repo: "nix-colors".to_owned(),
1244 ref_: Some("main".to_owned()),
1245 rev: None,
1246 location: RefLocation::PathComponent,
1247 }));
1248
1249 let parsed: FlakeRef = uri.try_into().unwrap();
1250 assert_eq!(expected, parsed);
1251 }
1252
1253 #[test]
1254 fn parse_simple_uri_sourcehut_host_param() {
1255 let uri = "sourcehut:~misterio/nix-colors?host=git.example.org";
1256 let mut params = LocationParameters::default();
1257 params.set_host(Some("git.example.org".into()));
1258 let expected = FlakeRef::default()
1259 .with_kind(FlakeRefType::GitForge(GitForge {
1260 platform: GitForgePlatform::SourceHut,
1261 owner: "~misterio".to_owned(),
1262 repo: "nix-colors".to_owned(),
1263 ref_: None,
1264 rev: None,
1265 location: RefLocation::PathComponent,
1266 }))
1267 .with_params(params);
1268
1269 let parsed: FlakeRef = uri.try_into().unwrap();
1270 assert_eq!(expected, parsed);
1271 }
1272
1273 #[test]
1274 fn parse_simple_uri_sourcehut_ref() {
1275 let uri = "sourcehut:~misterio/nix-colors/182b4b8709b8ffe4e9774a4c5d6877bf6bb9a21c";
1276 let expected = FlakeRef::default().with_kind(FlakeRefType::GitForge(GitForge {
1277 platform: GitForgePlatform::SourceHut,
1278 owner: "~misterio".to_owned(),
1279 repo: "nix-colors".to_owned(),
1280 ref_: None,
1281 rev: Some("182b4b8709b8ffe4e9774a4c5d6877bf6bb9a21c".to_owned()),
1282 location: RefLocation::PathComponent,
1283 }));
1284
1285 let parsed: FlakeRef = uri.try_into().unwrap();
1286 assert_eq!(expected, parsed);
1287 }
1288
1289 #[test]
1290 fn parse_simple_uri_sourcehut_ref_params() {
1291 let uri =
1292 "sourcehut:~misterio/nix-colors/21c1a380a6915d890d408e9f22203436a35bb2de?host=hg.sr.ht";
1293 let mut params = LocationParameters::default();
1294 params.set_host(Some("hg.sr.ht".into()));
1295 let expected = FlakeRef::default()
1296 .with_kind(FlakeRefType::GitForge(GitForge {
1297 platform: GitForgePlatform::SourceHut,
1298 owner: "~misterio".to_owned(),
1299 repo: "nix-colors".to_owned(),
1300 ref_: None,
1301 rev: Some("21c1a380a6915d890d408e9f22203436a35bb2de".to_owned()),
1302 location: RefLocation::PathComponent,
1303 }))
1304 .with_params(params);
1305
1306 let parsed: FlakeRef = uri.try_into().unwrap();
1307 assert_eq!(expected, parsed);
1308 }
1309
1310 #[test]
1311 fn display_simple_sourcehut_uri_ref_or_rev() {
1312 let expected = "sourcehut:~misterio/nix-colors/21c1a380a6915d890d408e9f22203436a35bb2de";
1313 let flake_ref = FlakeRef::default()
1314 .with_kind(FlakeRefType::GitForge(GitForge {
1315 platform: GitForgePlatform::SourceHut,
1316 owner: "~misterio".to_owned(),
1317 repo: "nix-colors".to_owned(),
1318 ref_: None,
1319 rev: Some("21c1a380a6915d890d408e9f22203436a35bb2de".to_owned()),
1320 location: RefLocation::PathComponent,
1321 }))
1322 .to_string();
1323
1324 assert_eq!(expected, flake_ref);
1325 }
1326
1327 #[test]
1328 fn display_simple_sourcehut_uri_ref_or_rev_host_param() {
1329 let expected =
1330 "sourcehut:~misterio/nix-colors/21c1a380a6915d890d408e9f22203436a35bb2de?host=hg.sr.ht";
1331 let mut params = LocationParameters::default();
1332 params.set_host(Some("hg.sr.ht".into()));
1333 let flake_ref = FlakeRef::default()
1334 .with_kind(FlakeRefType::GitForge(GitForge {
1335 platform: GitForgePlatform::SourceHut,
1336 owner: "~misterio".to_owned(),
1337 repo: "nix-colors".to_owned(),
1338 ref_: None,
1339 rev: Some("21c1a380a6915d890d408e9f22203436a35bb2de".to_owned()),
1340 location: RefLocation::PathComponent,
1341 }))
1342 .with_params(params)
1343 .to_string();
1344
1345 assert_eq!(expected, flake_ref);
1346 }
1347
1348 #[test]
1349 fn display_simple_github_uri_ref() {
1350 let expected = "github:zellij-org/zellij?ref=main";
1351 let flake_ref = FlakeRef::default()
1352 .with_kind(FlakeRefType::GitForge(GitForge {
1353 platform: GitForgePlatform::GitHub,
1354 owner: "zellij-org".into(),
1355 repo: "zellij".into(),
1356 ref_: Some("main".into()),
1357 rev: None,
1358 location: RefLocation::QueryParameter,
1359 }))
1360 .to_string();
1361
1362 assert_eq!(flake_ref, expected);
1363 }
1364
1365 #[test]
1366 fn display_simple_github_uri_rev() {
1367 let expected = "github:zellij-org/zellij?rev=b2df4e4e80e04cbb33a350f87717f4bd6140d298";
1368 let flake_ref = FlakeRef::default()
1369 .with_kind(FlakeRefType::GitForge(GitForge {
1370 platform: GitForgePlatform::GitHub,
1371 owner: "zellij-org".into(),
1372 repo: "zellij".into(),
1373 ref_: None,
1374 rev: Some("b2df4e4e80e04cbb33a350f87717f4bd6140d298".into()),
1375 location: RefLocation::QueryParameter,
1376 }))
1377 .to_string();
1378
1379 assert_eq!(flake_ref, expected);
1380 }
1381
1382 #[test]
1383 fn parse_simple_path_uri_indirect_absolute_without_prefix() {
1384 let uri = "/home/kenji/git";
1385 let expected = FlakeRef::default().with_kind(FlakeRefType::Path {
1386 path: "/home/kenji/git".to_owned(),
1387 rev: None,
1388 });
1389
1390 let parsed: FlakeRef = uri.try_into().unwrap();
1391 assert_eq!(expected, parsed);
1392 }
1393
1394 #[test]
1395 fn parse_simple_path_uri_indirect_absolute_without_prefix_with_params() {
1396 let uri = "/home/kenji/git?dir=dev";
1397 let mut params = LocationParameters::default();
1398 params.set_dir(Some("dev".into()));
1399 let expected = FlakeRef::default()
1400 .with_kind(FlakeRefType::Path {
1401 path: "/home/kenji/git".to_owned(),
1402 rev: None,
1403 })
1404 .with_params(params);
1405
1406 let parsed: FlakeRef = uri.try_into().unwrap();
1407 assert_eq!(expected, parsed);
1408 }
1409
1410 #[test]
1411 fn parse_simple_path_uri_indirect_local_without_prefix() {
1412 let uri = ".";
1413 let expected = FlakeRef::default().with_kind(FlakeRefType::Path {
1414 path: ".".to_owned(),
1415 rev: None,
1416 });
1417 let parsed: FlakeRef = uri.try_into().unwrap();
1418 assert_eq!(expected, parsed);
1419 }
1420
1421 #[test]
1422 fn parse_wrong_git_uri_extension_type() {
1423 let uri = "git+(:z";
1424 let parsed: NixUriResult<FlakeRef> = uri.try_into();
1425 let parsed = parsed.unwrap_err();
1426 assert_matches!(
1427 parsed,
1428 NixUriError::Unsupported(UnsupportedReason::TransportLayer { ty })
1429 => assert_eq!("(", ty)
1430 );
1431 }
1432
1433 #[test]
1434 fn parse_github_missing_parameter_public_surface() {
1435 use crate::ParseExpected;
1436
1437 assert_matches!(
1438 parse_nix_uri("github:"),
1439 Err(NixUriError::Parse {
1440 position: 7,
1441 expected: ParseExpected::Label("TakeTill1"),
1442 })
1443 );
1444 }
1445
1446 #[test]
1447 fn parse_github_missing_parameter_repo_public_surface() {
1448 use crate::ParseExpected;
1449
1450 assert_matches!(
1451 parse_nix_uri("github:nixos/"),
1452 Err(NixUriError::Parse {
1453 position: 13,
1454 expected: ParseExpected::Label("TakeTill1"),
1455 })
1456 );
1457 }
1458
1459 #[test]
1460 fn parse_resource_missing_separator_pins_tag_variant() {
1461 use crate::ParseExpected;
1462
1463 assert_matches!(
1464 parse_nix_uri("git:x"),
1465 Err(NixUriError::Parse {
1466 position: 4,
1467 expected: ParseExpected::Tag("//"),
1468 })
1469 );
1470 }
1471
1472 #[test]
1473 fn parse_github_starts_with_whitespace() {
1474 let uri = " github:nixos/nixpkgs";
1475 assert_matches!(
1476 uri.parse::<FlakeRef>(),
1477 Err(NixUriError::InvalidUrl(uri_match)) => assert_eq!(uri, uri_match)
1478 );
1479 }
1480
1481 #[test]
1482 fn parse_github_ends_with_whitespace() {
1483 let uri = "github:nixos/nixpkgs ";
1484 assert_matches!(
1485 uri.parse::<FlakeRef>(),
1486 Err(NixUriError::InvalidUrl(uri_match)) => assert_eq!(uri, uri_match)
1487 );
1488 }
1489
1490 #[test]
1491 fn parse_empty_invalid_url() {
1492 let uri = "";
1493 assert_matches!(
1494 uri.parse::<FlakeRef>().unwrap_err(),
1495 NixUriError::InvalidUrl(uri) => assert_eq!("", uri)
1496 );
1497 }
1498
1499 #[test]
1500 fn parse_empty_trim_invalid_url() {
1501 let uri = " ";
1502 assert_matches!(
1503 uri.parse::<FlakeRef>().unwrap_err(),
1504 NixUriError::InvalidUrl(uri_match) => assert_eq!(uri, uri_match)
1505 );
1506 }
1507
1508 #[test]
1509 fn parse_slash_trim_invalid_url() {
1510 let uri = " / ";
1511 assert_matches!(
1512 uri.parse::<FlakeRef>().unwrap_err(),
1513 NixUriError::InvalidUrl(uri_match) => assert_eq!(uri, uri_match)
1514 );
1515 }
1516
1517 #[test]
1518 fn parse_double_trim_invalid_url() {
1519 let uri = " : ";
1520 assert_matches!(
1521 uri.parse::<FlakeRef>().unwrap_err(),
1522 NixUriError::InvalidUrl(uri_match) => assert_eq!(uri, uri_match)
1523 );
1524 }
1525
1526 #[test]
1527 fn indirect_display_emits_flake_prefix() {
1528 let parsed: FlakeRef = "flake:nixpkgs/release-23.05".parse().unwrap();
1531 assert_eq!(parsed.to_string(), "flake:nixpkgs/release-23.05");
1532
1533 let parsed: FlakeRef = "nixpkgs".parse().unwrap();
1534 assert_eq!(parsed.to_string(), "flake:nixpkgs");
1535 }
1536
1537 #[test]
1538 fn path_display_emits_path_prefix() {
1539 let parsed: FlakeRef = "path:./foo".parse().unwrap();
1542 assert_eq!(parsed.to_string(), "path:./foo");
1543
1544 let parsed: FlakeRef = "/abs/path".parse().unwrap();
1545 assert_eq!(parsed.to_string(), "path:/abs/path");
1546 }
1547
1548 #[test]
1549 fn indirect_explicit_three_segment_round_trip() {
1550 let uri = "flake:nixpkgs/release-23.05/abc1234567890123456789012345678901234567";
1553 let parsed: FlakeRef = uri.parse().unwrap();
1554 assert_eq!(parsed.to_string(), uri);
1555 }
1556
1557 #[test]
1558 fn fragment_round_trip_github() {
1559 let uri = "github:nixos/nixpkgs#default";
1562 let parsed: FlakeRef = uri.parse().unwrap();
1563 assert_eq!(parsed.fragment.as_deref(), Some("default"));
1564 assert_eq!(parsed.to_string(), uri);
1565 }
1566
1567 #[test]
1568 fn fragment_round_trip_with_params() {
1569 let uri = "github:nixos/nixpkgs?dir=foo#bar";
1570 let parsed: FlakeRef = uri.parse().unwrap();
1571 assert_eq!(parsed.fragment.as_deref(), Some("bar"));
1572 assert_eq!(parsed.to_string(), uri);
1573 }
1574
1575 #[test]
1580 fn bare_two_segment_parses_as_indirect() {
1581 let parsed: FlakeRef = "nixos/nixpkgs".parse().unwrap();
1582 assert_eq!(
1583 *parsed.kind(),
1584 FlakeRefType::Indirect {
1585 id: "nixos".to_string(),
1586 ref_: Some("nixpkgs".to_string()),
1587 rev: None,
1588 location: RefLocation::PathComponent,
1589 },
1590 );
1591 assert_eq!(parsed.to_string(), "flake:nixos/nixpkgs");
1592 }
1593
1594 #[test]
1597 fn bare_three_segment_with_hex_parses_as_indirect() {
1598 let rev = "abc1234567890123456789012345678901234567";
1599 let uri = format!("nixos/nixpkgs/{rev}");
1600 let parsed: FlakeRef = uri.parse().unwrap();
1601 assert_eq!(
1602 *parsed.kind(),
1603 FlakeRefType::Indirect {
1604 id: "nixos".to_string(),
1605 ref_: Some("nixpkgs".to_string()),
1606 rev: Some(rev.to_string()),
1607 location: RefLocation::PathComponent,
1608 },
1609 );
1610 }
1611
1612 #[test]
1616 fn bare_four_segment_rejected() {
1617 let err = "nixos/nixpkgs/extra/parts"
1618 .parse::<FlakeRef>()
1619 .expect_err("bare four-segment must not parse");
1620 assert_matches!(err, NixUriError::MissingScheme { input } if input == "nixos/nixpkgs/extra/parts");
1621 }
1622
1623 #[test]
1628 fn flake_three_segment_non_hex_rejects() {
1629 let err = "flake:nixpkgs/release-23.05/notahex"
1630 .parse::<FlakeRef>()
1631 .expect_err("non-hex third segment must reject");
1632 assert_matches!(err, NixUriError::InvalidValue { field: "rev", .. },);
1633 }
1634
1635 #[test]
1638 fn flake_double_slash_collapses_skipempty() {
1639 let parsed: FlakeRef = "flake:nixpkgs//main".parse().unwrap();
1640 assert_eq!(
1641 *parsed.kind(),
1642 FlakeRefType::Indirect {
1643 id: "nixpkgs".to_string(),
1644 ref_: Some("main".to_string()),
1645 rev: None,
1646 location: RefLocation::PathComponent,
1647 },
1648 );
1649
1650 let parsed: FlakeRef = "flake:nixpkgs///main".parse().unwrap();
1651 assert_eq!(
1652 *parsed.kind(),
1653 FlakeRefType::Indirect {
1654 id: "nixpkgs".to_string(),
1655 ref_: Some("main".to_string()),
1656 rev: None,
1657 location: RefLocation::PathComponent,
1658 },
1659 );
1660 }
1661
1662 #[test]
1668 fn bare_double_slash_rejects() {
1669 let err = "//host/path"
1670 .parse::<FlakeRef>()
1671 .expect_err("bare //host/path must reject");
1672 assert_matches!(err, NixUriError::InvalidUrl(input) if input == "//host/path");
1673 }
1674
1675 #[test]
1679 fn bare_legitimate_paths_round_trip() {
1680 for (input, displayed) in [
1681 ("./relative", "path:./relative"),
1682 ("/abs/path", "path:/abs/path"),
1683 ] {
1684 let parsed: FlakeRef = input.parse().unwrap();
1685 assert!(matches!(parsed.kind(), FlakeRefType::Path { .. }));
1686 assert_eq!(parsed.to_string(), displayed);
1687 }
1688 }
1689
1690 #[test]
1694 fn flake_scheme_four_segment_rejected() {
1695 let err = "flake:nixpkgs/main/abc/extra"
1696 .parse::<FlakeRef>()
1697 .expect_err("flake: 4+ segments must not parse");
1698 assert_matches!(err, NixUriError::TooManyIndirectSegments { count: 4 });
1699 }
1700
1701 #[test]
1702 fn bare_single_segment_still_parses() {
1703 let parsed: FlakeRef = "nixpkgs".parse().unwrap();
1704 assert_eq!(
1705 *parsed.kind(),
1706 FlakeRefType::Indirect {
1707 id: "nixpkgs".to_string(),
1708 ref_: None,
1709 rev: None,
1710 location: RefLocation::PathComponent,
1711 },
1712 );
1713 }
1714}
1715
1716#[cfg(test)]
1717mod ref_rev_methods {
1718 use super::*;
1728 use rstest::rstest;
1729
1730 #[rstest]
1731 #[case(
1732 "github:nixos/nixpkgs/release-23.05",
1733 Some("release-23.05"),
1734 RefLocation::PathComponent
1735 )]
1736 #[case(
1737 "github:nixos/nixpkgs?ref=release-23.05",
1738 Some("release-23.05"),
1739 RefLocation::QueryParameter
1740 )]
1741 #[case(
1742 "github:nixos/nixpkgs?rev=b2df4e4e80e04cbb33a350f87717f4bd6140d298",
1743 Some("b2df4e4e80e04cbb33a350f87717f4bd6140d298"),
1744 RefLocation::QueryParameter
1745 )]
1746 #[case("flake:nixpkgs/unstable", Some("unstable"), RefLocation::PathComponent)]
1747 #[case("github:nixos/nixpkgs", None, RefLocation::PathComponent)]
1748 fn typed_ref_or_rev_round_trip(
1749 #[case] url: &str,
1750 #[case] expected_ref: Option<&str>,
1751 #[case] expected_location: RefLocation,
1752 ) {
1753 let parsed: FlakeRef = url.parse().unwrap();
1754 assert_eq!(
1755 parsed.ref_or_rev(),
1756 expected_ref,
1757 "ref_or_rev mismatch for {url}",
1758 );
1759 assert_eq!(
1760 parsed.ref_source_location(),
1761 expected_location,
1762 "ref_source_location mismatch for {url}",
1763 );
1764 }
1765
1766 #[test]
1767 fn set_ref_preserves_path_component_location() {
1768 let url = "github:nixos/nixpkgs/release-23.05";
1769 let mut parsed: FlakeRef = url.parse().unwrap();
1770
1771 assert_eq!(parsed.ref_source_location(), RefLocation::PathComponent);
1772 assert_eq!(parsed.ref_or_rev(), Some("release-23.05"));
1773
1774 parsed.set_ref(Some("release-24.05".to_string()));
1775
1776 assert_eq!(parsed.ref_source_location(), RefLocation::PathComponent);
1777 assert_eq!(parsed.ref_or_rev(), Some("release-24.05"));
1778 assert_eq!(parsed.to_string(), "github:nixos/nixpkgs/release-24.05");
1779 }
1780
1781 #[test]
1782 fn set_ref_preserves_query_parameter_location() {
1783 let url = "github:nixos/nixpkgs?ref=release-23.05";
1784 let mut parsed: FlakeRef = url.parse().unwrap();
1785
1786 assert_eq!(parsed.ref_source_location(), RefLocation::QueryParameter);
1787 assert_eq!(parsed.ref_or_rev(), Some("release-23.05"));
1788
1789 parsed.set_ref(Some("release-24.05".to_string()));
1790
1791 assert_eq!(parsed.ref_source_location(), RefLocation::QueryParameter);
1792 assert_eq!(parsed.ref_or_rev(), Some("release-24.05"));
1793 assert_eq!(parsed.to_string(), "github:nixos/nixpkgs?ref=release-24.05");
1794 }
1795
1796 #[test]
1797 fn set_ref_on_resource_writes_to_typed_slot_and_flips_location() {
1798 let url = "git+https://github.com/nixos/nixpkgs";
1803 let mut parsed: FlakeRef = url.parse().unwrap();
1804
1805 parsed.set_ref(Some("v1.0.0".to_string()));
1806 assert_eq!(parsed.ref_or_rev(), Some("v1.0.0"));
1807 assert_eq!(parsed.ref_source_location(), RefLocation::QueryParameter);
1808 match parsed.kind() {
1809 FlakeRefType::Resource(res) => {
1810 assert_eq!(res.ref_.as_deref(), Some("v1.0.0"));
1811 }
1812 other => panic!("expected Resource, got {other:?}"),
1813 }
1814 assert_eq!(
1816 parsed.to_string(),
1817 "git+https://github.com/nixos/nixpkgs?ref=v1.0.0",
1818 );
1819 }
1820
1821 #[test]
1822 fn set_ref_on_github_without_existing_ref_uses_path_component() {
1823 let url = "github:nixos/nixpkgs";
1824 let mut parsed: FlakeRef = url.parse().unwrap();
1825
1826 assert_eq!(parsed.ref_source_location(), RefLocation::PathComponent);
1829
1830 parsed.set_ref(Some("release-23.05".to_string()));
1831
1832 assert_eq!(parsed.ref_source_location(), RefLocation::PathComponent);
1833 assert_eq!(parsed.to_string(), "github:nixos/nixpkgs/release-23.05");
1834 }
1835
1836 #[test]
1837 fn set_rev_preserves_location() {
1838 let url = "github:nixos/nixpkgs/b2df4e4e80e04cbb33a350f87717f4bd6140d298";
1840 let mut parsed: FlakeRef = url.parse().unwrap();
1841
1842 parsed.set_rev(Some("c3ee5f5f91f15dcb44b461g98828g5ce7251e399".to_string()));
1843 assert_eq!(parsed.ref_source_location(), RefLocation::PathComponent);
1844 assert_eq!(
1845 parsed.to_string(),
1846 "github:nixos/nixpkgs/c3ee5f5f91f15dcb44b461g98828g5ce7251e399",
1847 );
1848
1849 let url2 = "github:nixos/nixpkgs?rev=b2df4e4e80e04cbb33a350f87717f4bd6140d298";
1851 let mut parsed2: FlakeRef = url2.parse().unwrap();
1852
1853 parsed2.set_rev(Some("c3ee5f5f91f15dcb44b461g98828g5ce7251e399".to_string()));
1854 assert_eq!(parsed2.ref_source_location(), RefLocation::QueryParameter);
1855 assert_eq!(
1856 parsed2.to_string(),
1857 "github:nixos/nixpkgs?rev=c3ee5f5f91f15dcb44b461g98828g5ce7251e399",
1858 );
1859 }
1860
1861 #[test]
1862 fn remove_ref_clears_value_and_drops_path_segment() {
1863 let url = "github:nixos/nixpkgs/release-23.05";
1865 let mut parsed: FlakeRef = url.parse().unwrap();
1866
1867 parsed.set_ref(None);
1868 assert_eq!(parsed.ref_or_rev(), None);
1869 assert_eq!(parsed.to_string(), "github:nixos/nixpkgs");
1870
1871 let url2 = "github:nixos/nixpkgs?ref=release-23.05";
1873 let mut parsed2: FlakeRef = url2.parse().unwrap();
1874
1875 parsed2.set_ref(None);
1876 assert_eq!(parsed2.ref_or_rev(), None);
1877 assert_eq!(parsed2.to_string(), "github:nixos/nixpkgs");
1878 }
1879
1880 #[test]
1881 fn indirect_set_ref_uses_path_component() {
1882 let url = "flake:nixpkgs";
1883 let mut parsed: FlakeRef = url.parse().unwrap();
1884
1885 parsed.set_ref(Some("unstable".to_string()));
1886 assert_eq!(parsed.ref_source_location(), RefLocation::PathComponent);
1887 assert_eq!(parsed.to_string(), "flake:nixpkgs/unstable");
1888 }
1889
1890 #[test]
1891 fn round_trip_path_component_ref() {
1892 let original = "github:nixos/nixpkgs/release-23.05";
1893 let parsed: FlakeRef = original.parse().unwrap();
1894 assert_eq!(parsed.to_string(), original);
1895 }
1896
1897 #[test]
1898 fn round_trip_query_parameter_ref() {
1899 let original = "github:nixos/nixpkgs?ref=release-23.05";
1900 let parsed: FlakeRef = original.parse().unwrap();
1901 assert_eq!(parsed.to_string(), original);
1902 }
1903
1904 #[test]
1905 fn round_trip_path_component_rev() {
1906 let original = "github:nixos/nixpkgs/b2df4e4e80e04cbb33a350f87717f4bd6140d298";
1907 let parsed: FlakeRef = original.parse().unwrap();
1908 assert_eq!(parsed.to_string(), original);
1909 match parsed.kind() {
1911 FlakeRefType::GitForge(forge) => {
1912 assert!(forge.ref_.is_none());
1913 assert_eq!(
1914 forge.rev.as_deref(),
1915 Some("b2df4e4e80e04cbb33a350f87717f4bd6140d298"),
1916 );
1917 }
1918 _ => panic!("expected GitForge"),
1919 }
1920 }
1921
1922 #[test]
1923 fn resource_set_ref_none_keeps_ref_location_when_rev_remains() {
1924 let url = "git+https://github.com/owner/repo?ref=main&rev=b2df4e4e80e04cbb33a350f87717f4bd6140d298";
1928 let mut parsed: FlakeRef = url.parse().unwrap();
1929 assert_eq!(parsed.ref_source_location(), RefLocation::QueryParameter);
1930
1931 parsed.set_ref(None);
1932 assert_eq!(parsed.ref_(), None);
1933 assert_eq!(
1934 parsed.rev(),
1935 Some("b2df4e4e80e04cbb33a350f87717f4bd6140d298"),
1936 );
1937 assert_eq!(
1938 parsed.ref_source_location(),
1939 RefLocation::QueryParameter,
1940 "clearing ref must not flip ref_location while rev is still set",
1941 );
1942
1943 parsed.set_rev(None);
1947 assert_eq!(parsed.ref_(), None);
1948 assert_eq!(parsed.rev(), None);
1949 assert_eq!(
1950 parsed.ref_source_location(),
1951 RefLocation::QueryParameter,
1952 "clearing rev must not flip ref_location either",
1953 );
1954 }
1955
1956 #[test]
1957 fn set_ref_and_rev_independently_on_gitforge() {
1958 let url = "github:owner/repo";
1959 let mut parsed: FlakeRef = url.parse().unwrap();
1960
1961 parsed.set_ref(Some("main".to_string()));
1962 match parsed.kind() {
1963 FlakeRefType::GitForge(forge) => {
1964 assert_eq!(forge.ref_.as_deref(), Some("main"));
1965 assert!(forge.rev.is_none());
1966 }
1967 _ => panic!("expected GitForge"),
1968 }
1969
1970 parsed.set_rev(Some("b2df4e4e80e04cbb33a350f87717f4bd6140d298".to_string()));
1972 match parsed.kind() {
1973 FlakeRefType::GitForge(forge) => {
1974 assert_eq!(forge.ref_.as_deref(), Some("main"));
1975 assert_eq!(
1976 forge.rev.as_deref(),
1977 Some("b2df4e4e80e04cbb33a350f87717f4bd6140d298"),
1978 );
1979 }
1980 _ => panic!("expected GitForge"),
1981 }
1982 }
1983}
1984
1985#[cfg(test)]
1986mod canonical_round_trip {
1987 use super::*;
1993 use rstest::rstest;
1994
1995 #[rstest]
1996 #[case("github:nixos/nixpkgs/release-23.05")]
1997 #[case("github:nixos/nixpkgs?ref=release-23.05")]
1998 #[case("git+https://github.com/owner/repo?ref=v1.0.0")]
1999 #[case("flake:nixpkgs/release-23.05/abc1234567890123456789012345678901234567")]
2000 #[case("path:./foo")]
2001 #[case("github:nixos/nixpkgs#default")]
2002 #[case("git+https://example.com/repo?lastModified=12345&narHash=sha256-abc&revCount=42")]
2006 fn round_trip(#[case] uri: &str) {
2007 let parsed: FlakeRef = uri.parse().unwrap();
2008 assert_eq!(parsed.to_string(), uri, "round-trip mismatch");
2009 }
2010
2011 #[test]
2012 fn query_keys_emit_alphabetical_across_typed_and_arbitrary() {
2013 let input = "git+https://example.com/repo?narHash=sha256-x&dir=foo&name=my-flake";
2019 let parsed: FlakeRef = input.parse().unwrap();
2020 assert_eq!(
2021 parsed.to_string(),
2022 "git+https://example.com/repo?dir=foo&name=my-flake&narHash=sha256-x"
2023 );
2024
2025 let reparsed: FlakeRef = parsed.to_string().parse().unwrap();
2026 assert_eq!(parsed, reparsed);
2027 assert_eq!(reparsed.to_string(), parsed.to_string());
2028 }
2029}
2030
2031#[cfg(test)]
2032mod resource_prefix_strip {
2033 use cool_asserts::assert_matches;
2038
2039 use super::*;
2040 use crate::{ResourceType, TransportLayer, flakeref::resource_url::ResourceUrl};
2041
2042 #[test]
2043 fn tarball_explicit_prefix_strips_on_display() {
2044 let parsed: FlakeRef = "tarball+https://example.com/foo.tar.gz".parse().unwrap();
2045 assert_eq!(parsed.to_string(), "https://example.com/foo.tar.gz");
2046 }
2047
2048 #[test]
2049 fn tarball_bare_https_round_trips() {
2050 let input = "https://example.com/foo.tar.gz";
2051 let parsed: FlakeRef = input.parse().unwrap();
2052 assert_eq!(parsed.to_string(), input);
2053 }
2054
2055 #[test]
2056 fn file_explicit_prefix_strips_on_display() {
2057 let parsed: FlakeRef = "file+https://example.com/data.bin".parse().unwrap();
2058 assert_eq!(parsed.to_string(), "https://example.com/data.bin");
2059 }
2060
2061 #[test]
2062 fn file_bare_https_round_trips() {
2063 let input = "https://example.com/data.bin";
2064 let parsed: FlakeRef = input.parse().unwrap();
2065 assert_eq!(parsed.to_string(), input);
2066 }
2067
2068 #[test]
2069 fn bare_file_with_tarball_extension_parses_as_tarball() {
2070 let parsed: FlakeRef = "file:///tmp/foo.tar.gz".parse().unwrap();
2076 assert_matches!(
2077 *parsed.kind(),
2078 FlakeRefType::Resource(ResourceUrl {
2079 res_type: ResourceType::Tarball,
2080 transport_type: Some(TransportLayer::File),
2081 ..
2082 })
2083 );
2084 }
2085
2086 #[test]
2087 fn bare_file_no_extension_parses_as_file() {
2088 let parsed: FlakeRef = "file:///tmp/data.bin".parse().unwrap();
2093 assert_matches!(
2094 *parsed.kind(),
2095 FlakeRefType::Resource(ResourceUrl {
2096 res_type: ResourceType::File,
2097 transport_type: Some(TransportLayer::File),
2098 ..
2099 })
2100 );
2101 }
2102
2103 #[test]
2104 fn tarball_plus_file_round_trips() {
2105 let input = "tarball+file:///x/y.tar.gz";
2111 let parsed: FlakeRef = input.parse().unwrap();
2112 let displayed = parsed.to_string();
2113 assert_eq!(displayed, "file:///x/y.tar.gz");
2114 let reparsed: FlakeRef = displayed.parse().unwrap();
2115 assert_eq!(parsed, reparsed);
2116 assert_eq!(reparsed.to_string(), displayed);
2117 }
2118}
2119
2120#[cfg(test)]
2121mod accessors {
2122 use super::*;
2126 use rstest::rstest;
2127
2128 #[test]
2129 fn forge_identity_for_github() {
2130 let parsed: FlakeRef = "github:nixos/nixpkgs".parse().unwrap();
2131 let id = parsed.forge_identity().unwrap();
2132 assert_eq!(id.platform, GitForgePlatform::GitHub);
2133 assert_eq!(id.owner, "nixos");
2134 assert_eq!(id.repo, "nixpkgs");
2135 assert_eq!(id.domain, "github.com");
2136 }
2137
2138 #[test]
2139 fn forge_identity_for_sourcehut() {
2140 let parsed: FlakeRef = "sourcehut:~owner/repo".parse().unwrap();
2144 let id = parsed.forge_identity().unwrap();
2145 assert_eq!(id.platform, GitForgePlatform::SourceHut);
2146 assert_eq!(id.owner, "~owner");
2147 assert_eq!(id.repo, "repo");
2148 assert_eq!(id.domain, "git.sr.ht");
2149 }
2150
2151 #[test]
2152 fn sourcehut_round_trips() {
2153 let uri = "sourcehut:nix-community/foo";
2154 let parsed: FlakeRef = uri.parse().unwrap();
2155 assert_eq!(parsed.to_string(), uri, "round-trip mismatch");
2156 assert_eq!(parsed.domain(), Some("git.sr.ht"));
2157 }
2158
2159 #[test]
2160 fn gitlab_with_host_override_returns_overridden_domain() {
2161 let parsed: FlakeRef = "gitlab:openldap/openldap?host=git.openldap.org"
2166 .parse()
2167 .unwrap();
2168 let id = parsed.forge_identity().unwrap();
2169 assert_eq!(id.domain, "git.openldap.org");
2170 assert_eq!(parsed.domain(), Some("git.openldap.org"));
2171 }
2172
2173 #[test]
2174 fn github_without_host_returns_canonical_domain() {
2175 let parsed: FlakeRef = "github:o/r".parse().unwrap();
2176 let id = parsed.forge_identity().unwrap();
2177 assert_eq!(id.domain, "github.com");
2178 assert_eq!(parsed.domain(), Some("github.com"));
2179 }
2180
2181 #[test]
2182 fn sourcehut_without_host_returns_git_sr_ht() {
2183 let parsed: FlakeRef = "sourcehut:~user/repo".parse().unwrap();
2186 let id = parsed.forge_identity().unwrap();
2187 assert_eq!(id.domain, "git.sr.ht");
2188 assert_eq!(parsed.domain(), Some("git.sr.ht"));
2189 }
2190
2191 #[test]
2192 fn forge_identity_none_for_path_indirect_resource() {
2193 for uri in [
2197 "path:./foo",
2198 "flake:nixpkgs",
2199 "git+https://example.com/owner/repo",
2200 ] {
2201 let parsed: FlakeRef = uri.parse().unwrap();
2202 assert!(parsed.forge_identity().is_none(), "expected None for {uri}",);
2203 }
2204 }
2205
2206 #[rstest]
2207 #[case(
2208 "github:nixos/nixpkgs",
2209 Some("nixos"),
2210 Some("nixpkgs"),
2211 Some("github.com")
2212 )]
2213 #[case("gitlab:owner/repo", Some("owner"), Some("repo"), Some("gitlab.com"))]
2214 #[case(
2215 "sourcehut:user/project",
2216 Some("user"),
2217 Some("project"),
2218 Some("git.sr.ht")
2219 )]
2220 #[case(
2221 "git+https://example.com/a/b",
2222 Some("a"),
2223 Some("b"),
2224 Some("example.com")
2225 )]
2226 #[case("path:./foo", None, None, None)]
2227 #[case("flake:nixpkgs", None, None, None)]
2228 fn identity_accessors(
2229 #[case] uri: &str,
2230 #[case] owner: Option<&str>,
2231 #[case] repo: Option<&str>,
2232 #[case] domain: Option<&str>,
2233 ) {
2234 let parsed: FlakeRef = uri.parse().unwrap();
2235 assert_eq!(parsed.owner(), owner, "owner mismatch for {uri}");
2236 assert_eq!(parsed.repo(), repo, "repo mismatch for {uri}");
2237 assert_eq!(parsed.domain(), domain, "domain mismatch for {uri}");
2238 }
2239
2240 #[rstest]
2241 #[case("github:nixos/nixpkgs", RefKind::None, false)]
2242 #[case("github:nixos/nixpkgs/release-23.05", RefKind::Ref, false)]
2243 #[case(
2244 "github:nixos/nixpkgs/abc1234567890123456789012345678901234567",
2245 RefKind::Rev,
2246 true
2247 )]
2248 #[case(
2249 "flake:nixpkgs/release-23.05/abc1234567890123456789012345678901234567",
2250 RefKind::Both,
2251 true
2252 )]
2253 #[case(
2254 "github:nixos/nixpkgs?rev=abc1234567890123456789012345678901234567",
2255 RefKind::Rev,
2256 true
2257 )]
2258 fn ref_kind_and_pinning(
2259 #[case] uri: &str,
2260 #[case] expected_kind: RefKind,
2261 #[case] pinned: bool,
2262 ) {
2263 let parsed: FlakeRef = uri.parse().unwrap();
2264 assert_eq!(
2265 parsed.ref_kind(),
2266 expected_kind,
2267 "ref_kind mismatch for {uri}"
2268 );
2269 assert_eq!(
2270 parsed.is_pinned_to_rev(),
2271 pinned,
2272 "is_pinned_to_rev mismatch for {uri}",
2273 );
2274 }
2275
2276 #[test]
2277 fn ref_or_rev_prefers_rev_when_pinned() {
2278 let parsed: FlakeRef =
2279 "flake:nixpkgs/release-23.05/abc1234567890123456789012345678901234567"
2280 .parse()
2281 .unwrap();
2282 assert_eq!(
2285 parsed.ref_or_rev(),
2286 Some("abc1234567890123456789012345678901234567"),
2287 );
2288 assert_eq!(parsed.ref_(), Some("release-23.05"));
2289 assert_eq!(
2290 parsed.rev(),
2291 Some("abc1234567890123456789012345678901234567")
2292 );
2293 }
2294}
2295
2296#[cfg(test)]
2297mod builders {
2298 use super::*;
2301
2302 #[test]
2303 fn with_ref_round_trip_path_component() {
2304 let updated = "github:nixos/nixpkgs"
2305 .parse::<FlakeRef>()
2306 .unwrap()
2307 .with_ref(Some("release-23.05".into()))
2308 .into_uri();
2309 assert_eq!(updated, "github:nixos/nixpkgs/release-23.05");
2310 }
2311
2312 #[test]
2313 fn with_rev_promotes_path_component_to_three_segment_for_indirect() {
2314 let updated = "flake:nixpkgs/release-23.05"
2317 .parse::<FlakeRef>()
2318 .unwrap()
2319 .with_rev(Some("abc1234567890123456789012345678901234567".into()))
2320 .into_uri();
2321 assert_eq!(
2322 updated,
2323 "flake:nixpkgs/release-23.05/abc1234567890123456789012345678901234567",
2324 );
2325 }
2326
2327 #[test]
2328 fn without_pin_clears_rev_keeps_ref() {
2329 let updated = "flake:nixpkgs/release-23.05/abc1234567890123456789012345678901234567"
2330 .parse::<FlakeRef>()
2331 .unwrap()
2332 .without_pin()
2333 .into_uri();
2334 assert_eq!(updated, "flake:nixpkgs/release-23.05");
2335 }
2336
2337 #[test]
2338 fn with_rev_on_resource_flips_to_query_parameter() {
2339 let updated = "git+https://github.com/owner/repo"
2340 .parse::<FlakeRef>()
2341 .unwrap()
2342 .with_rev(Some("abc1234567890123456789012345678901234567".into()))
2343 .into_uri();
2344 assert_eq!(
2345 updated,
2346 "git+https://github.com/owner/repo?rev=abc1234567890123456789012345678901234567",
2347 );
2348 }
2349
2350 #[test]
2351 fn with_fragment_round_trip() {
2352 let updated = "github:nixos/nixpkgs"
2353 .parse::<FlakeRef>()
2354 .unwrap()
2355 .with_fragment(Some("hello".into()))
2356 .into_uri();
2357 assert_eq!(updated, "github:nixos/nixpkgs#hello");
2358 }
2359
2360 #[test]
2361 fn with_ref_then_with_rev_chains_on_gitforge() {
2362 let updated = "github:nixos/nixpkgs"
2367 .parse::<FlakeRef>()
2368 .unwrap()
2369 .with_ref(Some("release-23.05".into()))
2370 .with_rev(Some("b2df4e4e80e04cbb33a350f87717f4bd6140d298".into()));
2371
2372 assert_eq!(updated.ref_(), Some("release-23.05"));
2373 assert_eq!(
2374 updated.rev(),
2375 Some("b2df4e4e80e04cbb33a350f87717f4bd6140d298")
2376 );
2377 }
2378
2379 #[test]
2380 fn with_ref_then_with_rev_chains_on_indirect() {
2381 let updated = "flake:nixpkgs"
2384 .parse::<FlakeRef>()
2385 .unwrap()
2386 .with_ref(Some("release-23.05".into()))
2387 .with_rev(Some("b2df4e4e80e04cbb33a350f87717f4bd6140d298".into()))
2388 .into_uri();
2389
2390 assert_eq!(
2391 updated,
2392 "flake:nixpkgs/release-23.05/b2df4e4e80e04cbb33a350f87717f4bd6140d298",
2393 );
2394 }
2395}
2396
2397#[cfg(test)]
2398mod https_github_classification {
2399 use super::*;
2412
2413 #[test]
2414 fn https_github_with_pull_path_does_not_reclassify_to_github_forge() {
2415 let url = "https://github.com/NixOS/nixpkgs/pull/483360.diff";
2416 let parsed: FlakeRef = url.parse().unwrap();
2417 assert!(
2418 !matches!(parsed.kind(), FlakeRefType::GitForge(_)),
2419 "expected non-GitForge classification for {url}, got {:?}",
2420 *parsed.kind(),
2421 );
2422 assert_eq!(parsed.to_string(), url);
2423 }
2424
2425 #[test]
2426 fn https_github_owner_repo_is_resource_not_gitforge() {
2427 let url = "https://github.com/nixos/nixpkgs";
2428 let parsed: FlakeRef = url.parse().unwrap();
2429 assert!(
2430 !matches!(parsed.kind(), FlakeRefType::GitForge(_)),
2431 "bare https://github.com/<o>/<r> must not auto-promote to GitForge, got {:?}",
2432 *parsed.kind(),
2433 );
2434 assert_eq!(parsed.to_string(), url);
2435 }
2436
2437 #[test]
2438 fn https_github_archive_tarball_remains_resource() {
2439 let url = "https://github.com/user/repo/archive/main.tar.gz";
2440 let parsed: FlakeRef = url.parse().unwrap();
2441 assert!(matches!(parsed.kind(), FlakeRefType::Resource(_)));
2442 assert_eq!(parsed.to_string(), url);
2443 }
2444}
2445
2446#[cfg(test)]
2447mod ref_rev_validation {
2448 use super::*;
2462 use crate::error::NixUriError;
2463 use cool_asserts::assert_matches;
2464 use rstest::rstest;
2465
2466 const HEX40: &str = "b2df4e4e80e04cbb33a350f87717f4bd6140d298";
2467
2468 #[rstest]
2469 #[case::github_both_in_query(
2470 "github:owner/repo?ref=main&rev=b2df4e4e80e04cbb33a350f87717f4bd6140d298"
2471 )]
2472 #[case::github_ref_path_rev_query(
2473 "github:owner/repo/main?rev=b2df4e4e80e04cbb33a350f87717f4bd6140d298"
2474 )]
2475 #[case::github_rev_path_ref_query(
2476 "github:owner/repo/b2df4e4e80e04cbb33a350f87717f4bd6140d298?ref=main"
2477 )]
2478 #[case::gitlab_both_in_query(
2479 "gitlab:owner/repo?ref=main&rev=b2df4e4e80e04cbb33a350f87717f4bd6140d298"
2480 )]
2481 #[case::gitlab_ref_path_rev_query(
2482 "gitlab:owner/repo/main?rev=b2df4e4e80e04cbb33a350f87717f4bd6140d298"
2483 )]
2484 #[case::gitlab_rev_path_ref_query(
2485 "gitlab:owner/repo/b2df4e4e80e04cbb33a350f87717f4bd6140d298?ref=main"
2486 )]
2487 #[case::sourcehut_both_in_query(
2488 "sourcehut:~owner/repo?ref=main&rev=b2df4e4e80e04cbb33a350f87717f4bd6140d298"
2489 )]
2490 #[case::sourcehut_ref_path_rev_query(
2491 "sourcehut:~owner/repo/main?rev=b2df4e4e80e04cbb33a350f87717f4bd6140d298"
2492 )]
2493 #[case::sourcehut_rev_path_ref_query(
2494 "sourcehut:~owner/repo/b2df4e4e80e04cbb33a350f87717f4bd6140d298?ref=main"
2495 )]
2496 fn gitforge_rejects_ref_and_rev_together(#[case] uri: &str) {
2497 assert_matches!(
2502 uri.parse::<FlakeRef>(),
2503 Err(NixUriError::FieldConflict {
2504 left: "ref",
2505 right: "rev",
2506 }),
2507 "expected mutual-exclusion rejection for {uri}",
2508 );
2509 }
2510
2511 #[rstest]
2512 #[case::github("github:owner/repo?rev=not-a-hash")]
2513 #[case::git_https("git+https://example.com/owner/repo?rev=not-a-hash")]
2514 #[case::hg_https("hg+https://example.com/repo?rev=zzzz")]
2515 #[case::indirect("flake:nixpkgs?rev=main")]
2516 #[case::gitlab("gitlab:owner/repo?rev=not-a-hash")]
2517 #[case::sourcehut("sourcehut:~owner/repo?rev=not-a-hash")]
2518 #[case::short_hex("github:owner/repo?rev=abc123")]
2519 #[case::between_40_and_64_hex(
2520 "github:owner/repo?rev=b2df4e4e80e04cbb33a350f87717f4bd6140d29800000"
2521 )]
2522 #[case::sixty_five_hex(
2523 "github:owner/repo?rev=00000000000000000000000000000000000000000000000000000000000000000"
2524 )]
2525 #[case::sixty_three_hex(
2526 "github:owner/repo?rev=000000000000000000000000000000000000000000000000000000000000000"
2527 )]
2528 fn query_rev_must_be_40_or_64_hex(#[case] uri: &str) {
2529 assert_matches!(
2533 uri.parse::<FlakeRef>(),
2534 Err(NixUriError::InvalidValue {
2535 field: "rev",
2536 reason,
2537 }) if reason == "expected 40-hex (SHA-1) or 64-hex (SHA-256) commit",
2538 "expected hex-validation rejection for {uri}",
2539 );
2540 }
2541
2542 #[rstest]
2549 #[case::gitlab_query_ref("gitlab:owner/repo?ref=main")]
2550 #[case::sourcehut_query_ref("sourcehut:~owner/repo?ref=main")]
2551 fn ref_only_query_still_parses_for_each_forge(#[case] uri: &str) {
2552 let parsed = uri
2553 .parse::<FlakeRef>()
2554 .expect("input must continue to parse cleanly");
2555 assert_eq!(parsed.to_string(), uri, "round-trip mismatch for {uri}");
2556 }
2557
2558 #[test]
2559 fn indirect_path_component_three_segment_still_parses() {
2560 let uri = format!("flake:nixpkgs/release-23.05/{HEX40}");
2564 let parsed: FlakeRef = uri.parse().unwrap();
2565 assert_eq!(parsed.ref_(), Some("release-23.05"));
2566 assert_eq!(parsed.rev(), Some(HEX40));
2567 }
2568
2569 #[test]
2570 fn resource_git_with_ref_and_rev_still_parses() {
2571 let uri = "git+https://git.somehost.tld/user/path?ref=branch&rev=fdc8ef970de2b4634e1b3dca296e1ed918459a9e";
2575 let parsed: FlakeRef = uri.parse().unwrap();
2576 assert_eq!(parsed.to_string(), uri);
2577 }
2578}
2579
2580#[cfg(test)]
2581mod path_authority_and_rev {
2582 use super::*;
2591 use crate::error::{NixUriError, UnsupportedReason};
2592 use cool_asserts::assert_matches;
2593 use rstest::rstest;
2594
2595 const HEX40: &str = "b2df4e4e80e04cbb33a350f87717f4bd6140d298";
2596
2597 #[rstest]
2598 #[case::host("path://somehost/abs/path")]
2599 #[case::host_no_path("path://x")]
2600 fn path_authority_rejected(#[case] uri: &str) {
2601 assert_matches!(
2602 uri.parse::<FlakeRef>(),
2603 Err(NixUriError::Unsupported(UnsupportedReason::Authority {
2604 scheme: "path",
2605 })),
2606 "expected authority rejection for {uri}",
2607 );
2608 }
2609
2610 #[rstest]
2615 #[case::triple_slash("path:///abs/path")]
2616 #[case::quad_slash("path:////a")]
2617 fn path_triple_slash_parses_as_absolute_path(#[case] uri: &str) {
2618 let parsed: FlakeRef = uri
2619 .parse()
2620 .unwrap_or_else(|e| panic!("empty-authority path must parse: {uri} -> {e}"));
2621 assert_matches!(parsed.kind(), FlakeRefType::Path { rev: None, .. });
2622 assert_eq!(parsed.to_string(), uri, "round-trip mismatch for {uri}");
2623 let reparsed: FlakeRef = parsed.to_string().parse().unwrap();
2624 assert_eq!(parsed, reparsed, "parse-Display-parse not stable for {uri}");
2625 }
2626
2627 #[test]
2628 fn path_with_authority_host_rejects() {
2629 assert_matches!(
2630 "path://host/abs".parse::<FlakeRef>(),
2631 Err(NixUriError::Unsupported(UnsupportedReason::Authority {
2632 scheme: "path",
2633 })),
2634 );
2635 }
2636
2637 #[rstest]
2638 #[case::abs("path:/foo/bar")]
2639 #[case::abs_trailing("path:/home/kenji/.config/dotfiles/")]
2640 #[case::cwd("path:./relative")]
2641 #[case::parent("path:..")]
2642 #[case::single_dot("path:.")]
2643 fn path_non_authority_still_parses(#[case] uri: &str) {
2644 let parsed: FlakeRef = uri
2645 .parse()
2646 .unwrap_or_else(|e| panic!("path body must continue to parse: {uri} -> {e}"));
2647 assert_eq!(parsed.to_string(), uri, "round-trip mismatch for {uri}");
2648 }
2649
2650 #[rstest]
2651 #[case::store_path(&format!("path:/nix/store/abc?rev={HEX40}"))]
2652 #[case::abs(&format!("path:/var/cache?rev={HEX40}"))]
2653 #[case::with_trailing_slash(&format!("path:/home/kenji/?rev={HEX40}"))]
2654 fn path_rev_query_round_trips(#[case] uri: &str) {
2655 let parsed: FlakeRef = uri.parse().expect("path with ?rev= must parse");
2656 assert_eq!(parsed.rev(), Some(HEX40), "rev not stored for {uri}");
2657 assert_eq!(parsed.to_string(), uri, "round-trip mismatch for {uri}");
2658 let reparsed: FlakeRef = parsed.to_string().parse().unwrap();
2659 assert_eq!(parsed, reparsed, "parse-Display-parse not stable for {uri}");
2660 }
2661
2662 #[test]
2663 fn path_rev_with_dir_keeps_alphabetical_query() {
2664 let uri = format!("path:/abs/path?dir=sub&rev={HEX40}");
2667 let parsed: FlakeRef = uri.parse().expect("must parse");
2668 assert_eq!(parsed.to_string(), uri);
2669 }
2670}
2671
2672#[cfg(test)]
2673mod percent_encoding_round_trip {
2674 use crate::{FlakeRef, NixUriError};
2680 use rstest::rstest;
2681
2682 #[rstest]
2683 #[case::space("github:o/r?dir=foo%20bar", "foo bar")]
2684 #[case::percent("github:o/r?dir=foo%25bar", "foo%bar")]
2685 #[case::semicolon("github:o/r?dir=foo%3Bbar", "foo;bar")]
2686 #[case::plus("github:o/r?dir=foo%2Bbar", "foo+bar")]
2687 #[case::ampersand("github:o/r?dir=foo%26bar", "foo&bar")]
2688 #[case::equals("github:o/r?dir=foo%3Dbar", "foo=bar")]
2689 #[case::hash("github:o/r?dir=foo%23bar", "foo#bar")]
2690 #[case::non_ascii("github:o/r?dir=f%C3%96%C3%B6", "fÖö")]
2691 fn query_value_round_trips_for_encoded_byte(#[case] input: &str, #[case] decoded: &str) {
2692 let parsed: FlakeRef = input.parse().expect("input must parse");
2693 let dir_value = parsed
2694 .params()
2695 .entries()
2696 .into_iter()
2697 .find(|(k, _)| *k == "dir")
2698 .map(|(_, v)| v.to_string());
2699 assert_eq!(dir_value, Some(decoded.to_string()));
2700 assert_eq!(parsed.to_string(), input);
2701 }
2702
2703 #[rstest]
2704 #[case::colon_in_value("github:o/r?dir=foo:bar")]
2705 #[case::at_in_value("github:o/r?dir=foo@bar")]
2706 #[case::slash_in_value("github:o/r?dir=foo/bar")]
2707 fn allowed_query_chars_remain_unencoded(#[case] input: &str) {
2708 let parsed: FlakeRef = input.parse().expect("input must parse");
2709 assert_eq!(parsed.to_string(), input);
2710 }
2711
2712 #[rstest]
2713 #[case::space("github:o/r#default%20package", "default package")]
2714 #[case::percent("github:o/r#a%25b", "a%b")]
2715 #[case::non_ascii("github:o/r#f%C3%96%C3%B6", "fÖö")]
2716 #[case::question_mark_in_fragment("github:o/r#a%3Fb", "a?b")]
2717 #[case::slash_in_fragment("github:o/r#a%2Fb", "a/b")]
2718 fn fragment_round_trips_for_encoded_byte(#[case] input: &str, #[case] decoded: &str) {
2719 let parsed: FlakeRef = input.parse().expect("input must parse");
2720 assert_eq!(parsed.fragment(), Some(decoded));
2721 assert_eq!(parsed.to_string(), input);
2722 }
2723
2724 #[rstest]
2725 #[case::truncated_one_hex("github:o/r?dir=%2")]
2726 #[case::truncated_no_hex("github:o/r?dir=%")]
2727 #[case::non_hex("github:o/r?dir=%XY")]
2728 #[case::non_hex_partial("github:o/r?dir=%2Z")]
2729 fn malformed_query_value_percent_encoding_rejected(#[case] input: &str) {
2730 match input.parse::<FlakeRef>() {
2731 Err(NixUriError::InvalidUrl(_)) => {}
2732 other => panic!("expected InvalidUrl for {input}, got {other:?}"),
2733 }
2734 }
2735
2736 #[rstest]
2737 #[case::truncated("github:o/r#a%2")]
2738 #[case::non_hex("github:o/r#a%XY")]
2739 fn malformed_fragment_percent_encoding_rejected(#[case] input: &str) {
2740 match input.parse::<FlakeRef>() {
2741 Err(NixUriError::InvalidUrl(_)) => {}
2742 other => panic!("expected InvalidUrl for {input}, got {other:?}"),
2743 }
2744 }
2745
2746 #[test]
2747 fn arbitrary_param_value_round_trips_with_space() {
2748 let input = "git+https://example.com/repo?name=hello%20world";
2752 let parsed: FlakeRef = input.parse().unwrap();
2753 assert_eq!(parsed.to_string(), input);
2754 }
2755}
2756
2757#[cfg(test)]
2758mod ref_rev_builders {
2759 use super::*;
2768 use crate::{NixUriError, UnsupportedReason};
2769
2770 const HEX40: &str = "b2df4e4e80e04cbb33a350f87717f4bd6140d298";
2771
2772 #[test]
2773 fn pin_to_rev_clears_path_component_ref() {
2774 let parsed: FlakeRef = "github:foo/bar/main".parse().unwrap();
2781 let pinned = parsed.pin_to_rev(HEX40.to_string());
2782 assert_eq!(pinned.ref_(), None);
2783 assert_eq!(pinned.rev(), Some(HEX40));
2784 let rendered = pinned.to_string();
2785 assert!(rendered.contains(HEX40), "expected rev in {rendered}");
2786 assert!(!rendered.contains("main"), "ref leaked into {rendered}");
2787 }
2788
2789 #[test]
2790 fn pin_to_rev_replaces_query_param_ref() {
2791 let parsed: FlakeRef = "github:foo/bar?ref=main".parse().unwrap();
2792 let pinned = parsed.pin_to_rev(HEX40.to_string());
2793 assert_eq!(pinned.ref_(), None);
2794 assert_eq!(pinned.rev(), Some(HEX40));
2795 let rendered = pinned.to_string();
2796 assert!(rendered.contains(HEX40), "expected rev in {rendered}");
2797 assert!(!rendered.contains("ref="), "ref= leaked into {rendered}");
2798 }
2799
2800 #[test]
2801 fn pin_to_rev_sets_rev_on_path() {
2802 let parsed: FlakeRef = "path:/x/y".parse().unwrap();
2805 let pinned = parsed.pin_to_rev(HEX40.to_string());
2806 assert_eq!(pinned.rev(), Some(HEX40));
2807 }
2808
2809 #[test]
2810 fn try_with_ref_path_returns_unsupported() {
2811 let parsed: FlakeRef = "path:/x/y".parse().unwrap();
2815 let result = parsed.try_with_ref(Some("main".into()));
2816 match result {
2817 Err(NixUriError::Unsupported(UnsupportedReason::Field { field, .. })) => {
2818 assert_eq!(field, "ref");
2819 }
2820 other => panic!("expected Unsupported(Field {{ field: \"ref\" }}), got {other:?}"),
2821 }
2822 }
2823
2824 #[test]
2825 fn try_with_ref_tarball_returns_unsupported() {
2826 let parsed: FlakeRef = "tarball+https://example.com/foo.tar.gz".parse().unwrap();
2828 let result = parsed.try_with_ref(Some("v1".into()));
2829 match result {
2830 Err(NixUriError::Unsupported(UnsupportedReason::Field { field, .. })) => {
2831 assert_eq!(field, "ref");
2832 }
2833 other => panic!("expected Unsupported(Field {{ field: \"ref\" }}), got {other:?}"),
2834 }
2835 }
2836
2837 #[test]
2838 fn try_with_ref_github_succeeds() {
2839 let parsed: FlakeRef = "github:foo/bar".parse().unwrap();
2840 let updated = parsed
2841 .try_with_ref(Some("main".into()))
2842 .expect("github accepts ref");
2843 assert_eq!(updated.ref_(), Some("main"));
2844 }
2845
2846 #[test]
2847 fn try_with_ref_clear_is_always_ok() {
2848 let parsed: FlakeRef = "path:/x/y".parse().unwrap();
2851 let cleared = parsed.try_with_ref(None).expect("clear is a no-op");
2852 assert_eq!(cleared.ref_(), None);
2853 }
2854
2855 #[test]
2856 fn try_with_rev_path_succeeds() {
2857 let parsed: FlakeRef = "path:/x/y".parse().unwrap();
2860 let pinned = parsed
2861 .try_with_rev(Some(HEX40.into()))
2862 .expect("path accepts rev");
2863 assert_eq!(pinned.rev(), Some(HEX40));
2864 }
2865
2866 #[test]
2867 fn with_ref_silent_noop_path() {
2868 let parsed: FlakeRef = "path:/x/y".parse().unwrap();
2872 let updated = parsed.with_ref(Some("main".into()));
2873 assert_eq!(updated.ref_(), None, "with_ref must remain a no-op on Path");
2874 }
2875}
2876
2877#[cfg(test)]
2878mod canonical_string {
2879 use super::*;
2885
2886 const HEX40: &str = "0000000000000000000000000000000000000000";
2887
2888 fn assert_canonical_reparses(input: &str) {
2893 let parsed: FlakeRef = input.parse().expect("input parses");
2894 let canonical = parsed.to_canonical_string();
2895 let _: FlakeRef = canonical
2896 .parse()
2897 .unwrap_or_else(|e| panic!("canonical {canonical:?} failed to re-parse: {e}"));
2898 }
2899
2900 #[test]
2903 fn github_ref_query_canonicalises_to_path_component() {
2904 let input = "github:nixos/nixpkgs?ref=nixos-23.11";
2905 let parsed: FlakeRef = input.parse().unwrap();
2906 assert_eq!(
2907 parsed.to_canonical_string(),
2908 "github:nixos/nixpkgs/nixos-23.11"
2909 );
2910 assert_eq!(parsed.to_string(), input);
2912 assert_canonical_reparses(input);
2913 }
2914
2915 #[test]
2916 fn github_rev_query_canonicalises_to_path_component() {
2917 let input = "github:nixos/nixpkgs?rev=0000000000000000000000000000000000000000";
2918 let parsed: FlakeRef = input.parse().unwrap();
2919 assert_eq!(
2920 parsed.to_canonical_string(),
2921 "github:nixos/nixpkgs/0000000000000000000000000000000000000000"
2922 );
2923 assert_eq!(parsed.to_string(), input);
2924 assert_canonical_reparses(input);
2925 }
2926
2927 #[test]
2928 fn github_path_component_ref_unchanged() {
2929 let input = "github:nixos/nixpkgs/main";
2931 let parsed: FlakeRef = input.parse().unwrap();
2932 assert_eq!(parsed.to_canonical_string(), input);
2933 assert_eq!(parsed.to_string(), input);
2934 }
2935
2936 #[test]
2937 fn gitlab_ref_query_canonicalises() {
2938 let input = "gitlab:foo/bar?ref=v1.0";
2939 let parsed: FlakeRef = input.parse().unwrap();
2940 assert_eq!(parsed.to_canonical_string(), "gitlab:foo/bar/v1.0");
2941 assert_eq!(parsed.to_string(), input);
2942 }
2943
2944 #[test]
2945 fn sourcehut_ref_query_canonicalises() {
2946 let input = "sourcehut:~user/repo?ref=branch";
2947 let parsed: FlakeRef = input.parse().unwrap();
2948 assert_eq!(parsed.to_canonical_string(), "sourcehut:~user/repo/branch");
2949 assert_eq!(parsed.to_string(), input);
2950 }
2951
2952 #[test]
2953 fn github_canonical_keeps_host_and_nar_hash() {
2954 let input = "github:nixos/nixpkgs/main?host=ghe.example.com&narHash=sha256-abc";
2958 let parsed: FlakeRef = input.parse().unwrap();
2959 assert_eq!(
2960 parsed.to_canonical_string(),
2961 "github:nixos/nixpkgs/main?host=ghe.example.com&narHash=sha256-abc"
2962 );
2963 }
2964
2965 #[test]
2966 fn github_canonical_picks_rev_over_ref() {
2967 let mut forge = GitForge {
2973 platform: GitForgePlatform::GitHub,
2974 owner: "nixos".into(),
2975 repo: "nixpkgs".into(),
2976 ref_: Some("main".into()),
2977 rev: Some(HEX40.into()),
2978 location: RefLocation::PathComponent,
2979 };
2980 forge.location = RefLocation::PathComponent;
2983 let f = FlakeRef::default().with_kind(FlakeRefType::GitForge(forge));
2984 assert_eq!(
2985 f.to_canonical_string(),
2986 format!("github:nixos/nixpkgs/{HEX40}")
2987 );
2988 }
2989
2990 #[test]
2994 fn git_all_refs_dropped_on_canonical() {
2995 let input = "git+https://github.com/nixos/nixpkgs?allRefs=1";
2996 let parsed: FlakeRef = input.parse().unwrap();
2997 assert_eq!(
3000 parsed.to_canonical_string(),
3001 "git+https://github.com/nixos/nixpkgs"
3002 );
3003 assert_eq!(parsed.to_string(), input);
3005 assert_canonical_reparses(input);
3006 }
3007
3008 #[test]
3009 fn git_lfs_truthy_kept() {
3010 let input = "git+https://example.com/repo?lfs=1";
3011 let parsed: FlakeRef = input.parse().unwrap();
3012 assert_eq!(parsed.to_canonical_string(), input);
3013 assert_eq!(parsed.to_string(), input);
3014 }
3015
3016 #[test]
3017 fn git_lfs_falsy_dropped() {
3018 let input = "git+https://example.com/repo?lfs=0";
3019 let parsed: FlakeRef = input.parse().unwrap();
3020 assert_eq!(parsed.to_canonical_string(), "git+https://example.com/repo");
3021 assert_eq!(parsed.to_string(), input);
3022 }
3023
3024 #[test]
3025 fn git_submodules_truthy_kept() {
3026 let input = "git+https://example.com/repo?submodules=1";
3027 let parsed: FlakeRef = input.parse().unwrap();
3028 assert_eq!(parsed.to_canonical_string(), input);
3029 assert_eq!(parsed.to_string(), input);
3030 }
3031
3032 #[test]
3033 fn git_shallow_truthy_kept() {
3034 let input = "git+https://example.com/repo?shallow=1";
3035 let parsed: FlakeRef = input.parse().unwrap();
3036 assert_eq!(parsed.to_canonical_string(), input);
3037 assert_eq!(parsed.to_string(), input);
3038 }
3039
3040 #[test]
3041 fn git_export_ignore_truthy_kept() {
3042 let input = "git+https://example.com/repo?exportIgnore=1";
3043 let parsed: FlakeRef = input.parse().unwrap();
3044 assert_eq!(parsed.to_canonical_string(), input);
3045 }
3046
3047 #[test]
3048 fn git_verify_commit_truthy_kept() {
3049 let input = "git+https://example.com/repo?verifyCommit=1";
3050 let parsed: FlakeRef = input.parse().unwrap();
3051 assert_eq!(parsed.to_canonical_string(), input);
3052 }
3053
3054 #[test]
3055 fn git_locked_attrs_dropped() {
3056 let input = "git+https://example.com/repo?lastModified=42&narHash=sha256-x&revCount=7";
3059 let parsed: FlakeRef = input.parse().unwrap();
3060 assert_eq!(parsed.to_canonical_string(), "git+https://example.com/repo");
3061 }
3062
3063 #[test]
3064 fn git_ref_and_rev_canonical_alphabetised() {
3065 let input = format!("git+https://example.com/repo?ref=main&rev={HEX40}");
3068 let parsed: FlakeRef = input.parse().unwrap();
3069 assert_eq!(parsed.to_canonical_string(), input);
3070 }
3071
3072 #[test]
3075 fn hg_canonical_keeps_only_ref_and_rev() {
3076 let input = "hg+https://example.com/repo?ref=main";
3077 let parsed: FlakeRef = input.parse().unwrap();
3078 assert_eq!(parsed.to_canonical_string(), input);
3079 }
3080
3081 #[test]
3084 fn indirect_canonical_matches_display() {
3085 for input in ["flake:nixpkgs", "flake:nixpkgs/main", "flake:nixos/nixpkgs"] {
3086 let parsed: FlakeRef = input.parse().unwrap();
3087 assert_eq!(parsed.to_canonical_string(), parsed.to_string(), "{input}");
3088 assert_eq!(parsed.to_canonical_string(), input);
3089 }
3090 }
3091
3092 #[test]
3093 fn path_canonical_matches_display() {
3094 for input in ["path:./foo", "path:/abs/path"] {
3095 let parsed: FlakeRef = input.parse().unwrap();
3096 assert_eq!(parsed.to_canonical_string(), parsed.to_string(), "{input}");
3097 assert_eq!(parsed.to_canonical_string(), input);
3098 }
3099 }
3100
3101 #[test]
3102 fn fragment_survives_canonicalisation() {
3103 let input = "github:nixos/nixpkgs/main#hello";
3104 let parsed: FlakeRef = input.parse().unwrap();
3105 assert_eq!(
3106 parsed.to_canonical_string(),
3107 "github:nixos/nixpkgs/main#hello"
3108 );
3109 assert_eq!(parsed.to_string(), input);
3110 }
3111}
3112
3113#[cfg(test)]
3114mod historical_seed_round_trip {
3115 use super::*;
3120
3121 fn assert_round_trip(value: &FlakeRef) {
3122 let displayed = value.to_string();
3123 let parsed: FlakeRef = displayed
3124 .parse()
3125 .unwrap_or_else(|e| panic!("Display output {displayed:?} failed to parse: {e}"));
3126 assert_eq!(*value, parsed, "round-trip mismatch for {displayed:?}");
3127 }
3128
3129 #[test]
3135 fn path_double_slash_round_trips() {
3136 let value = FlakeRef::new(FlakeRefType::Path {
3137 path: "//".to_string(),
3138 rev: None,
3139 });
3140 assert_eq!(value.to_string(), "path://");
3141 assert_round_trip(&value);
3142 }
3143}