///|
/// Escapes a string for use as a JSON string value.
pub fn json_escape(value : String) -> String {
let builder = StringBuilder(size_hint=value.length())
for index = 0; index < value.length(); index = index + 1 {
match value.unsafe_get(index) {
'"' => builder.write_string("\\\"")
'\\' => builder.write_string("\\\\")
'\n' => builder.write_string("\\n")
'\r' => builder.write_string("\\r")
'\t' => builder.write_string("\\t")
'\b' => builder.write_string("\\b")
'\u{000C}' => builder.write_string("\\f")
char => builder.write_char(char.to_int().unsafe_to_char())
}
}
builder.to_string()
}
///|
/// Encodes one journal event. `payload_json` must return a valid JSON value.
pub fn[P] event_to_json(
event : JournalEvent[P],
payload_json : (P) -> String,
) -> String {
"{\"sequence\":\{event.sequence},\"kind\":\"\{json_escape(event.kind)}\",\"payload\":\{payload_json(event.payload)},\"correlation_id\":\"\{json_escape(event.correlation_id)}\",\"previous_hash\":\{event.previous_hash},\"hash\":\{event.hash}}"
}
///|
/// Encodes a journal as a stable JSON array.
pub fn[P] journal_to_json(
journal : Journal[P],
payload_json : (P) -> String,
) -> String {
let builder = StringBuilder()
builder.write_char('[')
let events = journal.events()
for index = 0; index < events.length(); index = index + 1 {
if index > 0 {
builder.write_char(',')
}
builder.write_string(event_to_json(events[index], payload_json))
}
builder.write_char(']')
builder.to_string()
}
///|
/// Returns a stable machine-readable label for a replay status.
pub fn replay_status_name(status : ReplayStatus) -> String {
match status {
Completed => "completed"
Rejected(_, _) => "rejected"
InvalidJournal(_) => "invalid_journal"
InvalidCheckpoint(_) => "invalid_checkpoint"
}
}
///|
/// Returns a stable machine-readable label for a divergence kind.
pub fn divergence_kind_name(kind : DivergenceKind) -> String {
match kind {
StateMismatch => "state_mismatch"
StatusMismatch => "status_mismatch"
LengthMismatch => "length_mismatch"
EventMismatch => "event_mismatch"
}
}
///|
/// Encodes the summary and final state of a replay report.
///
/// `state_json` must return a valid JSON value.
pub fn[S] replay_report_to_json(
report : ReplayReport[S],
state_json : (S) -> String,
) -> String {
"{\"status\":\"\{replay_status_name(report.status)}\",\"initial_sequence\":\{report.initial_sequence},\"applied_events\":\{report.applied_events},\"final_sequence\":\{report.final_sequence},\"final_state\":\{state_json(report.final_state)},\"final_state_hash\":\{report.final_state_hash},\"journal_tail_hash\":\{report.journal_tail_hash}}"
}
///|
/// Encodes the portable summary of a replay evidence tree.
pub fn replay_evidence_to_json(tree : ReplayEvidenceTree) -> String {
"{\"event_count\":\{tree.event_count},\"root\":\{tree.root},\"levels\":\{tree.levels.length()}}"
}
///|
/// Encodes an inclusion proof without application state serialization.
pub fn replay_inclusion_proof_to_json(proof : ReplayInclusionProof) -> String {
let builder = StringBuilder()
builder.write_string(
"{\"sequence\":\{proof.sequence},\"event_hash\":\{proof.event_hash},\"state_hash\":\{proof.state_hash},\"leaf_hash\":\{proof.leaf_hash},\"root\":\{proof.root},\"path\":[",
)
for index = 0; index < proof.siblings.length(); index = index + 1 {
if index > 0 {
builder.write_char(',')
}
let side = if proof.sibling_on_left[index] { "left" } else { "right" }
builder.write_string(
"{\"sibling\":\{proof.siblings[index]},\"side\":\"\{side}\"}",
)
}
builder.write_string("]}")
builder.to_string()
}