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 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 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}