///|
pub fn encode_replay_cases(
cases : ArrayView[CatalogReplayCase],
) -> String raise PricingError {
check_replay_cases(cases)
let values : Array[Json] = cases.map(c => {
"on": c.on,
"usages": c.usages.map(usage_json),
})
bounded_json({ "schema": "jieheng.replay-cases.v1", "cases": values })
}
///|
pub fn decode_replay_cases(
text : String,
) -> Array[CatalogReplayCase] raise PricingError {
let r = fields(parse_document(text), ["schema", "cases"], "replay")
expect_value(r, "schema", "jieheng.replay-cases.v1", "replay")
let cases = bounded_items(r["cases"], 256, "replay.cases").mapi((i, v) => {
let path = "replay.cases[\{i}]"
let c = fields(v, ["on", "usages"], path)
{
on: text_value(c["on"], path + ".on"),
usages: bounded_items(c["usages"], 64, path + ".usages").map(
usage_from_json,
),
}
})
check_replay_cases(cases[:])
cases
}
///|
pub fn encode_replay_policy(policy : ReplayPolicy) -> String raise PricingError {
validate_replay_policy(policy)
let percent = match policy.max_increase_percent {
Some(s) => Json::string(s)
None => Json::null()
}
bounded_json({
"schema": "jieheng.replay-policy.v1",
"currency": policy.currency,
"max_increase": policy.max_increase,
"max_increase_percent": percent,
})
}
///|
pub fn decode_replay_policy(text : String) -> ReplayPolicy raise PricingError {
let r = fields(
parse_document(text),
["schema", "currency", "max_increase", "max_increase_percent"],
"replay.policy",
)
expect_value(r, "schema", "jieheng.replay-policy.v1", "replay.policy")
let policy = {
currency: text_value(r["currency"], "replay.policy.currency"),
max_increase: text_value(r["max_increase"], "replay.policy.max_increase"),
max_increase_percent: match r["max_increase_percent"] {
Null => None
v => Some(text_value(v, "replay.policy.max_increase_percent"))
},
}
validate_replay_policy(policy)
policy
}
///|
pub fn encode_catalog_replay(
report : CatalogReplay,
) -> String raise PricingError {
if report.rows.length() == 0 || report.rows.length() > 256 {
invalid("size", "replay.rows", "expected 1 to 256 rows")
}
for n in [report.increased, report.decreased, report.unchanged] {
if n < 0 || n > 256 {
invalid("count", "replay.counts", "counts must be 0..256")
}
}
if report.max_increase_index is Some(i) && (i < 0 || i > 255) {
invalid(
"index", "replay.max_increase_index", "index must be 0..255 or null",
)
}
let rows : Array[Json] = report.rows.map(r => {
"on": r.on,
"comparison": bill_comparison_json(r.comparison),
})
let index = match report.max_increase_index {
Some(i) => Json(i)
None => Json::null()
}
bounded_json({
"schema": "jieheng.catalog-replay.v1",
"currency": report.currency,
"rows": rows,
"baseline_exact": report.baseline_exact,
"candidate_exact": report.candidate_exact,
"baseline_settled": report.baseline_settled,
"candidate_settled": report.candidate_settled,
"exact_delta": report.exact_delta,
"settled_delta": report.settled_delta,
"increased": report.increased,
"decreased": report.decreased,
"unchanged": report.unchanged,
"max_increase": report.max_increase,
"max_increase_index": index,
})
}
///|
/// Strict structural decoding; verify_catalog_replay still needs both catalogs,
/// complete dated cases and the settlement policy from a trusted source.
pub fn decode_catalog_replay(text : String) -> CatalogReplay raise PricingError {
let r = fields(
parse_document(text),
[
"schema", "currency", "rows", "baseline_exact", "candidate_exact", "baseline_settled",
"candidate_settled", "exact_delta", "settled_delta", "increased", "decreased",
"unchanged", "max_increase", "max_increase_index",
],
"replay",
)
expect_value(r, "schema", "jieheng.catalog-replay.v1", "replay")
let rows : Array[CatalogReplayRow] = bounded_items(
r["rows"],
256,
"replay.rows",
).mapi((i, v) => {
let path = "replay.rows[\{i}]"
let row = fields(v, ["on", "comparison"], path)
{
on: text_value(row["on"], path + ".on"),
comparison: bill_comparison_from_json(row["comparison"]),
}
})
{
currency: text_value(r["currency"], "replay.currency"),
rows,
baseline_exact: text_value(r["baseline_exact"], "replay.baseline_exact"),
candidate_exact: text_value(r["candidate_exact"], "replay.candidate_exact"),
baseline_settled: text_value(
r["baseline_settled"],
"replay.baseline_settled",
),
candidate_settled: text_value(
r["candidate_settled"],
"replay.candidate_settled",
),
exact_delta: text_value(r["exact_delta"], "replay.exact_delta"),
settled_delta: text_value(r["settled_delta"], "replay.settled_delta"),
increased: small_integer(r["increased"], 256, "replay.increased"),
decreased: small_integer(r["decreased"], 256, "replay.decreased"),
unchanged: small_integer(r["unchanged"], 256, "replay.unchanged"),
max_increase: text_value(r["max_increase"], "replay.max_increase"),
max_increase_index: match r["max_increase_index"] {
Null => None
v => Some(small_integer(v, 255, "replay.max_increase_index"))
},
}
}