Skip to main content

flake_edit/
edit.rs

1use std::collections::HashMap;
2
3use crate::change::Change;
4use crate::error::FlakeEditError;
5use crate::input::{Follows, Input};
6use crate::validate;
7use crate::walk::Walker;
8
9pub struct FlakeEdit {
10    changes: Vec<Change>,
11    walker: Walker,
12}
13
14#[derive(Default, Debug)]
15pub enum Outputs {
16    #[default]
17    None,
18    Multiple(Vec<String>),
19    Any(Vec<String>),
20}
21
22pub type InputMap = HashMap<String, Input>;
23
24pub fn sorted_input_ids(inputs: &InputMap) -> Vec<&String> {
25    let mut keys: Vec<_> = inputs.keys().collect();
26    keys.sort();
27    keys
28}
29
30/// Returns owned strings, for contexts where references won't work.
31pub fn sorted_input_ids_owned(inputs: &InputMap) -> Vec<String> {
32    let mut keys: Vec<String> = inputs.keys().cloned().collect();
33    keys.sort();
34    keys
35}
36
37#[derive(Default, Debug)]
38pub enum OutputChange {
39    #[default]
40    None,
41    Add(String),
42    Remove(String),
43}
44
45impl FlakeEdit {
46    pub fn new(changes: Vec<Change>, walker: Walker) -> Self {
47        Self { changes, walker }
48    }
49
50    pub fn from_text(stream: &str) -> Result<Self, FlakeEditError> {
51        let validation = validate::validate(stream);
52        if validation.has_errors() {
53            return Err(FlakeEditError::Validation(validation.errors));
54        }
55
56        let walker = Walker::new(stream);
57        Ok(Self::new(Vec::new(), walker))
58    }
59
60    pub fn changes(&self) -> &[Change] {
61        self.changes.as_ref()
62    }
63
64    pub fn add_change(&mut self, change: Change) {
65        self.changes.push(change);
66    }
67
68    pub fn source_text(&self) -> String {
69        self.walker.root.to_string()
70    }
71
72    pub fn curr_list(&self) -> &InputMap {
73        &self.walker.inputs
74    }
75
76    /// Will walk and then list the inputs, for listing the current inputs,
77    /// use `curr_list()`.
78    pub fn list(&mut self) -> &InputMap {
79        self.walker.inputs.clear();
80        // Walk returns Ok(None) when no changes are made (expected for listing)
81        assert!(self.walker.walk(&Change::None).ok().flatten().is_none());
82        &self.walker.inputs
83    }
84    /// Apply a specific change to a walker, on some inputs it will need to walk
85    /// multiple times, will error, if the edit could not be applied successfully.
86    pub fn apply_change(&mut self, change: Change) -> Result<Option<String>, FlakeEditError> {
87        match change {
88            Change::None => Ok(None),
89            Change::Add { .. } => {
90                // Check for duplicate input before adding
91                if let Some(input_id) = change.id() {
92                    self.ensure_inputs_populated()?;
93
94                    let input_id_string = input_id.to_string();
95                    if self.walker.inputs.contains_key(&input_id_string) {
96                        return Err(FlakeEditError::DuplicateInput(input_id_string));
97                    }
98                }
99
100                if let Some(maybe_changed_node) = self.walker.walk(&change.clone())? {
101                    let outputs = self.walker.list_outputs()?;
102                    match outputs {
103                        Outputs::Multiple(out) => {
104                            let id = change.id().unwrap().to_string();
105                            if !out.contains(&id) {
106                                self.walker.root = maybe_changed_node.clone();
107                                if let Some(maybe_changed_node) =
108                                    self.walker.change_outputs(OutputChange::Add(id))?
109                                {
110                                    return Ok(Some(maybe_changed_node.to_string()));
111                                }
112                            }
113                        }
114                        Outputs::None | Outputs::Any(_) => {}
115                    }
116                    Ok(Some(maybe_changed_node.to_string()))
117                } else {
118                    self.walker.add_toplevel = true;
119                    let maybe_changed_node = self.walker.walk(&change)?;
120                    Ok(maybe_changed_node.map(|n| n.to_string()))
121                }
122            }
123            Change::Remove { .. } => {
124                self.ensure_inputs_populated()?;
125
126                let removed_id = change.id().unwrap().to_string();
127
128                // If we remove a node, it could be a flat structure,
129                // we want to remove all of the references to its toplevel.
130                let mut res = None;
131                while let Some(changed_node) = self.walker.walk(&change)? {
132                    if res == Some(changed_node.clone()) {
133                        break;
134                    }
135                    res = Some(changed_node.clone());
136                    self.walker.root = changed_node.clone();
137                }
138                // Removed nodes should be removed from the outputs
139                let outputs = self.walker.list_outputs()?;
140                match outputs {
141                    Outputs::Multiple(out) | Outputs::Any(out) => {
142                        if out.contains(&removed_id)
143                            && let Some(changed_node) = self
144                                .walker
145                                .change_outputs(OutputChange::Remove(removed_id.clone()))?
146                        {
147                            res = Some(changed_node.clone());
148                            self.walker.root = changed_node.clone();
149                        }
150                    }
151                    Outputs::None => {}
152                }
153
154                // Remove orphaned follows references that point to the removed input
155                let orphaned_follows = self.collect_orphaned_follows(&removed_id);
156                for orphan_change in orphaned_follows {
157                    while let Some(changed_node) = self.walker.walk(&orphan_change)? {
158                        if res == Some(changed_node.clone()) {
159                            break;
160                        }
161                        res = Some(changed_node.clone());
162                        self.walker.root = changed_node.clone();
163                    }
164                }
165
166                Ok(res.map(|n| n.to_string()))
167            }
168            Change::Follows { ref input, .. } => {
169                self.ensure_inputs_populated()?;
170
171                let parent_id = input.input();
172                if !self.walker.inputs.contains_key(parent_id) {
173                    return Err(FlakeEditError::InputNotFound(parent_id.to_string()));
174                }
175
176                if let Some(maybe_changed_node) = self.walker.walk(&change)? {
177                    Ok(Some(maybe_changed_node.to_string()))
178                } else {
179                    Ok(None)
180                }
181            }
182            Change::Change { .. } => {
183                if let Some(input_id) = change.id() {
184                    self.ensure_inputs_populated()?;
185
186                    let input_id_string = input_id.to_string();
187                    if !self.walker.inputs.contains_key(&input_id_string) {
188                        return Err(FlakeEditError::InputNotFound(input_id_string));
189                    }
190                }
191
192                if let Some(maybe_changed_node) = self.walker.walk(&change)? {
193                    Ok(Some(maybe_changed_node.to_string()))
194                } else {
195                    Ok(None)
196                }
197            }
198        }
199    }
200
201    pub fn walker(&self) -> &Walker {
202        &self.walker
203    }
204
205    /// Ensure the inputs map is populated by walking if empty.
206    fn ensure_inputs_populated(&mut self) -> Result<(), FlakeEditError> {
207        if self.walker.inputs.is_empty() {
208            let _ = self.walker.walk(&Change::None)?;
209        }
210        Ok(())
211    }
212
213    /// Collect follows references that point to a removed input.
214    /// Returns a list of Change::Remove for orphaned follows.
215    fn collect_orphaned_follows(&self, removed_id: &str) -> Vec<Change> {
216        let mut orphaned = Vec::new();
217        for (input_id, input) in &self.walker.inputs {
218            for follows in input.follows() {
219                if let Follows::Indirect(follows_name, target) = follows {
220                    // target is the RHS of `follows = "target"`
221                    if target.trim_matches('"') == removed_id {
222                        let nested_id = format!("{}.{}", input_id, follows_name);
223                        orphaned.push(Change::Remove {
224                            ids: vec![nested_id.into()],
225                        });
226                    }
227                }
228            }
229        }
230        orphaned
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn already_follows_is_noop() {
240        let flake = r#"{
241  inputs = {
242    nixpkgs.url = "github:nixos/nixpkgs";
243    crane = {
244      url = "github:ipetkov/crane";
245      inputs.nixpkgs.follows = "nixpkgs";
246    };
247  };
248  outputs = { ... }: { };
249}"#;
250        let mut fe = FlakeEdit::from_text(flake).unwrap();
251        let original = fe.source_text();
252        let change = Change::Follows {
253            input: "crane.nixpkgs".to_string().into(),
254            target: "nixpkgs".to_string(),
255        };
256        let result = fe.apply_change(change).unwrap();
257        // Walker returns the unchanged text; caller detects the no-op
258        match result {
259            Some(text) => assert_eq!(text, original, "text should be unchanged"),
260            None => {} // also acceptable
261        }
262    }
263
264    #[test]
265    fn new_follows_succeeds() {
266        let flake = r#"{
267  inputs = {
268    nixpkgs.url = "github:nixos/nixpkgs";
269    crane = {
270      url = "github:ipetkov/crane";
271    };
272  };
273  outputs = { ... }: { };
274}"#;
275        let mut fe = FlakeEdit::from_text(flake).unwrap();
276        let change = Change::Follows {
277            input: "crane.nixpkgs".to_string().into(),
278            target: "nixpkgs".to_string(),
279        };
280        let result = fe.apply_change(change);
281        assert!(result.is_ok(), "expected Ok, got: {:?}", result);
282        let text = result.unwrap().unwrap();
283        assert!(text.contains("inputs.nixpkgs.follows = \"nixpkgs\""));
284    }
285}