///|
priv enum JsonValue {
JsonString(String)
JsonNumber(Int)
JsonDecimal(Double)
JsonBool(Bool)
JsonArray(Array[JsonValue])
JsonObject(Array[(String, JsonValue)])
}
///|
fn error_text(error : PinyinError) -> String {
match error {
InvalidSeparator(reason) => "invalid_separator:" + reason
InvalidSyllable(value, reason) => "invalid_syllable:" + value + ":" + reason
UnknownCharacter(code) => "unknown_character:" + code.to_string()
AmbiguousCharacter(code, count) =>
"ambiguous_character:" + code.to_string() + ":" + count.to_string()
InvalidSourceSpan(start, end) =>
"invalid_source_span:" + start.to_string() + ":" + end.to_string()
InvalidLexiconEntry(value, reason) =>
"invalid_lexicon_entry:" + value + ":" + reason
ConflictingLexiconEntry(value) => "conflicting_lexicon_entry:" + value
InvalidLexiconText(reason) => "invalid_lexicon_text:" + reason
InvalidBatchItem(value, reason) =>
"invalid_batch_item:" + value + ":" + reason
InvalidSearchDocument(value, reason) =>
"invalid_search_document:" + value + ":" + reason
InvalidSearchOptions(name, reason) =>
"invalid_search_options:" + name + ":" + reason
InvalidSearchQuery(reason) => "invalid_search_query:" + reason
InvalidVariantLimit(limit) => "invalid_variant_limit:" + limit.to_string()
InvalidConversionResult(reason) => "invalid_conversion_result:" + reason
InvalidAlignment(reason) => "invalid_alignment:" + reason
InvalidWeight(name, value) =>
"invalid_weight:" + name + ":" + value.to_string()
InvalidGeneratedData(reason) => "invalid_generated_data:" + reason
}
}
///|
fn json_hex_digit(value : Int) -> String {
let code = if value < 10 { 48 + value } else { 65 + value - 10 }
code.to_uint16().unsafe_to_char().to_string()
}
///|
fn json_control_escape(code : Int) -> String {
"\\u" +
json_hex_digit(code / 4096 % 16) +
json_hex_digit(code / 256 % 16) +
json_hex_digit(code / 16 % 16) +
json_hex_digit(code % 16)
}
///|
fn json_escape(value : String) -> String {
let mut output = ""
for code in text_code_points(value) {
let escaped = match code {
34 => "\\\""
92 => "\\\\"
8 => "\\b"
9 => "\\t"
10 => "\\n"
12 => "\\f"
13 => "\\r"
_ =>
if code >= 0 && code <= 31 {
json_control_escape(code)
} else {
code_points_text([code])
}
}
output = output + escaped
}
output
}
///|
fn json_encode(value : JsonValue) -> String {
match value {
JsonString(text) => "\"" + json_escape(text) + "\""
JsonNumber(number) => number.to_string()
JsonDecimal(number) => number.to_string()
JsonBool(value) => if value { "true" } else { "false" }
JsonArray(values) => {
let mut output = "["
for index = 0; index < values.length(); index = index + 1 {
if index > 0 {
output = output + ","
}
output = output + json_encode(values[index])
}
output + "]"
}
JsonObject(fields) => {
let mut output = "{"
for index = 0; index < fields.length(); index = index + 1 {
if index > 0 {
output = output + ","
}
let (name, field_value) = fields[index]
output = output +
"\"" +
json_escape(name) +
"\":" +
json_encode(field_value)
}
output + "}"
}
}
}
///|
fn batch_item_json(item : BatchItemResult) -> JsonValue {
let source = item.item()
match item.outcome() {
BatchSucceeded(converted) =>
JsonObject([
("id", JsonString(source.identifier())),
("source", JsonString(source.source())),
("status", JsonString("ok")),
("rendered", JsonString(converted.rendered())),
])
BatchFailed(error) =>
JsonObject([
("id", JsonString(source.identifier())),
("source", JsonString(source.source())),
("status", JsonString("error")),
("error", JsonString(error_text(error))),
])
}
}
///|
fn batch_summary_json(summary : ConversionSummary) -> JsonValue {
JsonObject([
("total", JsonNumber(summary.total())),
("succeeded", JsonNumber(summary.succeeded())),
("failed", JsonNumber(summary.failed())),
("unknown_failures", JsonNumber(summary.unknown_failures())),
("ambiguous_failures", JsonNumber(summary.ambiguous_failures())),
])
}
///|
/// Produces a stable line-oriented report in caller input order.
pub fn batch_report_text(result : BatchResult) -> String {
let summary = result.summary()
let mut output = "items=" +
summary.total().to_string() +
"\nsucceeded=" +
summary.succeeded().to_string() +
"\nfailed=" +
summary.failed().to_string()
for item in result.items() {
output = output + "\n[" + item.item().identifier() + "] "
match item.outcome() {
BatchSucceeded(converted) =>
output = output + "ok: " + converted.rendered()
BatchFailed(error) => output = output + "error: " + error_text(error)
}
}
output
}
///|
/// Produces stable compact JSON using a structured encoder.
pub fn batch_report_json(result : BatchResult) -> String {
let item_values : Array[JsonValue] = []
for item in result.items() {
item_values.push(batch_item_json(item))
}
json_encode(
JsonObject([
("summary", batch_summary_json(result.summary())),
("items", JsonArray(item_values)),
]),
)
}
///|
fn json_string_array(values : Array[String]) -> JsonValue {
let output : Array[JsonValue] = []
for value in values {
output.push(JsonString(value))
}
JsonArray(output)
}
///|
fn token_kind_text(kind : TokenKind) -> String {
match kind {
PinyinToken => "pinyin"
LiteralToken => "literal"
}
}
///|
fn decision_source_text(source : DecisionSource) -> String {
match source {
PhraseLexicon => "phrase_lexicon"
UnicodeReading => "unicode_reading"
UnknownPreserved => "unknown_preserved"
}
}
///|
fn converted_token_json(token : ConvertedToken) -> JsonValue {
JsonObject([
("original", JsonString(token.original())),
("rendered", JsonString(token.rendered())),
("kind", JsonString(token_kind_text(token.kind()))),
("decision", JsonString(decision_source_text(token.decision_source()))),
("start", JsonNumber(token.span().start())),
("end", JsonNumber(token.span().end())),
("candidates", json_string_array(token.candidates())),
])
}
///|
fn conversion_summary_json(stats : ConversionStats) -> JsonValue {
JsonObject([
("source_scalars", JsonNumber(stats.source_scalars())),
("output_tokens", JsonNumber(stats.output_tokens())),
("pinyin_tokens", JsonNumber(stats.pinyin_tokens())),
("literal_tokens", JsonNumber(stats.literal_tokens())),
("phrase_decisions", JsonNumber(stats.phrase_decisions())),
("unicode_decisions", JsonNumber(stats.unicode_decisions())),
("preserved_unknown", JsonNumber(stats.preserved_unknown())),
("diagnostics", JsonNumber(stats.diagnostics())),
])
}
///|
fn diagnostic_json(value : Diagnostic) -> JsonValue {
let fields : Array[(String, JsonValue)] = [
("code", JsonString(value.code())),
("message", JsonString(value.message())),
]
match value.span() {
Some(span) => {
fields.push(("start", JsonNumber(span.start())))
fields.push(("end", JsonNumber(span.end())))
}
None => ()
}
JsonObject(fields)
}
///|
/// Emits conversion tokens, decisions, candidates, spans, and diagnostics.
pub fn conversion_report_json(result : ConversionResult) -> String {
let tokens : Array[JsonValue] = []
for token in result.tokens() {
tokens.push(converted_token_json(token))
}
let diagnostics : Array[JsonValue] = []
for value in result.diagnostics() {
diagnostics.push(diagnostic_json(value))
}
json_encode(
JsonObject([
("source", JsonString(result.source())),
("rendered", JsonString(result.rendered())),
("summary", conversion_summary_json(analyze_conversion(result))),
("tokens", JsonArray(tokens)),
("diagnostics", JsonArray(diagnostics)),
]),
)
}
///|
fn reading_evidence_json(value : ScalarReadingEvidence) -> JsonValue {
JsonObject([
("code_point", JsonNumber(value.code_point())),
("original", JsonString(value.original())),
("start", JsonNumber(value.span().start())),
("end", JsonNumber(value.span().end())),
("known", JsonBool(value.is_known())),
("ambiguous", JsonBool(value.is_ambiguous())),
("candidates", json_string_array(value.candidates())),
])
}
///|
/// Emits source coverage and per-scalar Unicode reading candidates.
pub fn reading_inspection_report_json(inspection : ReadingInspection) -> String {
let entries : Array[JsonValue] = []
for value in inspection.entries() {
entries.push(reading_evidence_json(value))
}
json_encode(
JsonObject([
(
"summary",
JsonObject([
("total_scalars", JsonNumber(inspection.total_scalars())),
("known_scalars", JsonNumber(inspection.known_scalars())),
("unknown_scalars", JsonNumber(inspection.unknown_scalars())),
("ambiguous_scalars", JsonNumber(inspection.ambiguous_scalars())),
]),
),
("entries", JsonArray(entries)),
]),
)
}
///|
fn search_hit_json(hit : SearchHit) -> JsonValue {
let explanation = hit.explanation()
JsonObject([
("id", JsonString(hit.identifier())),
("source", JsonString(hit.source())),
("position", JsonNumber(hit.original_position())),
("score", JsonDecimal(explanation.score())),
("exact", JsonDecimal(explanation.exact_component())),
("prefix", JsonDecimal(explanation.prefix_component())),
("initials", JsonDecimal(explanation.initials_component())),
("fuzzy", JsonDecimal(explanation.fuzzy_component())),
("distance", JsonDecimal(explanation.distance().cost())),
])
}
///|
/// Emits ranked hits and all score components in stable order.
pub fn search_hits_report_json(
query : String,
hits : Array[SearchHit],
) -> String {
let values : Array[JsonValue] = []
for hit in hits {
values.push(search_hit_json(hit))
}
json_encode(
JsonObject([
("query", JsonString(query)),
("count", JsonNumber(hits.length())),
("hits", JsonArray(values)),
]),
)
}