1use std::path::PathBuf;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct FlakeRef {
11 pub flake_dir: PathBuf,
13 pub attribute: String,
15}
16
17impl FlakeRef {
18 pub fn parse(input: &str) -> Result<Self, FlakeRefError> {
38 if let Some((path_part, attr)) = input.split_once('#') {
39 let dir = if path_part == "." || path_part.is_empty() {
40 std::env::current_dir()
41 .map_err(|e| FlakeRefError::InvalidPath(e.to_string()))?
42 } else {
43 let raw = path_part.strip_prefix("path:").unwrap_or(path_part);
49 PathBuf::from(raw)
50 };
51 Ok(Self {
52 flake_dir: dir,
53 attribute: attr.to_string(),
54 })
55 } else {
56 Err(FlakeRefError::MissingAttribute(input.to_string()))
57 }
58 }
59}
60
61#[derive(Debug, thiserror::Error)]
63pub enum FlakeRefError {
64 #[error("flake reference missing '#attribute': {0}")]
66 MissingAttribute(String),
67 #[error("invalid flake path: {0}")]
69 InvalidPath(String),
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 #[test]
77 fn parse_dot_hash_cid() {
78 let fr = FlakeRef::parse(".#cid").unwrap();
79 assert_eq!(fr.flake_dir, std::env::current_dir().unwrap());
81 assert_eq!(fr.attribute, "cid");
82 }
83
84 #[test]
85 fn parse_absolute_path() {
86 let fr = FlakeRef::parse("/absolute/path#attr").unwrap();
87 assert_eq!(fr.flake_dir, PathBuf::from("/absolute/path"));
88 assert_eq!(fr.attribute, "attr");
89 }
90
91 #[test]
92 fn parse_relative_path() {
93 let fr = FlakeRef::parse("relative/path#attr").unwrap();
94 assert_eq!(fr.flake_dir, PathBuf::from("relative/path"));
95 assert_eq!(fr.attribute, "attr");
96 }
97
98 #[test]
99 fn parse_missing_hash_returns_error() {
100 let err = FlakeRef::parse("no-hash-here").unwrap_err();
101 assert!(matches!(err, FlakeRefError::MissingAttribute(_)));
102 assert!(err.to_string().contains("no-hash-here"));
103 }
104
105 #[test]
106 fn parse_empty_attribute_allowed() {
107 let fr = FlakeRef::parse(".#").unwrap();
108 assert_eq!(fr.attribute, "");
109 }
110
111 #[test]
112 fn parse_empty_path_uses_cwd() {
113 let fr = FlakeRef::parse("#attr").unwrap();
114 assert_eq!(fr.flake_dir, std::env::current_dir().unwrap());
115 assert_eq!(fr.attribute, "attr");
116 }
117
118 #[test]
119 fn parse_dotted_attribute() {
120 let fr = FlakeRef::parse("/nix#darwinConfigurations.cid.system").unwrap();
121 assert_eq!(fr.flake_dir, PathBuf::from("/nix"));
122 assert_eq!(fr.attribute, "darwinConfigurations.cid.system");
123 }
124
125 #[test]
126 fn parse_strips_path_scheme() {
127 let fr = FlakeRef::parse("path:/etc/nixos#nixosConfigurations.rio").unwrap();
128 assert_eq!(fr.flake_dir, PathBuf::from("/etc/nixos"));
129 assert_eq!(fr.attribute, "nixosConfigurations.rio");
130 }
131
132 #[test]
133 fn parse_strips_path_scheme_relative() {
134 let fr = FlakeRef::parse("path:./config#attr").unwrap();
135 assert_eq!(fr.flake_dir, PathBuf::from("./config"));
136 assert_eq!(fr.attribute, "attr");
137 }
138
139 #[test]
140 fn parse_multiple_hashes_splits_on_first() {
141 let fr = FlakeRef::parse("/path#attr#extra").unwrap();
142 assert_eq!(fr.flake_dir, PathBuf::from("/path"));
143 assert_eq!(fr.attribute, "attr#extra");
144 }
145
146 #[test]
147 fn error_display_missing_attribute() {
148 let err = FlakeRefError::MissingAttribute("foo".into());
149 assert!(err.to_string().contains("missing '#attribute'"));
150 assert!(err.to_string().contains("foo"));
151 }
152
153 #[test]
154 fn error_display_invalid_path() {
155 let err = FlakeRefError::InvalidPath("bad path".into());
156 assert!(err.to_string().contains("invalid flake path"));
157 assert!(err.to_string().contains("bad path"));
158 }
159}