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
30pub 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 pub fn list(&mut self) -> &InputMap {
79 self.walker.inputs.clear();
80 assert!(self.walker.walk(&Change::None).ok().flatten().is_none());
82 &self.walker.inputs
83 }
84 pub fn apply_change(&mut self, change: Change) -> Result<Option<String>, FlakeEditError> {
87 match change {
88 Change::None => Ok(None),
89 Change::Add { .. } => {
90 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 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 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 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 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 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 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 match result {
259 Some(text) => assert_eq!(text, original, "text should be unchanged"),
260 None => {} }
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}