1use crate::walk::Context;
2
3pub fn split_quoted_path(s: &str) -> Option<(&str, &str)> {
15 let bytes = s.as_bytes();
16 let mut i = 0;
17 while i < bytes.len() {
18 if bytes[i] == b'"' {
19 i += 1;
21 while i < bytes.len() && bytes[i] != b'"' {
22 i += 1;
23 }
24 if i < bytes.len() {
26 i += 1;
27 }
28 } else if bytes[i] == b'.' {
29 return Some((&s[..i], &s[i + 1..]));
30 } else {
31 i += 1;
32 }
33 }
34 None
35}
36
37#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
38pub enum Change {
39 #[default]
40 None,
41 Add {
42 id: Option<String>,
43 uri: Option<String>,
44 flake: bool,
46 },
47 Remove {
48 ids: Vec<ChangeId>,
49 },
50 Change {
51 id: Option<String>,
52 uri: Option<String>,
53 ref_or_rev: Option<String>,
54 },
55 Follows {
59 input: ChangeId,
61 target: String,
63 },
64}
65
66#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
67pub struct ChangeId(String);
68
69impl ChangeId {
70 pub fn follows(&self) -> Option<&str> {
72 split_quoted_path(&self.0).map(|(_, post)| post)
73 }
74
75 pub fn input(&self) -> &str {
77 split_quoted_path(&self.0).map_or(&self.0, |(pre, _)| pre)
78 }
79
80 fn matches(&self, input: &str, follows: Option<&str>) -> bool {
82 if self.input() != input {
83 return false;
84 }
85 match (self.follows(), follows) {
86 (Some(self_follows), Some(f)) => self_follows == f,
87 (Some(_), None) => false,
88 (None, _) => true,
89 }
90 }
91
92 pub fn matches_with_follows(&self, input: &str, follows: Option<String>) -> bool {
93 self.matches(input, follows.as_deref())
94 }
95
96 pub fn matches_with_ctx(&self, follows: &str, ctx: Option<Context>) -> bool {
98 let ctx_input = ctx.and_then(|f| f.level().first().cloned());
99 match ctx_input {
100 Some(input) => self.matches(&input, Some(follows)),
101 None => self.input() == follows,
102 }
103 }
104}
105
106impl std::fmt::Display for ChangeId {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 write!(f, "{}", self.0)
109 }
110}
111
112impl From<String> for ChangeId {
113 fn from(value: String) -> Self {
114 ChangeId(value)
115 }
116}
117
118impl Change {
119 pub fn id(&self) -> Option<ChangeId> {
120 match self {
121 Change::None => None,
122 Change::Add { id, .. } => id.clone().map(|id| id.into()),
123 Change::Remove { ids } => ids.first().cloned(),
124 Change::Change { id, .. } => id.clone().map(|id| id.into()),
125 Change::Follows { input, .. } => Some(input.clone()),
126 }
127 }
128
129 pub fn ids(&self) -> Vec<ChangeId> {
130 match self {
131 Change::Remove { ids } => ids.clone(),
132 Change::Follows { input, .. } => vec![input.clone()],
133 _ => self.id().into_iter().collect(),
134 }
135 }
136 pub fn is_remove(&self) -> bool {
137 matches!(self, Change::Remove { .. })
138 }
139 pub fn is_some(&self) -> bool {
140 !matches!(self, Change::None)
141 }
142 pub fn is_add(&self) -> bool {
143 matches!(self, Change::Add { .. })
144 }
145 pub fn is_change(&self) -> bool {
146 matches!(self, Change::Change { .. })
147 }
148 pub fn is_follows(&self) -> bool {
149 matches!(self, Change::Follows { .. })
150 }
151 pub fn uri(&self) -> Option<&String> {
152 match self {
153 Change::Change { uri, .. } | Change::Add { uri, .. } => uri.as_ref(),
154 _ => None,
155 }
156 }
157 pub fn follows_target(&self) -> Option<&String> {
158 match self {
159 Change::Follows { target, .. } => Some(target),
160 _ => None,
161 }
162 }
163
164 pub fn success_messages(&self) -> Vec<String> {
165 match self {
166 Change::Add { id, uri, .. } => {
167 vec![format!(
168 "Added input: {} = {}",
169 id.as_deref().unwrap_or("?"),
170 uri.as_deref().unwrap_or("?")
171 )]
172 }
173 Change::Remove { ids } => ids
174 .iter()
175 .map(|id| format!("Removed input: {}", id))
176 .collect(),
177 Change::Change { id, uri, .. } => {
178 vec![format!(
179 "Changed input: {} -> {}",
180 id.as_deref().unwrap_or("?"),
181 uri.as_deref().unwrap_or("?")
182 )]
183 }
184 Change::Follows { input, target } => {
185 let msg = if let Some(nested) = input.follows() {
186 format!(
187 "Added follows: {}.inputs.{}.follows = \"{}\"",
188 input.input(),
189 nested,
190 target
191 )
192 } else {
193 format!(
194 "Added follows: inputs.{}.follows = \"{}\"",
195 input.input(),
196 target
197 )
198 };
199 vec![msg]
200 }
201 Change::None => vec![],
202 }
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn split_plain() {
212 assert_eq!(
213 split_quoted_path("crane.nixpkgs"),
214 Some(("crane", "nixpkgs"))
215 );
216 assert_eq!(split_quoted_path("nixpkgs"), None);
217 }
218
219 #[test]
220 fn split_quoted_dot_in_name() {
221 assert_eq!(
222 split_quoted_path("\"hls-1.10\".nixpkgs"),
223 Some(("\"hls-1.10\"", "nixpkgs"))
224 );
225 assert_eq!(split_quoted_path("\"hls-1.10\""), None);
226 }
227
228 #[test]
229 fn change_id_quoted_dot() {
230 let id = ChangeId::from("\"hls-1.10\".nixpkgs".to_string());
231 assert_eq!(id.input(), "\"hls-1.10\"");
232 assert_eq!(id.follows(), Some("nixpkgs"));
233 }
234}