1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
14pub struct FlakeLock {
15 pub version: u8,
17
18 pub root: String,
20
21 pub nodes: HashMap<String, FlakeNode>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33pub struct FlakeNode {
34 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub flake: Option<bool>,
38
39 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub locked: Option<LockedInfo>,
43
44 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub original: Option<OriginalInfo>,
48
49 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
51 pub inputs: HashMap<String, InputRef>,
52}
53
54impl FlakeNode {
55 #[must_use]
57 pub const fn is_root(&self) -> bool {
58 self.locked.is_none() && self.original.is_none() && self.flake.is_none()
59 }
60
61 #[must_use]
63 pub const fn is_input(&self) -> bool {
64 self.locked.is_some() || self.original.is_some() || self.flake.is_some()
65 }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
73#[serde(untagged)]
74pub enum InputRef {
75 Direct(String),
77 Follows(Vec<String>),
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
88pub struct LockedInfo {
89 #[serde(rename = "type")]
91 pub locked_type: String,
92
93 #[serde(rename = "lastModified")]
95 pub last_modified: Option<i64>,
96
97 #[serde(rename = "narHash")]
100 pub nar_hash: Option<String>,
101
102 pub rev: Option<String>,
104
105 pub owner: Option<String>,
107
108 pub repo: Option<String>,
110
111 pub url: Option<String>,
113
114 #[serde(rename = "revCount")]
116 pub rev_count: Option<i64>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
124pub struct OriginalInfo {
125 #[serde(rename = "type")]
127 pub original_type: String,
128
129 #[serde(rename = "ref")]
132 pub reference: Option<String>,
133
134 pub owner: Option<String>,
136
137 pub repo: Option<String>,
139
140 pub url: Option<String>,
142}
143
144impl FlakeLock {
145 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
150 serde_json::from_str(json)
151 }
152
153 #[must_use]
155 pub fn root_node(&self) -> Option<&FlakeNode> {
156 self.nodes.get(&self.root).filter(|node| node.is_root())
157 }
158
159 #[must_use]
161 pub fn get_input(&self, name: &str) -> Option<&FlakeNode> {
162 self.nodes.get(name).filter(|node| node.is_input())
163 }
164
165 #[must_use]
167 pub fn get_node(&self, name: &str) -> Option<&FlakeNode> {
168 self.nodes.get(name)
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn test_parse_minimal_flake_lock() {
178 let json = r#"{
179 "nodes": {
180 "root": { "inputs": {} }
181 },
182 "root": "root",
183 "version": 7
184 }"#;
185
186 let lock = FlakeLock::from_json(json).unwrap();
187 assert_eq!(lock.version, 7);
188 assert_eq!(lock.root, "root");
189 assert!(lock.root_node().is_some());
190 }
191
192 #[test]
193 fn test_parse_with_locked_input() {
194 let json = r#"{
195 "nodes": {
196 "nixpkgs": {
197 "locked": {
198 "type": "github",
199 "owner": "NixOS",
200 "repo": "nixpkgs",
201 "rev": "abc123",
202 "narHash": "sha256-xxxxxxxxxxxxx"
203 },
204 "original": {
205 "type": "github",
206 "owner": "NixOS",
207 "repo": "nixpkgs"
208 }
209 },
210 "root": {
211 "inputs": { "nixpkgs": "nixpkgs" }
212 }
213 },
214 "root": "root",
215 "version": 7
216 }"#;
217
218 let lock = FlakeLock::from_json(json).unwrap();
219 let input = lock.get_input("nixpkgs").unwrap();
220 assert!(input.locked.is_some());
221 let locked = input.locked.as_ref().unwrap();
222 assert_eq!(locked.locked_type, "github");
223 assert_eq!(locked.nar_hash.as_deref(), Some("sha256-xxxxxxxxxxxxx"));
224 }
225
226 #[test]
227 fn test_parse_follows_reference() {
228 let json = r#"{
229 "nodes": {
230 "nixpkgs": {
231 "locked": {
232 "type": "github",
233 "narHash": "sha256-abc"
234 }
235 },
236 "rust-overlay": {
237 "inputs": {
238 "nixpkgs": ["nixpkgs"]
239 },
240 "locked": {
241 "type": "github",
242 "narHash": "sha256-def"
243 }
244 },
245 "root": {
246 "inputs": {
247 "nixpkgs": "nixpkgs",
248 "rust-overlay": "rust-overlay"
249 }
250 }
251 },
252 "root": "root",
253 "version": 7
254 }"#;
255
256 let lock = FlakeLock::from_json(json).unwrap();
257 let rust_overlay = lock.get_input("rust-overlay").unwrap();
258
259 let nixpkgs_ref = rust_overlay.inputs.get("nixpkgs").unwrap();
261 assert!(matches!(nixpkgs_ref, InputRef::Follows(path) if path == &["nixpkgs"]));
262 }
263
264 #[test]
265 fn test_parse_non_flake_input() {
266 let json = r#"{
267 "nodes": {
268 "advisory-db": {
269 "flake": false,
270 "locked": {
271 "type": "github",
272 "narHash": "sha256-xyz"
273 }
274 },
275 "root": {
276 "inputs": { "advisory-db": "advisory-db" }
277 }
278 },
279 "root": "root",
280 "version": 7
281 }"#;
282
283 let lock = FlakeLock::from_json(json).unwrap();
284 let input = lock.get_input("advisory-db").unwrap();
285 assert_eq!(input.flake, Some(false));
286 }
287}