1use serde::Serialize;
25
26use crate::connectors::json_selection::JSONSelection;
27use crate::connectors::json_selection::Key;
28use crate::connectors::json_selection::LitExpr;
29use crate::connectors::json_selection::NamedSelection;
30use crate::connectors::json_selection::PathList;
31use crate::connectors::json_selection::PathSelection;
32use crate::connectors::json_selection::SubSelection;
33use crate::connectors::json_selection::TopLevelSelection;
34use crate::connectors::json_selection::location::OffsetRange;
35use crate::connectors::json_selection::location::Ranged;
36use crate::connectors::json_selection::location::WithRange;
37
38#[derive(Debug, Clone, Serialize)]
50#[serde(tag = "kind", rename_all = "snake_case")]
51pub enum DiffKind {
52 KeyFlippedToLiteralNull {
55 source_range: Option<(usize, usize)>,
56 followed_by: FollowedBy,
57 },
58
59 KeyFlippedToLiteralBool {
63 value: bool,
64 source_range: Option<(usize, usize)>,
65 followed_by: FollowedBy,
66 },
67
68 KeyFieldFlippedToLiteralString {
72 text: String,
73 source_range: Option<(usize, usize)>,
74 followed_by: FollowedBy,
75 },
76
77 KeyQuotedFlippedToLiteralString {
80 text: String,
81 source_range: Option<(usize, usize)>,
82 followed_by: FollowedBy,
83 },
84
85 SubSelectionToLitObject {
91 source_range: Option<(usize, usize)>,
92 },
93
94 LegacyObjectToLitObject {
98 source_range: Option<(usize, usize)>,
99 },
100
101 Other {
104 v03_variant: &'static str,
105 v04_variant: &'static str,
106 source_range: Option<(usize, usize)>,
107 },
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
116#[serde(rename_all = "snake_case")]
117pub enum FollowedBy {
118 Nothing,
120 KeyAccess,
122 Method,
124 SubSelection,
126 Question,
128 Expr,
130}
131
132fn classify_followed_by(tail: &WithRange<PathList>) -> FollowedBy {
133 match tail.as_ref() {
134 PathList::Empty => FollowedBy::Nothing,
135 PathList::Key(_, _) => FollowedBy::KeyAccess,
136 PathList::Method(_, _, _) => FollowedBy::Method,
137 PathList::Selection(_) => FollowedBy::SubSelection,
138 PathList::Question(_) => FollowedBy::Question,
139 PathList::Expr(_, _) => FollowedBy::Expr,
140 PathList::Var(_, _) => FollowedBy::KeyAccess,
141 }
142}
143
144impl JSONSelection {
145 pub fn diff_kinds(&self, other: &Self) -> Vec<DiffKind> {
155 let mut out = Vec::new();
156 diff_top_level(&self.inner, &other.inner, &mut out);
157 out
158 }
159}
160
161fn diff_top_level(v3: &TopLevelSelection, v4: &TopLevelSelection, out: &mut Vec<DiffKind>) {
162 match (v3, v4) {
163 (TopLevelSelection::Named(s3), TopLevelSelection::Named(s4)) => {
164 diff_subselection(s3, s4, out)
165 }
166 (TopLevelSelection::Value(l3), TopLevelSelection::Value(l4)) => diff_litexpr(l3, l4, out),
167 (TopLevelSelection::Named(s3), TopLevelSelection::Value(l4)) => {
172 if let Some(diff) = classify_top_level_key_to_literal(s3, l4) {
173 out.push(diff);
174 return;
175 }
176 out.push(DiffKind::Other {
177 v03_variant: "TopLevel::Named",
178 v04_variant: "TopLevel::Value",
179 source_range: range_to_pair(l4.range()),
180 });
181 }
182 (TopLevelSelection::Value(_), TopLevelSelection::Named(_)) => out.push(DiffKind::Other {
183 v03_variant: "TopLevel::Value",
184 v04_variant: "TopLevel::Named",
185 source_range: None,
186 }),
187 }
188}
189
190fn classify_top_level_key_to_literal(
194 v3: &SubSelection,
195 v4: &WithRange<LitExpr>,
196) -> Option<DiffKind> {
197 let only = match v3.selections.as_slice() {
198 [n] => n,
199 _ => return None,
200 };
201 let path = match only.path.as_ref() {
202 LitExpr::Path(p) => p,
203 _ => return None,
204 };
205 let key = single_key_of_path(path)?;
206 let range = range_to_pair(v4.range());
207 let followed_by = FollowedBy::Nothing;
209 match (key, v4.as_ref()) {
210 (Key::Field(name), LitExpr::Null) if name == "null" => {
211 Some(DiffKind::KeyFlippedToLiteralNull {
212 source_range: range,
213 followed_by,
214 })
215 }
216 (Key::Field(name), LitExpr::Bool(value)) if name == "true" || name == "false" => {
217 Some(DiffKind::KeyFlippedToLiteralBool {
218 value: *value,
219 source_range: range,
220 followed_by,
221 })
222 }
223 (Key::Field(name), LitExpr::String(_)) => Some(DiffKind::KeyFieldFlippedToLiteralString {
224 text: name.clone(),
225 source_range: range,
226 followed_by,
227 }),
228 (Key::Quoted(name), LitExpr::String(_)) => {
229 Some(DiffKind::KeyQuotedFlippedToLiteralString {
230 text: name.clone(),
231 source_range: range,
232 followed_by,
233 })
234 }
235 _ => None,
236 }
237}
238
239fn diff_subselection(v3: &SubSelection, v4: &SubSelection, out: &mut Vec<DiffKind>) {
240 if v3.selections.len() != v4.selections.len() {
241 out.push(DiffKind::Other {
242 v03_variant: "SubSelection",
243 v04_variant: "SubSelection",
244 source_range: range_to_pair(v4.range()),
245 });
246 return;
247 }
248 for (n3, n4) in v3.selections.iter().zip(v4.selections.iter()) {
249 diff_named_selection(n3, n4, out);
250 }
251}
252
253fn diff_named_selection(v3: &NamedSelection, v4: &NamedSelection, out: &mut Vec<DiffKind>) {
254 diff_litexpr(&v3.path, &v4.path, out);
255}
256
257fn diff_litexpr(v3: &WithRange<LitExpr>, v4: &WithRange<LitExpr>, out: &mut Vec<DiffKind>) {
258 let v3_inner = v3.as_ref();
259 let v4_inner = v4.as_ref();
260
261 if v3_inner == v4_inner {
263 return;
264 }
265
266 if let LitExpr::Path(path) = v3_inner
268 && let Some(key) = single_key_of_path(path)
269 {
270 match (key, v4_inner) {
271 (Key::Field(name), LitExpr::Null) if name == "null" => {
272 out.push(DiffKind::KeyFlippedToLiteralNull {
273 source_range: range_to_pair(v4.range()),
274 followed_by: FollowedBy::Nothing,
275 });
276 return;
277 }
278 (Key::Field(name), LitExpr::Bool(value)) if name == "true" || name == "false" => {
279 out.push(DiffKind::KeyFlippedToLiteralBool {
280 value: *value,
281 source_range: range_to_pair(v4.range()),
282 followed_by: FollowedBy::Nothing,
283 });
284 return;
285 }
286 (Key::Field(name), LitExpr::String(s)) => {
287 out.push(DiffKind::KeyFieldFlippedToLiteralString {
288 text: name.clone(),
289 source_range: range_to_pair(v4.range()),
290 followed_by: FollowedBy::Nothing,
291 });
292 let _ = s;
293 return;
294 }
295 (Key::Quoted(name), LitExpr::String(_)) => {
296 out.push(DiffKind::KeyQuotedFlippedToLiteralString {
297 text: name.clone(),
298 source_range: range_to_pair(v4.range()),
299 followed_by: FollowedBy::Nothing,
300 });
301 return;
302 }
303 _ => {}
304 }
305 }
306
307 if let (LitExpr::Path(path), LitExpr::Object(obj4)) = (v3_inner, v4_inner)
310 && let Some(sub3) = path_starts_with_subselection_only(path)
311 {
312 out.push(DiffKind::SubSelectionToLitObject {
313 source_range: range_to_pair(v4.range()),
314 });
315 diff_subselection(sub3, obj4, out);
316 return;
317 }
318
319 if let (LitExpr::LegacyObject(_), LitExpr::Object(_)) = (v3_inner, v4_inner) {
322 out.push(DiffKind::LegacyObjectToLitObject {
323 source_range: range_to_pair(v4.range()),
324 });
325 return;
326 }
327
328 if let (LitExpr::Path(path), LitExpr::LitPath(root, tail4)) = (v3_inner, v4_inner)
332 && let PathList::Key(key, rest3) = path.path.as_ref()
333 {
334 let range = range_to_pair(v4.range());
335 let followed_by = classify_followed_by(tail4);
336 let mut emitted = None;
337 match (key.as_ref(), root.as_ref()) {
338 (Key::Field(name), LitExpr::Null) if name == "null" => {
339 emitted = Some(DiffKind::KeyFlippedToLiteralNull {
340 source_range: range,
341 followed_by,
342 });
343 }
344 (Key::Field(name), LitExpr::Bool(value)) if name == "true" || name == "false" => {
345 emitted = Some(DiffKind::KeyFlippedToLiteralBool {
346 value: *value,
347 source_range: range,
348 followed_by,
349 });
350 }
351 (Key::Field(name), LitExpr::String(_)) => {
352 emitted = Some(DiffKind::KeyFieldFlippedToLiteralString {
353 text: name.clone(),
354 source_range: range,
355 followed_by,
356 });
357 }
358 (Key::Quoted(name), LitExpr::String(_)) => {
359 emitted = Some(DiffKind::KeyQuotedFlippedToLiteralString {
360 text: name.clone(),
361 source_range: range,
362 followed_by,
363 });
364 }
365 _ => {}
366 }
367 if let Some(diff) = emitted {
368 out.push(diff);
369 diff_pathlist(rest3, tail4, out, v4.range());
370 return;
371 }
372 }
373
374 match (v3_inner, v4_inner) {
376 (LitExpr::Object(a), LitExpr::Object(b)) => diff_subselection(a, b, out),
377 (LitExpr::Array(a), LitExpr::Array(b)) => {
378 if a.len() != b.len() {
379 out.push(DiffKind::Other {
380 v03_variant: "LitExpr::Array",
381 v04_variant: "LitExpr::Array",
382 source_range: range_to_pair(v4.range()),
383 });
384 } else {
385 for (x, y) in a.iter().zip(b.iter()) {
386 diff_litexpr(x, y, out);
387 }
388 }
389 }
390 (LitExpr::Path(a), LitExpr::Path(b)) => diff_pathselection(a, b, out, v4.range()),
391 _ => out.push(DiffKind::Other {
393 v03_variant: lit_variant_name(v3_inner),
394 v04_variant: lit_variant_name(v4_inner),
395 source_range: range_to_pair(v4.range()),
396 }),
397 }
398}
399
400fn diff_pathselection(
401 v3: &PathSelection,
402 v4: &PathSelection,
403 out: &mut Vec<DiffKind>,
404 range: OffsetRange,
405) {
406 diff_pathlist(&v3.path, &v4.path, out, range);
407}
408
409fn diff_pathlist(
410 v3: &WithRange<PathList>,
411 v4: &WithRange<PathList>,
412 out: &mut Vec<DiffKind>,
413 fallback_range: OffsetRange,
414) {
415 if v3.as_ref() == v4.as_ref() {
416 return;
417 }
418 match (v3.as_ref(), v4.as_ref()) {
419 (PathList::Key(_, tail3), PathList::Key(_, tail4)) => {
420 diff_pathlist(tail3, tail4, out, fallback_range);
421 }
422 (PathList::Var(_, tail3), PathList::Var(_, tail4)) => {
423 diff_pathlist(tail3, tail4, out, fallback_range);
424 }
425 (PathList::Method(_, args3, tail3), PathList::Method(_, args4, tail4)) => {
426 match (args3, args4) {
427 (Some(a3), Some(a4)) if a3.args.len() == a4.args.len() => {
428 for (x, y) in a3.args.iter().zip(a4.args.iter()) {
429 diff_litexpr(x, y, out);
430 }
431 }
432 (None, None) => {}
433 _ => out.push(DiffKind::Other {
434 v03_variant: "MethodArgs",
435 v04_variant: "MethodArgs",
436 source_range: range_to_pair(fallback_range.clone()),
437 }),
438 }
439 diff_pathlist(tail3, tail4, out, fallback_range);
440 }
441 (PathList::Expr(e3, tail3), PathList::Expr(e4, tail4)) => {
442 diff_litexpr(e3, e4, out);
443 diff_pathlist(tail3, tail4, out, fallback_range);
444 }
445 (PathList::Question(tail3), PathList::Question(tail4)) => {
446 diff_pathlist(tail3, tail4, out, fallback_range);
447 }
448 (PathList::Selection(a), PathList::Selection(b)) => diff_subselection(a, b, out),
449 _ => out.push(DiffKind::Other {
450 v03_variant: pathlist_variant_name(v3.as_ref()),
451 v04_variant: pathlist_variant_name(v4.as_ref()),
452 source_range: range_to_pair(v4.range().or(fallback_range)),
453 }),
454 }
455}
456
457fn single_key_of_path(path: &PathSelection) -> Option<&Key> {
458 if let PathList::Key(key, tail) = path.path.as_ref()
459 && matches!(tail.as_ref(), PathList::Empty)
460 {
461 return Some(key.as_ref());
462 }
463 None
464}
465
466fn path_starts_with_subselection_only(path: &PathSelection) -> Option<&SubSelection> {
467 if let PathList::Selection(sub) = path.path.as_ref() {
468 return Some(sub);
469 }
470 None
471}
472
473fn lit_variant_name(l: &LitExpr) -> &'static str {
474 match l {
475 LitExpr::String(_) => "LitExpr::String",
476 LitExpr::Number(_) => "LitExpr::Number",
477 LitExpr::Bool(_) => "LitExpr::Bool",
478 LitExpr::Null => "LitExpr::Null",
479 LitExpr::LegacyObject(_) => "LitExpr::LegacyObject",
480 LitExpr::Object(_) => "LitExpr::Object",
481 LitExpr::Array(_) => "LitExpr::Array",
482 LitExpr::Path(_) => "LitExpr::Path",
483 LitExpr::LitPath(_, _) => "LitExpr::LitPath",
484 LitExpr::OpChain(_, _) => "LitExpr::OpChain",
485 }
486}
487
488fn pathlist_variant_name(p: &PathList) -> &'static str {
489 match p {
490 PathList::Var(_, _) => "PathList::Var",
491 PathList::Key(_, _) => "PathList::Key",
492 PathList::Expr(_, _) => "PathList::Expr",
493 PathList::Method(_, _, _) => "PathList::Method",
494 PathList::Question(_) => "PathList::Question",
495 PathList::Selection(_) => "PathList::Selection",
496 PathList::Empty => "PathList::Empty",
497 }
498}
499
500fn range_to_pair(r: OffsetRange) -> Option<(usize, usize)> {
501 r.map(|range| (range.start, range.end))
502}