///|
/// A semantic reason a parsed value would change under JCS normalization.
pub enum CanonicalIssueKind {
NumberSpelling
ObjectMemberOrder
}
///|
pub(all) struct CanonicalIssue {
path : String
kind : CanonicalIssueKind
source : String
canonical : String
}
///|
pub fn CanonicalIssueKind::to_string(self : CanonicalIssueKind) -> String {
match self {
NumberSpelling => "number-spelling"
ObjectMemberOrder => "object-member-order"
}
}
///|
/// Inspect parsed values for semantic normalization changes. Whitespace and
/// string escaping require source text and are covered by `check_canonical`;
/// this API focuses on values where the parser has already discarded layout.
pub fn canonical_issues(value : JsonValue) -> Array[CanonicalIssue] {
let issues : Array[CanonicalIssue] = []
scan_issues(value, "", issues)
issues
}
///|
pub fn diagnose_document(
input : String,
) -> Result[Array[CanonicalIssue], String] {
let value = match parse(input) {
Ok(value) => value
Err(error) => return Err(error.message())
}
Ok(canonical_issues(value))
}
///|
pub fn CanonicalIssue::to_text(self : CanonicalIssue) -> String {
self.kind.to_string() +
" " +
(if self.path == "" { "$" } else { self.path }) +
": " +
self.source +
" -> " +
self.canonical
}
///|
fn scan_issues(
value : JsonValue,
path : String,
issues : Array[CanonicalIssue],
) -> Unit {
match value {
Number(source) =>
match canonical_number(source) {
Ok(canonical) =>
if source != canonical {
issues.push({ path, kind: NumberSpelling, source, canonical })
}
Err(_) => ()
}
Array(values) => {
let mut index = 0
while index < values.length() {
scan_issues(values[index], path + "/" + index.to_string(), issues)
index = index + 1
}
}
Object(entries) => {
let mut previous : String? = None
for entry in entries {
match previous {
Some(key) =>
if compare_keys(key, entry.0) > 0 {
issues.push({
path,
kind: ObjectMemberOrder,
source: key + " before " + entry.0,
canonical: "UTF-16 ascending order",
})
}
None => ()
}
previous = Some(entry.0)
scan_issues(
entry.1,
path + "/" + diagnostic_pointer_escape(entry.0),
issues,
)
}
}
_ => ()
}
}
///|
fn diagnostic_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
}