///|
/// The semantic kind of a difference between two JSON values.
pub enum DiffKind {
Added
Removed
Changed
TypeChanged
}
///|
/// One difference, addressed by an RFC 6901-style JSON Pointer.
pub(all) struct JsonDiff {
path : String
kind : DiffKind
left : String?
right : String?
}
///|
pub fn DiffKind::to_string(self : DiffKind) -> String {
match self {
Added => "added"
Removed => "removed"
Changed => "changed"
TypeChanged => "type-changed"
}
}
///|
/// Compare two parsed documents semantically. Object member order is ignored,
/// while arrays remain ordered. The result is stable in left-to-right path
/// order, which makes it suitable for review output and snapshot tests.
pub fn diff_json(left : JsonValue, right : JsonValue) -> Array[JsonDiff] {
let differences : Array[JsonDiff] = []
diff_value(left, right, "", differences)
differences
}
///|
/// Parse and compare two documents, returning parser diagnostics as text.
pub fn diff_documents(
left : String,
right : String,
) -> Result[Array[JsonDiff], String] {
let left_value = match parse(left) {
Ok(value) => value
Err(error) => return Err("left document: " + error.message())
}
let right_value = match parse(right) {
Ok(value) => value
Err(error) => return Err("right document: " + error.message())
}
Ok(diff_json(left_value, right_value))
}
///|
fn diff_value(
left : JsonValue,
right : JsonValue,
path : String,
differences : Array[JsonDiff],
) -> Unit {
if canonical_equal(left, right) {
return
}
match (left, right) {
(Array(left_values), Array(right_values)) => {
let shared = if left_values.length() < right_values.length() {
left_values.length()
} else {
right_values.length()
}
let mut index = 0
while index < shared {
diff_value(
left_values[index],
right_values[index],
pointer_index(path, index),
differences,
)
index = index + 1
}
while index < left_values.length() {
differences.push({
path: pointer_index(path, index),
kind: Removed,
left: Some(value_preview(left_values[index])),
right: None,
})
index = index + 1
}
while index < right_values.length() {
differences.push({
path: pointer_index(path, index),
kind: Added,
left: None,
right: Some(value_preview(right_values[index])),
})
index = index + 1
}
}
(Object(left_entries), Object(right_entries)) => {
for entry in left_entries {
let key = entry.0
match object_value(right_entries, key) {
Some(right_value) =>
diff_value(
entry.1,
right_value,
pointer_key(path, key),
differences,
)
None =>
differences.push({
path: pointer_key(path, key),
kind: Removed,
left: Some(value_preview(entry.1)),
right: None,
})
}
}
for entry in right_entries {
if object_value(left_entries, entry.0) is None {
differences.push({
path: pointer_key(path, entry.0),
kind: Added,
left: None,
right: Some(value_preview(entry.1)),
})
}
}
}
_ => {
let kind = if value_kind(left) == value_kind(right) {
Changed
} else {
TypeChanged
}
differences.push({
path,
kind,
left: Some(value_preview(left)),
right: Some(value_preview(right)),
})
}
}
}
///|
fn canonical_equal(left : JsonValue, right : JsonValue) -> Bool {
match (canonicalize(left), canonicalize(right)) {
(Ok(left_text), Ok(right_text)) => left_text == right_text
_ => false
}
}
///|
fn object_value(
entries : Array[(String, JsonValue)],
key : String,
) -> JsonValue? {
for entry in entries {
if entry.0 == key {
return Some(entry.1)
}
}
None
}
///|
fn pointer_key(path : String, key : String) -> String {
path + "/" + pointer_escape(key)
}
///|
fn pointer_index(path : String, index : Int) -> String {
path + "/" + index.to_string()
}
///|
fn pointer_escape(value : String) -> String {
let mut output = ""
for char in value {
if char == '~' {
output = output + "~0"
} else if char == '/' {
output = output + "~1"
} else {
output = output + char.to_string()
}
}
output
}
///|
fn value_kind(value : JsonValue) -> String {
match value {
Null => "null"
Bool(_) => "boolean"
Number(_) => "number"
String(_) => "string"
Array(_) => "array"
Object(_) => "object"
}
}
///|
fn value_preview(value : JsonValue) -> String {
match canonicalize(value) {
Ok(serialized) => serialized
Err(_) => ""
}
}
///|
pub fn JsonDiff::to_text(self : JsonDiff) -> String {
let left = match self.left {
Some(value) => value
None => "-"
}
let right = match self.right {
Some(value) => value
None => "-"
}
self.kind.to_string() + " " + self.path + " : " + left + " -> " + right
}