use serde::Serialize;
use crate::connectors::json_selection::JSONSelection;
use crate::connectors::json_selection::Key;
use crate::connectors::json_selection::LitExpr;
use crate::connectors::json_selection::NamedSelection;
use crate::connectors::json_selection::PathList;
use crate::connectors::json_selection::PathSelection;
use crate::connectors::json_selection::SubSelection;
use crate::connectors::json_selection::TopLevelSelection;
use crate::connectors::json_selection::location::OffsetRange;
use crate::connectors::json_selection::location::Ranged;
use crate::connectors::json_selection::location::WithRange;
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum DiffKind {
KeyFlippedToLiteralNull {
source_range: Option<(usize, usize)>,
followed_by: FollowedBy,
},
KeyFlippedToLiteralBool {
value: bool,
source_range: Option<(usize, usize)>,
followed_by: FollowedBy,
},
KeyFieldFlippedToLiteralString {
text: String,
source_range: Option<(usize, usize)>,
followed_by: FollowedBy,
},
KeyQuotedFlippedToLiteralString {
text: String,
source_range: Option<(usize, usize)>,
followed_by: FollowedBy,
},
SubSelectionToLitObject {
source_range: Option<(usize, usize)>,
},
LegacyObjectToLitObject {
source_range: Option<(usize, usize)>,
},
Other {
v03_variant: &'static str,
v04_variant: &'static str,
source_range: Option<(usize, usize)>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum FollowedBy {
Nothing,
KeyAccess,
Method,
SubSelection,
Question,
Expr,
}
fn classify_followed_by(tail: &WithRange<PathList>) -> FollowedBy {
match tail.as_ref() {
PathList::Empty => FollowedBy::Nothing,
PathList::Key(_, _) => FollowedBy::KeyAccess,
PathList::Method(_, _, _) => FollowedBy::Method,
PathList::Selection(_) => FollowedBy::SubSelection,
PathList::Question(_) => FollowedBy::Question,
PathList::Expr(_, _) => FollowedBy::Expr,
PathList::Var(_, _) => FollowedBy::KeyAccess,
}
}
impl JSONSelection {
pub fn diff_kinds(&self, other: &Self) -> Vec<DiffKind> {
let mut out = Vec::new();
diff_top_level(&self.inner, &other.inner, &mut out);
out
}
}
fn diff_top_level(v3: &TopLevelSelection, v4: &TopLevelSelection, out: &mut Vec<DiffKind>) {
match (v3, v4) {
(TopLevelSelection::Named(s3), TopLevelSelection::Named(s4)) => {
diff_subselection(s3, s4, out)
}
(TopLevelSelection::Value(l3), TopLevelSelection::Value(l4)) => diff_litexpr(l3, l4, out),
(TopLevelSelection::Named(s3), TopLevelSelection::Value(l4)) => {
if let Some(diff) = classify_top_level_key_to_literal(s3, l4) {
out.push(diff);
return;
}
out.push(DiffKind::Other {
v03_variant: "TopLevel::Named",
v04_variant: "TopLevel::Value",
source_range: range_to_pair(l4.range()),
});
}
(TopLevelSelection::Value(_), TopLevelSelection::Named(_)) => out.push(DiffKind::Other {
v03_variant: "TopLevel::Value",
v04_variant: "TopLevel::Named",
source_range: None,
}),
}
}
fn classify_top_level_key_to_literal(
v3: &SubSelection,
v4: &WithRange<LitExpr>,
) -> Option<DiffKind> {
let only = match v3.selections.as_slice() {
[n] => n,
_ => return None,
};
let path = match only.path.as_ref() {
LitExpr::Path(p) => p,
_ => return None,
};
let key = single_key_of_path(path)?;
let range = range_to_pair(v4.range());
let followed_by = FollowedBy::Nothing;
match (key, v4.as_ref()) {
(Key::Field(name), LitExpr::Null) if name == "null" => {
Some(DiffKind::KeyFlippedToLiteralNull {
source_range: range,
followed_by,
})
}
(Key::Field(name), LitExpr::Bool(value)) if name == "true" || name == "false" => {
Some(DiffKind::KeyFlippedToLiteralBool {
value: *value,
source_range: range,
followed_by,
})
}
(Key::Field(name), LitExpr::String(_)) => Some(DiffKind::KeyFieldFlippedToLiteralString {
text: name.clone(),
source_range: range,
followed_by,
}),
(Key::Quoted(name), LitExpr::String(_)) => {
Some(DiffKind::KeyQuotedFlippedToLiteralString {
text: name.clone(),
source_range: range,
followed_by,
})
}
_ => None,
}
}
fn diff_subselection(v3: &SubSelection, v4: &SubSelection, out: &mut Vec<DiffKind>) {
if v3.selections.len() != v4.selections.len() {
out.push(DiffKind::Other {
v03_variant: "SubSelection",
v04_variant: "SubSelection",
source_range: range_to_pair(v4.range()),
});
return;
}
for (n3, n4) in v3.selections.iter().zip(v4.selections.iter()) {
diff_named_selection(n3, n4, out);
}
}
fn diff_named_selection(v3: &NamedSelection, v4: &NamedSelection, out: &mut Vec<DiffKind>) {
diff_litexpr(&v3.path, &v4.path, out);
}
fn diff_litexpr(v3: &WithRange<LitExpr>, v4: &WithRange<LitExpr>, out: &mut Vec<DiffKind>) {
let v3_inner = v3.as_ref();
let v4_inner = v4.as_ref();
if v3_inner == v4_inner {
return;
}
if let LitExpr::Path(path) = v3_inner
&& let Some(key) = single_key_of_path(path)
{
match (key, v4_inner) {
(Key::Field(name), LitExpr::Null) if name == "null" => {
out.push(DiffKind::KeyFlippedToLiteralNull {
source_range: range_to_pair(v4.range()),
followed_by: FollowedBy::Nothing,
});
return;
}
(Key::Field(name), LitExpr::Bool(value)) if name == "true" || name == "false" => {
out.push(DiffKind::KeyFlippedToLiteralBool {
value: *value,
source_range: range_to_pair(v4.range()),
followed_by: FollowedBy::Nothing,
});
return;
}
(Key::Field(name), LitExpr::String(s)) => {
out.push(DiffKind::KeyFieldFlippedToLiteralString {
text: name.clone(),
source_range: range_to_pair(v4.range()),
followed_by: FollowedBy::Nothing,
});
let _ = s;
return;
}
(Key::Quoted(name), LitExpr::String(_)) => {
out.push(DiffKind::KeyQuotedFlippedToLiteralString {
text: name.clone(),
source_range: range_to_pair(v4.range()),
followed_by: FollowedBy::Nothing,
});
return;
}
_ => {}
}
}
if let (LitExpr::Path(path), LitExpr::Object(obj4)) = (v3_inner, v4_inner)
&& let Some(sub3) = path_starts_with_subselection_only(path)
{
out.push(DiffKind::SubSelectionToLitObject {
source_range: range_to_pair(v4.range()),
});
diff_subselection(sub3, obj4, out);
return;
}
if let (LitExpr::LegacyObject(_), LitExpr::Object(_)) = (v3_inner, v4_inner) {
out.push(DiffKind::LegacyObjectToLitObject {
source_range: range_to_pair(v4.range()),
});
return;
}
if let (LitExpr::Path(path), LitExpr::LitPath(root, tail4)) = (v3_inner, v4_inner)
&& let PathList::Key(key, rest3) = path.path.as_ref()
{
let range = range_to_pair(v4.range());
let followed_by = classify_followed_by(tail4);
let mut emitted = None;
match (key.as_ref(), root.as_ref()) {
(Key::Field(name), LitExpr::Null) if name == "null" => {
emitted = Some(DiffKind::KeyFlippedToLiteralNull {
source_range: range,
followed_by,
});
}
(Key::Field(name), LitExpr::Bool(value)) if name == "true" || name == "false" => {
emitted = Some(DiffKind::KeyFlippedToLiteralBool {
value: *value,
source_range: range,
followed_by,
});
}
(Key::Field(name), LitExpr::String(_)) => {
emitted = Some(DiffKind::KeyFieldFlippedToLiteralString {
text: name.clone(),
source_range: range,
followed_by,
});
}
(Key::Quoted(name), LitExpr::String(_)) => {
emitted = Some(DiffKind::KeyQuotedFlippedToLiteralString {
text: name.clone(),
source_range: range,
followed_by,
});
}
_ => {}
}
if let Some(diff) = emitted {
out.push(diff);
diff_pathlist(rest3, tail4, out, v4.range());
return;
}
}
match (v3_inner, v4_inner) {
(LitExpr::Object(a), LitExpr::Object(b)) => diff_subselection(a, b, out),
(LitExpr::Array(a), LitExpr::Array(b)) => {
if a.len() != b.len() {
out.push(DiffKind::Other {
v03_variant: "LitExpr::Array",
v04_variant: "LitExpr::Array",
source_range: range_to_pair(v4.range()),
});
} else {
for (x, y) in a.iter().zip(b.iter()) {
diff_litexpr(x, y, out);
}
}
}
(LitExpr::Path(a), LitExpr::Path(b)) => diff_pathselection(a, b, out, v4.range()),
_ => out.push(DiffKind::Other {
v03_variant: lit_variant_name(v3_inner),
v04_variant: lit_variant_name(v4_inner),
source_range: range_to_pair(v4.range()),
}),
}
}
fn diff_pathselection(
v3: &PathSelection,
v4: &PathSelection,
out: &mut Vec<DiffKind>,
range: OffsetRange,
) {
diff_pathlist(&v3.path, &v4.path, out, range);
}
fn diff_pathlist(
v3: &WithRange<PathList>,
v4: &WithRange<PathList>,
out: &mut Vec<DiffKind>,
fallback_range: OffsetRange,
) {
if v3.as_ref() == v4.as_ref() {
return;
}
match (v3.as_ref(), v4.as_ref()) {
(PathList::Key(_, tail3), PathList::Key(_, tail4)) => {
diff_pathlist(tail3, tail4, out, fallback_range);
}
(PathList::Var(_, tail3), PathList::Var(_, tail4)) => {
diff_pathlist(tail3, tail4, out, fallback_range);
}
(PathList::Method(_, args3, tail3), PathList::Method(_, args4, tail4)) => {
match (args3, args4) {
(Some(a3), Some(a4)) if a3.args.len() == a4.args.len() => {
for (x, y) in a3.args.iter().zip(a4.args.iter()) {
diff_litexpr(x, y, out);
}
}
(None, None) => {}
_ => out.push(DiffKind::Other {
v03_variant: "MethodArgs",
v04_variant: "MethodArgs",
source_range: range_to_pair(fallback_range.clone()),
}),
}
diff_pathlist(tail3, tail4, out, fallback_range);
}
(PathList::Expr(e3, tail3), PathList::Expr(e4, tail4)) => {
diff_litexpr(e3, e4, out);
diff_pathlist(tail3, tail4, out, fallback_range);
}
(PathList::Question(tail3), PathList::Question(tail4)) => {
diff_pathlist(tail3, tail4, out, fallback_range);
}
(PathList::Selection(a), PathList::Selection(b)) => diff_subselection(a, b, out),
_ => out.push(DiffKind::Other {
v03_variant: pathlist_variant_name(v3.as_ref()),
v04_variant: pathlist_variant_name(v4.as_ref()),
source_range: range_to_pair(v4.range().or(fallback_range)),
}),
}
}
fn single_key_of_path(path: &PathSelection) -> Option<&Key> {
if let PathList::Key(key, tail) = path.path.as_ref()
&& matches!(tail.as_ref(), PathList::Empty)
{
return Some(key.as_ref());
}
None
}
fn path_starts_with_subselection_only(path: &PathSelection) -> Option<&SubSelection> {
if let PathList::Selection(sub) = path.path.as_ref() {
return Some(sub);
}
None
}
fn lit_variant_name(l: &LitExpr) -> &'static str {
match l {
LitExpr::String(_) => "LitExpr::String",
LitExpr::Number(_) => "LitExpr::Number",
LitExpr::Bool(_) => "LitExpr::Bool",
LitExpr::Null => "LitExpr::Null",
LitExpr::LegacyObject(_) => "LitExpr::LegacyObject",
LitExpr::Object(_) => "LitExpr::Object",
LitExpr::Array(_) => "LitExpr::Array",
LitExpr::Path(_) => "LitExpr::Path",
LitExpr::LitPath(_, _) => "LitExpr::LitPath",
LitExpr::OpChain(_, _) => "LitExpr::OpChain",
}
}
fn pathlist_variant_name(p: &PathList) -> &'static str {
match p {
PathList::Var(_, _) => "PathList::Var",
PathList::Key(_, _) => "PathList::Key",
PathList::Expr(_, _) => "PathList::Expr",
PathList::Method(_, _, _) => "PathList::Method",
PathList::Question(_) => "PathList::Question",
PathList::Selection(_) => "PathList::Selection",
PathList::Empty => "PathList::Empty",
}
}
fn range_to_pair(r: OffsetRange) -> Option<(usize, usize)> {
r.map(|range| (range.start, range.end))
}