flake_edit/
lock.rs

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, Serialize, Deserialize)]
9pub struct FlakeLock {
10    nodes: HashMap<String, Node>,
11    root: String,
12    version: u8,
13}
14
15#[derive(Debug, Serialize, Deserialize)]
16pub struct Node {
17    inputs: Option<HashMap<String, Input>>,
18    locked: Option<Locked>,
19    original: Option<Original>,
20}
21
22impl Node {
23    fn get_rev(&self) -> String {
24        self.locked.clone().unwrap().get_rev().to_string()
25    }
26}
27
28#[derive(Debug, Serialize, Deserialize, Clone)]
29#[serde(untagged)]
30pub enum Input {
31    Direct(String),
32    Indirect(Vec<String>),
33}
34
35impl Input {
36    fn get_id(&self) -> String {
37        match self {
38            Input::Direct(id) => id.to_string(),
39            Input::Indirect(_) => todo!(),
40        }
41    }
42}
43
44#[derive(Debug, Serialize, Deserialize, Clone)]
45pub struct Locked {
46    owner: Option<String>,
47    repo: Option<String>,
48    rev: Option<String>,
49    #[serde(rename = "type")]
50    node_type: String,
51    #[serde(rename = "ref")]
52    ref_field: Option<String>,
53}
54
55impl Locked {
56    fn get_rev(&self) -> String {
57        self.rev.clone().unwrap()
58    }
59}
60
61#[derive(Debug, Serialize, Deserialize)]
62pub struct Original {
63    owner: Option<String>,
64    repo: Option<String>,
65    #[serde(rename = "type")]
66    node_type: String,
67    #[serde(rename = "ref")]
68    ref_field: Option<String>,
69    url: Option<String>,
70}
71
72impl FlakeLock {
73    const LOCK: &'static str = "flake.lock";
74
75    // TODO: implement root path traversal
76    pub fn from_default_path() -> Result<Self, FlakeEditError> {
77        let path = PathBuf::from(Self::LOCK);
78        Self::from_file(path)
79    }
80    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, FlakeEditError> {
81        let mut file = File::open(path)?;
82        let mut contents = String::new();
83        file.read_to_string(&mut contents)?;
84        Self::read_from_str(&contents)
85    }
86    pub fn read_from_str(str: &str) -> Result<Self, FlakeEditError> {
87        Ok(serde_json::from_str(str)?)
88    }
89    pub fn root(&self) -> &str {
90        &self.root
91    }
92    /// Query the lock file for a specific rev.
93    /// TODO: implement proper root resolving
94    pub fn get_rev_by_id(&self, id: &str) -> Result<String, FlakeEditError> {
95        let root = self.root();
96        let resolved_root = self.nodes.get(root).ok_or(FlakeEditError::NotARoot)?;
97        let binding = resolved_root
98            .inputs
99            .clone()
100            .ok_or_else(|| FlakeEditError::LockError("Could not resolve root.".into()))?;
101        let resolved_id = binding
102            .get(id)
103            .ok_or_else(|| FlakeEditError::LockError("Could not resolve id.".into()))?;
104        let id = resolved_id.get_id();
105        let node = self
106            .nodes
107            .get(&id)
108            .ok_or_else(|| FlakeEditError::LockError("Could not find node with id.".into()))?;
109        Ok(node.get_rev())
110    }
111}
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    fn minimal_lock() -> &'static str {
117        r#"
118    {
119  "nodes": {
120    "nixpkgs": {
121      "locked": {
122        "lastModified": 1718714799,
123        "narHash": "sha256-FUZpz9rg3gL8NVPKbqU8ei1VkPLsTIfAJ2fdAf5qjak=",
124        "owner": "nixos",
125        "repo": "nixpkgs",
126        "rev": "c00d587b1a1afbf200b1d8f0b0e4ba9deb1c7f0e",
127        "type": "github"
128      },
129      "original": {
130        "owner": "nixos",
131        "ref": "nixos-unstable",
132        "repo": "nixpkgs",
133        "type": "github"
134      }
135    },
136    "root": {
137      "inputs": {
138        "nixpkgs": "nixpkgs"
139      }
140    }
141  },
142  "root": "root",
143  "version": 7
144}
145    "#
146    }
147    fn minimal_independent_lock_no_overrides() -> &'static str {
148        r#"
149    {
150  "nodes": {
151    "nixpkgs": {
152      "locked": {
153        "lastModified": 1721138476,
154        "narHash": "sha256-+W5eZOhhemLQxelojLxETfbFbc19NWawsXBlapYpqIA=",
155        "owner": "nixos",
156        "repo": "nixpkgs",
157        "rev": "ad0b5eed1b6031efaed382844806550c3dcb4206",
158        "type": "github"
159      },
160      "original": {
161        "owner": "nixos",
162        "ref": "nixos-unstable",
163        "repo": "nixpkgs",
164        "type": "github"
165      }
166    },
167    "nixpkgs_2": {
168      "locked": {
169        "lastModified": 1719690277,
170        "narHash": "sha256-0xSej1g7eP2kaUF+JQp8jdyNmpmCJKRpO12mKl/36Kc=",
171        "owner": "nixos",
172        "repo": "nixpkgs",
173        "rev": "2741b4b489b55df32afac57bc4bfd220e8bf617e",
174        "type": "github"
175      },
176      "original": {
177        "owner": "nixos",
178        "ref": "nixos-unstable",
179        "repo": "nixpkgs",
180        "type": "github"
181      }
182    },
183    "root": {
184      "inputs": {
185        "nixpkgs": "nixpkgs",
186        "treefmt-nix": "treefmt-nix"
187      }
188    },
189    "treefmt-nix": {
190      "inputs": {
191        "nixpkgs": "nixpkgs_2"
192      },
193      "locked": {
194        "lastModified": 1721382922,
195        "narHash": "sha256-GYpibTC0YYKRpFR9aftym9jjRdUk67ejw1IWiaQkaiU=",
196        "owner": "numtide",
197        "repo": "treefmt-nix",
198        "rev": "50104496fb55c9140501ea80d183f3223d13ff65",
199        "type": "github"
200      },
201      "original": {
202        "owner": "numtide",
203        "repo": "treefmt-nix",
204        "type": "github"
205      }
206    }
207  },
208  "root": "root",
209  "version": 7
210}
211    "#
212    }
213
214    fn minimal_independent_lock_nixpkgs_overridden() -> &'static str {
215        r#"
216    {
217  "nodes": {
218    "nixpkgs": {
219      "locked": {
220        "lastModified": 1721138476,
221        "narHash": "sha256-+W5eZOhhemLQxelojLxETfbFbc19NWawsXBlapYpqIA=",
222        "owner": "nixos",
223        "repo": "nixpkgs",
224        "rev": "ad0b5eed1b6031efaed382844806550c3dcb4206",
225        "type": "github"
226      },
227      "original": {
228        "owner": "nixos",
229        "ref": "nixos-unstable",
230        "repo": "nixpkgs",
231        "type": "github"
232      }
233    },
234    "root": {
235      "inputs": {
236        "nixpkgs": "nixpkgs",
237        "treefmt-nix": "treefmt-nix"
238      }
239    },
240    "treefmt-nix": {
241      "inputs": {
242        "nixpkgs": [
243          "nixpkgs"
244        ]
245      },
246      "locked": {
247        "lastModified": 1721382922,
248        "narHash": "sha256-GYpibTC0YYKRpFR9aftym9jjRdUk67ejw1IWiaQkaiU=",
249        "owner": "numtide",
250        "repo": "treefmt-nix",
251        "rev": "50104496fb55c9140501ea80d183f3223d13ff65",
252        "type": "github"
253      },
254      "original": {
255        "owner": "numtide",
256        "repo": "treefmt-nix",
257        "type": "github"
258      }
259    }
260  },
261  "root": "root",
262  "version": 7
263}
264    "#
265    }
266
267    #[test]
268    fn parse_minimal() {
269        let minimal_lock = minimal_lock();
270        FlakeLock::read_from_str(minimal_lock).expect("Should be parsed correctly.");
271    }
272    #[test]
273    fn parse_minimal_version() {
274        let minimal_lock = minimal_lock();
275        let parsed_lock =
276            FlakeLock::read_from_str(minimal_lock).expect("Should be parsed correctly.");
277        assert_eq!(7, parsed_lock.version);
278    }
279    #[test]
280    fn parse_minimal_root() {
281        let minimal_lock = minimal_lock();
282        let parsed_lock =
283            FlakeLock::read_from_str(minimal_lock).expect("Should be parsed correctly.");
284        assert_eq!("root", parsed_lock.root);
285    }
286    #[test]
287    fn minimal_ref() {
288        let minimal_lock = minimal_lock();
289        let parsed_lock =
290            FlakeLock::read_from_str(minimal_lock).expect("Should be parsed correctly.");
291        assert_eq!(
292            "c00d587b1a1afbf200b1d8f0b0e4ba9deb1c7f0e",
293            parsed_lock
294                .get_rev_by_id("nixpkgs")
295                .expect("Id: nixpkgs is in the lockfile.")
296        );
297    }
298    #[test]
299    fn parse_minimal_independent_lock_no_overrides() {
300        let minimal_lock = minimal_independent_lock_no_overrides();
301        FlakeLock::read_from_str(minimal_lock).expect("Should be parsed correctly.");
302    }
303    #[test]
304    fn minimal_independent_lock_no_overrides_ref() {
305        let minimal_lock = minimal_independent_lock_no_overrides();
306        let parsed_lock =
307            FlakeLock::read_from_str(minimal_lock).expect("Should be parsed correctly.");
308        assert_eq!(
309            "ad0b5eed1b6031efaed382844806550c3dcb4206",
310            parsed_lock
311                .get_rev_by_id("nixpkgs")
312                .expect("Id: nixpkgs is in the lockfile.")
313        );
314    }
315    #[test]
316    fn parse_minimal_independent_lock_nixpkgs_overridden() {
317        let minimal_lock = minimal_independent_lock_nixpkgs_overridden();
318        FlakeLock::read_from_str(minimal_lock).expect("Should be parsed correctly.");
319    }
320}