1use crate::error::FlakeEditError;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fs::File;
5use std::io::Read;
6use std::path::{Path, PathBuf};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct NestedInput {
11 pub path: String,
13 pub follows: Option<String>,
15}
16
17impl NestedInput {
18 pub fn to_display_string(&self) -> String {
21 match &self.follows {
22 Some(target) => format!("{}\t{}", self.path, target),
23 None => self.path.clone(),
24 }
25 }
26}
27
28#[derive(Debug, Serialize, Deserialize)]
29pub struct FlakeLock {
30 nodes: HashMap<String, Node>,
31 root: String,
32 version: u8,
33}
34
35#[derive(Debug, Serialize, Deserialize)]
36pub struct Node {
37 inputs: Option<HashMap<String, Input>>,
38 locked: Option<Locked>,
39 original: Option<Original>,
40}
41
42impl Node {
43 fn rev(&self) -> String {
44 self.locked.clone().unwrap().rev().to_string()
45 }
46}
47
48#[derive(Debug, Serialize, Deserialize, Clone)]
49#[serde(untagged)]
50pub enum Input {
51 Direct(String),
52 Indirect(Vec<String>),
53}
54
55impl Input {
56 fn id(&self) -> String {
60 match self {
61 Input::Direct(id) => id.to_string(),
62 Input::Indirect(path) => path.last().cloned().unwrap_or_default(),
63 }
64 }
65}
66
67#[derive(Debug, Serialize, Deserialize, Clone)]
68pub struct Locked {
69 owner: Option<String>,
70 repo: Option<String>,
71 rev: Option<String>,
72 #[serde(rename = "type")]
73 node_type: String,
74 #[serde(rename = "ref")]
75 ref_field: Option<String>,
76}
77
78impl Locked {
79 fn rev(&self) -> String {
80 self.rev.clone().unwrap()
81 }
82}
83
84#[derive(Debug, Serialize, Deserialize)]
85pub struct Original {
86 owner: Option<String>,
87 repo: Option<String>,
88 #[serde(rename = "type")]
89 node_type: String,
90 #[serde(rename = "ref")]
91 ref_field: Option<String>,
92 url: Option<String>,
93}
94
95impl FlakeLock {
96 const LOCK: &'static str = "flake.lock";
97
98 pub fn from_default_path() -> Result<Self, FlakeEditError> {
99 let path = PathBuf::from(Self::LOCK);
100 Self::from_file(path)
101 }
102
103 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, FlakeEditError> {
104 let mut file = File::open(path)?;
105 let mut contents = String::new();
106 file.read_to_string(&mut contents)?;
107 Self::read_from_str(&contents)
108 }
109 pub fn read_from_str(str: &str) -> Result<Self, FlakeEditError> {
110 Ok(serde_json::from_str(str)?)
111 }
112 pub fn root(&self) -> &str {
113 &self.root
114 }
115 pub fn rev_for(&self, id: &str) -> Result<String, FlakeEditError> {
117 let root = self.root();
118 let resolved_root = self
119 .nodes
120 .get(root)
121 .ok_or(FlakeEditError::LockMissingRoot)?;
122 let binding = resolved_root
123 .inputs
124 .clone()
125 .ok_or_else(|| FlakeEditError::LockError("Could not resolve root.".into()))?;
126 let resolved_id = binding
127 .get(id)
128 .ok_or_else(|| FlakeEditError::LockError("Could not resolve id.".into()))?;
129 let id = resolved_id.id();
130 let node = self
131 .nodes
132 .get(&id)
133 .ok_or_else(|| FlakeEditError::LockError("Could not find node with id.".into()))?;
134 Ok(node.rev())
135 }
136
137 pub fn nested_input_paths(&self) -> Vec<String> {
140 self.nested_inputs()
141 .into_iter()
142 .map(|input| input.path)
143 .collect()
144 }
145
146 pub fn nested_inputs(&self) -> Vec<NestedInput> {
148 let mut inputs = Vec::new();
149
150 let Some(root_node) = self.nodes.get(&self.root) else {
152 return inputs;
153 };
154
155 let Some(root_inputs) = &root_node.inputs else {
157 return inputs;
158 };
159
160 for (top_level_name, top_level_ref) in root_inputs {
162 let node_name = match top_level_ref {
164 Input::Direct(name) => name.clone(),
165 Input::Indirect(_) => {
166 continue;
168 }
169 };
170
171 if let Some(node) = self.nodes.get(&node_name) {
173 if let Some(nested_inputs) = &node.inputs {
175 for (nested_name, nested_ref) in nested_inputs {
176 let path = format!("{}.{}", top_level_name, nested_name);
177 let follows = match nested_ref {
178 Input::Indirect(targets) => Some(targets.join(".")),
179 Input::Direct(_) => None,
180 };
181 inputs.push(NestedInput { path, follows });
182 }
183 }
184 }
185 }
186
187 inputs.sort_by(|a, b| a.path.cmp(&b.path));
188 inputs
189 }
190}
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 fn minimal_lock() -> &'static str {
196 r#"
197 {
198 "nodes": {
199 "nixpkgs": {
200 "locked": {
201 "lastModified": 1718714799,
202 "narHash": "sha256-FUZpz9rg3gL8NVPKbqU8ei1VkPLsTIfAJ2fdAf5qjak=",
203 "owner": "nixos",
204 "repo": "nixpkgs",
205 "rev": "c00d587b1a1afbf200b1d8f0b0e4ba9deb1c7f0e",
206 "type": "github"
207 },
208 "original": {
209 "owner": "nixos",
210 "ref": "nixos-unstable",
211 "repo": "nixpkgs",
212 "type": "github"
213 }
214 },
215 "root": {
216 "inputs": {
217 "nixpkgs": "nixpkgs"
218 }
219 }
220 },
221 "root": "root",
222 "version": 7
223}
224 "#
225 }
226 fn minimal_independent_lock_no_overrides() -> &'static str {
227 r#"
228 {
229 "nodes": {
230 "nixpkgs": {
231 "locked": {
232 "lastModified": 1721138476,
233 "narHash": "sha256-+W5eZOhhemLQxelojLxETfbFbc19NWawsXBlapYpqIA=",
234 "owner": "nixos",
235 "repo": "nixpkgs",
236 "rev": "ad0b5eed1b6031efaed382844806550c3dcb4206",
237 "type": "github"
238 },
239 "original": {
240 "owner": "nixos",
241 "ref": "nixos-unstable",
242 "repo": "nixpkgs",
243 "type": "github"
244 }
245 },
246 "nixpkgs_2": {
247 "locked": {
248 "lastModified": 1719690277,
249 "narHash": "sha256-0xSej1g7eP2kaUF+JQp8jdyNmpmCJKRpO12mKl/36Kc=",
250 "owner": "nixos",
251 "repo": "nixpkgs",
252 "rev": "2741b4b489b55df32afac57bc4bfd220e8bf617e",
253 "type": "github"
254 },
255 "original": {
256 "owner": "nixos",
257 "ref": "nixos-unstable",
258 "repo": "nixpkgs",
259 "type": "github"
260 }
261 },
262 "root": {
263 "inputs": {
264 "nixpkgs": "nixpkgs",
265 "treefmt-nix": "treefmt-nix"
266 }
267 },
268 "treefmt-nix": {
269 "inputs": {
270 "nixpkgs": "nixpkgs_2"
271 },
272 "locked": {
273 "lastModified": 1721382922,
274 "narHash": "sha256-GYpibTC0YYKRpFR9aftym9jjRdUk67ejw1IWiaQkaiU=",
275 "owner": "numtide",
276 "repo": "treefmt-nix",
277 "rev": "50104496fb55c9140501ea80d183f3223d13ff65",
278 "type": "github"
279 },
280 "original": {
281 "owner": "numtide",
282 "repo": "treefmt-nix",
283 "type": "github"
284 }
285 }
286 },
287 "root": "root",
288 "version": 7
289}
290 "#
291 }
292
293 fn minimal_independent_lock_nixpkgs_overridden() -> &'static str {
294 r#"
295 {
296 "nodes": {
297 "nixpkgs": {
298 "locked": {
299 "lastModified": 1721138476,
300 "narHash": "sha256-+W5eZOhhemLQxelojLxETfbFbc19NWawsXBlapYpqIA=",
301 "owner": "nixos",
302 "repo": "nixpkgs",
303 "rev": "ad0b5eed1b6031efaed382844806550c3dcb4206",
304 "type": "github"
305 },
306 "original": {
307 "owner": "nixos",
308 "ref": "nixos-unstable",
309 "repo": "nixpkgs",
310 "type": "github"
311 }
312 },
313 "root": {
314 "inputs": {
315 "nixpkgs": "nixpkgs",
316 "treefmt-nix": "treefmt-nix"
317 }
318 },
319 "treefmt-nix": {
320 "inputs": {
321 "nixpkgs": [
322 "nixpkgs"
323 ]
324 },
325 "locked": {
326 "lastModified": 1721382922,
327 "narHash": "sha256-GYpibTC0YYKRpFR9aftym9jjRdUk67ejw1IWiaQkaiU=",
328 "owner": "numtide",
329 "repo": "treefmt-nix",
330 "rev": "50104496fb55c9140501ea80d183f3223d13ff65",
331 "type": "github"
332 },
333 "original": {
334 "owner": "numtide",
335 "repo": "treefmt-nix",
336 "type": "github"
337 }
338 }
339 },
340 "root": "root",
341 "version": 7
342}
343 "#
344 }
345
346 #[test]
347 fn parse_minimal() {
348 let minimal_lock = minimal_lock();
349 FlakeLock::read_from_str(minimal_lock).expect("Should be parsed correctly.");
350 }
351 #[test]
352 fn parse_minimal_version() {
353 let minimal_lock = minimal_lock();
354 let parsed_lock =
355 FlakeLock::read_from_str(minimal_lock).expect("Should be parsed correctly.");
356 assert_eq!(7, parsed_lock.version);
357 }
358 #[test]
359 fn parse_minimal_root() {
360 let minimal_lock = minimal_lock();
361 let parsed_lock =
362 FlakeLock::read_from_str(minimal_lock).expect("Should be parsed correctly.");
363 assert_eq!("root", parsed_lock.root);
364 }
365 #[test]
366 fn minimal_ref() {
367 let minimal_lock = minimal_lock();
368 let parsed_lock =
369 FlakeLock::read_from_str(minimal_lock).expect("Should be parsed correctly.");
370 assert_eq!(
371 "c00d587b1a1afbf200b1d8f0b0e4ba9deb1c7f0e",
372 parsed_lock
373 .rev_for("nixpkgs")
374 .expect("Id: nixpkgs is in the lockfile.")
375 );
376 }
377 #[test]
378 fn parse_minimal_independent_lock_no_overrides() {
379 let minimal_lock = minimal_independent_lock_no_overrides();
380 FlakeLock::read_from_str(minimal_lock).expect("Should be parsed correctly.");
381 }
382 #[test]
383 fn minimal_independent_lock_no_overrides_ref() {
384 let minimal_lock = minimal_independent_lock_no_overrides();
385 let parsed_lock =
386 FlakeLock::read_from_str(minimal_lock).expect("Should be parsed correctly.");
387 assert_eq!(
388 "ad0b5eed1b6031efaed382844806550c3dcb4206",
389 parsed_lock
390 .rev_for("nixpkgs")
391 .expect("Id: nixpkgs is in the lockfile.")
392 );
393 }
394 #[test]
395 fn parse_minimal_independent_lock_nixpkgs_overridden() {
396 let minimal_lock = minimal_independent_lock_nixpkgs_overridden();
397 FlakeLock::read_from_str(minimal_lock).expect("Should be parsed correctly.");
398 }
399
400 #[test]
401 fn input_indirect_id() {
402 let input = Input::Indirect(vec!["nixpkgs".to_string()]);
404 assert_eq!("nixpkgs", input.id());
405 }
406}