1use crate::follows::{AttrPath, AttrPathParseError, Segment};
2use crate::walk::Context;
3
4#[derive(Debug, Default, Clone, serde::Serialize)]
5pub enum Change {
6 #[default]
7 None,
8 Add {
9 id: Option<ChangeId>,
10 uri: Option<String>,
11 flake: bool,
13 },
14 Remove {
15 ids: Vec<ChangeId>,
16 },
17 Change {
18 id: Option<ChangeId>,
19 uri: Option<String>,
20 },
21 Follows {
26 input: ChangeId,
29 target: AttrPath,
31 },
32 Toggle {
39 id: ChangeId,
40 uri: String,
42 previous: String,
44 },
45 ToggleRemove {
52 id: ChangeId,
53 uri: String,
55 activate: Option<String>,
57 },
58}
59
60#[derive(Debug, Clone, PartialEq, serde::Serialize)]
65pub struct ChangeId(AttrPath);
66
67impl ChangeId {
68 pub fn new(path: AttrPath) -> Self {
69 ChangeId(path)
70 }
71
72 pub fn parse(s: &str) -> Result<Self, AttrPathParseError> {
75 Ok(ChangeId(AttrPath::parse(s)?))
76 }
77
78 pub fn path(&self) -> &AttrPath {
79 &self.0
80 }
81
82 pub fn input(&self) -> &Segment {
85 self.0.first()
86 }
87
88 pub fn follows(&self) -> Option<&Segment> {
90 self.0.child()
91 }
92
93 fn matches(&self, input: &Segment, follows: Option<&Segment>) -> bool {
94 if self.input() != input {
95 return false;
96 }
97 match (self.follows(), follows) {
98 (Some(self_follows), Some(f)) => self_follows == f,
99 (Some(_), None) => false,
100 (None, _) => true,
101 }
102 }
103
104 pub fn matches_with_follows(&self, input: &Segment, follows: Option<&Segment>) -> bool {
106 self.matches(input, follows)
107 }
108
109 pub fn matches_with_ctx(&self, follows: &Segment, ctx: Option<Context>) -> bool {
112 let ctx_input = ctx.and_then(|c| c.first().cloned());
113 match ctx_input {
114 Some(input) => self.matches(&input, Some(follows)),
115 None => self.input() == follows,
116 }
117 }
118}
119
120impl std::fmt::Display for ChangeId {
121 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 write!(f, "{}", self.0)
123 }
124}
125
126impl TryFrom<String> for ChangeId {
127 type Error = AttrPathParseError;
128
129 fn try_from(value: String) -> Result<Self, Self::Error> {
130 ChangeId::parse(&value)
131 }
132}
133
134impl TryFrom<&str> for ChangeId {
135 type Error = AttrPathParseError;
136
137 fn try_from(value: &str) -> Result<Self, Self::Error> {
138 ChangeId::parse(value)
139 }
140}
141
142impl From<AttrPath> for ChangeId {
143 fn from(value: AttrPath) -> Self {
144 ChangeId(value)
145 }
146}
147
148impl From<Segment> for ChangeId {
149 fn from(value: Segment) -> Self {
150 ChangeId(AttrPath::new(value))
151 }
152}
153
154impl Change {
155 pub fn id(&self) -> Option<ChangeId> {
156 match self {
157 Change::None => None,
158 Change::Add { id, .. } => id.clone(),
159 Change::Remove { ids } => ids.first().cloned(),
160 Change::Change { id, .. } => id.clone(),
161 Change::Follows { input, .. } => Some(input.clone()),
162 Change::Toggle { id, .. } | Change::ToggleRemove { id, .. } => Some(id.clone()),
163 }
164 }
165
166 pub fn ids(&self) -> Vec<ChangeId> {
167 match self {
168 Change::Remove { ids } => ids.clone(),
169 Change::Follows { input, .. } => vec![input.clone()],
170 _ => self.id().into_iter().collect(),
171 }
172 }
173 pub fn is_remove(&self) -> bool {
174 matches!(self, Change::Remove { .. })
175 }
176 pub fn is_follows(&self) -> bool {
177 matches!(self, Change::Follows { .. })
178 }
179 pub fn uri(&self) -> Option<&String> {
180 match self {
181 Change::Change { uri, .. } | Change::Add { uri, .. } => uri.as_ref(),
182 Change::Toggle { uri, .. } => Some(uri),
183 _ => None,
184 }
185 }
186 pub fn follows_target(&self) -> Option<&AttrPath> {
187 match self {
188 Change::Follows { target, .. } => Some(target),
189 _ => None,
190 }
191 }
192
193 pub fn success_messages(&self) -> Vec<String> {
194 match self {
195 Change::Add { id, uri, .. } => {
196 let id = id.as_ref().map(ChangeId::to_string);
197 vec![format!(
198 "Added input: {} = {}",
199 id.as_deref().unwrap_or("?"),
200 uri.as_deref().unwrap_or("?")
201 )]
202 }
203 Change::Remove { ids } => ids
204 .iter()
205 .map(|id| format!("Removed input: {}", id))
206 .collect(),
207 Change::Change { id, uri, .. } => {
208 let id = id.as_ref().map(ChangeId::to_string);
209 vec![format!(
210 "Changed input: {} -> {}",
211 id.as_deref().unwrap_or("?"),
212 uri.as_deref().unwrap_or("?")
213 )]
214 }
215 Change::Follows { input, target } => {
216 let segments = input.path().segments();
219 let path = if segments.len() == 1 {
220 format!("inputs.{}", segments[0].render())
221 } else {
222 let mut out = String::new();
223 for (i, seg) in segments.iter().enumerate() {
224 if i == 0 {
225 out.push_str(&seg.render());
226 } else {
227 out.push_str(".inputs.");
228 out.push_str(&seg.render());
229 }
230 }
231 out
232 };
233 vec![format!(
234 "Added follows: {}.follows = \"{}\"",
235 path,
236 target.to_flake_follows_string()
237 )]
238 }
239 Change::Toggle { id, uri, previous } => {
240 vec![format!("Toggled {}: {} -> {}", id, previous, uri)]
241 }
242 Change::ToggleRemove { id, uri, activate } => {
243 let removed = format!("Removed {} alternate: {}", id, uri);
244 match activate {
245 Some(activate) => {
246 vec![format!("Toggled {}: {} -> {}", id, uri, activate), removed]
247 }
248 None => vec![removed],
249 }
250 }
251 Change::None => vec![],
252 }
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn change_id_quoted_dot() {
262 let id = ChangeId::parse("\"hls-1.10\".nixpkgs").unwrap();
263 assert_eq!(id.input().as_str(), "hls-1.10");
264 assert_eq!(id.follows().unwrap().as_str(), "nixpkgs");
265 }
266
267 #[test]
268 fn change_id_single_segment_no_follows() {
269 let id = ChangeId::parse("nixpkgs").unwrap();
270 assert_eq!(id.input().as_str(), "nixpkgs");
271 assert!(id.follows().is_none());
272 }
273
274 #[test]
275 fn success_message_depth_three_has_two_inputs_separators() {
276 let change = Change::Follows {
277 input: ChangeId::parse("neovim.nixvim.flake-parts").unwrap(),
278 target: AttrPath::parse("flake-parts").unwrap(),
279 };
280 let msgs = change.success_messages();
281 assert_eq!(msgs.len(), 1);
282 let msg = &msgs[0];
283 let inputs_count = msg.matches(".inputs.").count();
284 assert_eq!(
285 inputs_count, 2,
286 "depth-3 message should contain exactly two `.inputs.` separators, got: {msg}"
287 );
288 }
289
290 #[test]
291 fn success_message_depth_two_has_one_inputs_separator() {
292 let change = Change::Follows {
293 input: ChangeId::parse("crane.nixpkgs").unwrap(),
294 target: AttrPath::parse("nixpkgs").unwrap(),
295 };
296 let msgs = change.success_messages();
297 let msg = &msgs[0];
298 assert_eq!(msg.matches(".inputs.").count(), 1);
299 }
300
301 #[test]
302 fn success_message_depth_one_uses_inputs_prefix() {
303 let change = Change::Follows {
304 input: ChangeId::parse("nixpkgs").unwrap(),
305 target: AttrPath::parse("foo").unwrap(),
306 };
307 let msgs = change.success_messages();
308 let msg = &msgs[0];
309 assert!(
310 msg.starts_with("Added follows: inputs.nixpkgs.follows ="),
311 "depth-1 message should start with `inputs.<id>.follows =`, got: {msg}"
312 );
313 }
314}