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 curr_list(&self) -> &InputMap {
69        &self.walker.inputs
70    }
71
72    /// Will walk and then list the inputs, for listing the current inputs,
73    /// use `curr_list()`.
74    pub fn list(&mut self) -> &InputMap {
75        self.walker.inputs.clear();
76        // Walk returns Ok(None) when no changes are made (expected for listing)
77        assert!(self.walker.walk(&Change::None).ok().flatten().is_none());
78        &self.walker.inputs
79    }
80    /// Apply a specific change to a walker, on some inputs it will need to walk
81    /// multiple times, will error, if the edit could not be applied successfully.
82    pub fn apply_change(&mut self, change: Change) -> Result<Option<String>, FlakeEditError> {
83        match change {
84            Change::None => Ok(None),
85            Change::Add { .. } => {
86                // Check for duplicate input before adding
87                if let Some(input_id) = change.id() {
88                    self.ensure_inputs_populated()?;
89
90                    let input_id_string = input_id.to_string();
91                    if self.walker.inputs.contains_key(&input_id_string) {
92                        return Err(FlakeEditError::DuplicateInput(input_id_string));
93                    }
94                }
95
96                if let Some(maybe_changed_node) = self.walker.walk(&change.clone())? {
97                    let outputs = self.walker.list_outputs()?;
98                    match outputs {
99                        Outputs::Multiple(out) => {
100                            let id = change.id().unwrap().to_string();
101                            if !out.contains(&id) {
102                                self.walker.root = maybe_changed_node.clone();
103                                if let Some(maybe_changed_node) =
104                                    self.walker.change_outputs(OutputChange::Add(id))?
105                                {
106                                    return Ok(Some(maybe_changed_node.to_string()));
107                                }
108                            }
109                        }
110                        Outputs::None | Outputs::Any(_) => {}
111                    }
112                    Ok(Some(maybe_changed_node.to_string()))
113                } else {
114                    self.walker.add_toplevel = true;
115                    let maybe_changed_node = self.walker.walk(&change)?;
116                    Ok(maybe_changed_node.map(|n| n.to_string()))
117                }
118            }
119            Change::Remove { .. } => {
120                self.ensure_inputs_populated()?;
121
122                let removed_id = change.id().unwrap().to_string();
123
124                // If we remove a node, it could be a flat structure,
125                // we want to remove all of the references to its toplevel.
126                let mut res = None;
127                while let Some(changed_node) = self.walker.walk(&change)? {
128                    if res == Some(changed_node.clone()) {
129                        break;
130                    }
131                    res = Some(changed_node.clone());
132                    self.walker.root = changed_node.clone();
133                }
134                // Removed nodes should be removed from the outputs
135                let outputs = self.walker.list_outputs()?;
136                match outputs {
137                    Outputs::Multiple(out) | Outputs::Any(out) => {
138                        if out.contains(&removed_id)
139                            && let Some(changed_node) = self
140                                .walker
141                                .change_outputs(OutputChange::Remove(removed_id.clone()))?
142                        {
143                            res = Some(changed_node.clone());
144                            self.walker.root = changed_node.clone();
145                        }
146                    }
147                    Outputs::None => {}
148                }
149
150                // Remove orphaned follows references that point to the removed input
151                let orphaned_follows = self.collect_orphaned_follows(&removed_id);
152                for orphan_change in orphaned_follows {
153                    while let Some(changed_node) = self.walker.walk(&orphan_change)? {
154                        if res == Some(changed_node.clone()) {
155                            break;
156                        }
157                        res = Some(changed_node.clone());
158                        self.walker.root = changed_node.clone();
159                    }
160                }
161
162                Ok(res.map(|n| n.to_string()))
163            }
164            Change::Follows { .. } => {
165                self.ensure_inputs_populated()?;
166
167                // Validate that the parent input exists
168                if let Some(input_id) = change.id() {
169                    let parent_input = input_id.input();
170                    if !self.walker.inputs.contains_key(parent_input) {
171                        return Err(FlakeEditError::InputNotFound(parent_input.to_string()));
172                    }
173                }
174
175                if let Some(maybe_changed_node) = self.walker.walk(&change)? {
176                    Ok(Some(maybe_changed_node.to_string()))
177                } else {
178                    Ok(None)
179                }
180            }
181            Change::Change { .. } => {
182                if let Some(input_id) = change.id() {
183                    self.ensure_inputs_populated()?;
184
185                    let input_id_string = input_id.to_string();
186                    if !self.walker.inputs.contains_key(&input_id_string) {
187                        return Err(FlakeEditError::InputNotFound(input_id_string));
188                    }
189                }
190
191                if let Some(maybe_changed_node) = self.walker.walk(&change)? {
192                    Ok(Some(maybe_changed_node.to_string()))
193                } else {
194                    Ok(None)
195                }
196            }
197        }
198    }
199
200    pub fn walker(&self) -> &Walker {
201        &self.walker
202    }
203
204    /// Ensure the inputs map is populated by walking if empty.
205    fn ensure_inputs_populated(&mut self) -> Result<(), FlakeEditError> {
206        if self.walker.inputs.is_empty() {
207            let _ = self.walker.walk(&Change::None)?;
208        }
209        Ok(())
210    }
211
212    /// Collect follows references that point to a removed input.
213    /// Returns a list of Change::Remove for orphaned follows.
214    fn collect_orphaned_follows(&self, removed_id: &str) -> Vec<Change> {
215        let mut orphaned = Vec::new();
216        for (input_id, input) in &self.walker.inputs {
217            for follows in input.follows() {
218                if let Follows::Indirect(follows_name, target) = follows {
219                    // target is the RHS of `follows = "target"`
220                    if target.trim_matches('"') == removed_id {
221                        let nested_id = format!("{}.{}", input_id, follows_name);
222                        orphaned.push(Change::Remove {
223                            ids: vec![nested_id.into()],
224                        });
225                    }
226                }
227            }
228        }
229        orphaned
230    }
231}