// Copyright (c) 2026 moonbit-bimap contributors
// SPDX-License-Identifier: Apache-2.0
// from_json / from_json_with for BiMap (and, from M3, BiBTreeMap): the
// deserialization entry point with STRICT bijection validation, plus the
// shared BiMapDecodeError type.
//
// This is a data ENTRY point, not a mutation chokepoint: after validating the
// input (whole-object decode first, then O(n) right-value dedup) the map is
// built through the PUBLIC construction path (from_array), so the private
// forward/backward/order/positions fields are never touched here (AGENTS.md
// discipline). Design authority: docs/plans/v0.3.0.md; normative semantics
// in SPEC §12.
// ---------------------------------------------------------------------------
// Error type
// ---------------------------------------------------------------------------
///|
/// Error raised by `from_json` / `from_json_with`.
///
/// Toolchain ruling (moonc v0.10.12): a plain `enum` cannot be raised
/// ([4127] "Type is not an error type") — error types must be declared as
/// `suberror`. The public surface (name, variants, payloads, derives)
/// matches docs/plans/v0.3.0.md §2 exactly.
pub(all) suberror BiMapDecodeError {
///
/// JSON syntax/type error from the core @json decoder, passed through
/// unchanged. Raised for non-object inputs (core's "Map::from_json:
/// expected object") and for any value whose JSON type does not decode as
/// `R`. Value decode errors take priority over conflict detection
/// (plan §3.5, SPEC §12).
Decode(@json.JsonDecodeError)
///
/// Strict bijection conflict: two different left keys decode to the same
/// right value. Payload order (plan §2): (right-value rendering, occupied
/// left-key rendering, conflicting new left-key rendering) — all three are
/// PRE-rendered strings:
/// - right value: `Json::stringify()` of the raw JSON node of the
/// incoming pair (ruling 2026-09: R has no Show bound, and core
/// `Show for Json` is deprecated — stringify is compact and stable);
/// - left keys: the text keys, rendered as JSON strings
/// (`Json::string(k).stringify()` — quoting/escaping follow stringify).
DuplicateRightValue(String, String, String)
} derive(Debug, Eq)
///|
/// Hand-written Show — the stable text rendering shared by error messages
/// and the Tier-2 golden snapshots (plan §2). The format is LOCKED (recorded
/// in SPEC §12.2; do not change without updating SPEC and the golden tests):
///
/// - `DuplicateRightValue(1, "a", "b")` — the pre-rendered payload strings
/// joined with ", ";
/// - `Decode()` — core's derived Show, which
/// includes the JSON-pointer path, e.g.
/// `Decode(JsonDecodeError((/a, "Int::from_json: expected number")))`.
pub impl Show for BiMapDecodeError with fn output(self, logger) {
match self {
Decode(err) => {
logger.write_string("Decode(")
Show::output(err, logger)
logger.write_string(")")
}
DuplicateRightValue(right, first_left, new_left) => {
logger.write_string("DuplicateRightValue(")
logger.write_string(right)
logger.write_string(", ")
logger.write_string(first_left)
logger.write_string(", ")
logger.write_string(new_left)
logger.write_string(")")
}
}
}
// ---------------------------------------------------------------------------
// Private helpers (shared by both map types)
// ---------------------------------------------------------------------------
///|
/// Identity parse_key for the String-keyed `from_json` (P1-2: total by
/// construction).
fn identity_parse_key(k : String) -> String {
k
}
///|
/// Render a text key exactly like a JSON object key: `Json::string(k)`
/// stringified — quoting and escaping rules follow stringify output (the
/// ruling's "引号规则随 stringify 输出").
fn render_key(k : String) -> String {
Json::string(k).stringify()
}
///|
/// Raw JSON node for a text key of the decoded object. Guaranteed present:
/// `obj` was decoded from `json` by core's Map decoder, so every text key
/// exists in the raw object (the abort branch is unreachable).
fn raw_node_of(raw : Map[String, Json], k : String) -> Json {
match raw.get(k) {
Some(j) => j
None =>
abort(
"BiMap::from_json_with: unreachable - decoded key missing from the raw object",
)
}
}
// ---------------------------------------------------------------------------
// BiMap deserialization
// ---------------------------------------------------------------------------
///|
/// Deserialize a `BiMap` from a JSON object. Object keys become `String`
/// left keys; the map's insertion order equals the TEXT key order of the
/// JSON (core Map preserves order — the M1 ordering smoke test pins this),
/// and duplicate text keys are last-wins (§3.2).
///
/// Strict bijection validation: two different left keys decoding to the same
/// right value raise `DuplicateRightValue` — the whole parse fails, no
/// partial map, no silent C3 eviction (the deviation from Rust bimap's serde,
/// which silently overwrites; SPEC §12.1). Non-object input or any value
/// decode failure raises `Decode` with core's `@json.JsonDecodeError`
/// passed through; value decode errors take priority over conflict detection
/// (§3.5). For a `String`-keyed map `from_json(m.to_json()) == m` holds
/// (Eq set semantics) with the same pair order.
pub fn[R : FromJson + Hash + Eq] BiMap::from_json(
json : Json,
) -> BiMap[String, R] raise BiMapDecodeError {
BiMap::from_json_with(json, identity_parse_key)
}
///|
/// Deserialize a `BiMap` from a JSON object, mapping each object key through
/// `parse_key : (String) -> K` before inserting.
///
/// **`parse_key` MUST be total (P1-2)**: the signature has no raise channel,
/// so a parse failure must be absorbed inside parse_key (e.g. catch
/// `@string.parse_int` and fall back to a default value). parse_key
/// collisions (two text keys mapping to the same logical key) are last-wins,
/// like duplicate text keys (§3.2, P2 ruling). All other semantics — text
/// order preservation, strict right-value conflict errors, error priority —
/// are identical to `BiMap::from_json`.
pub fn[K : Hash + Eq, R : FromJson + Hash + Eq] BiMap::from_json_with(
json : Json,
parse_key : (String) -> K,
) -> BiMap[K, R] raise BiMapDecodeError {
// Step 1 — whole-object decode (§3.5 error priority): any value decode
// error, or a non-object input, raises Decode (core's error passed
// through) BEFORE any conflict detection. core Map is insertion-ordered
// and resolves duplicate text keys last-wins (§3.2), so `obj` is the
// text-ordered, last-wins pair sequence.
let obj : Map[String, R] = @json.from_json(json) catch {
err => raise Decode(err)
}
// Step 2 — raw object for conflict rendering. Guaranteed `Object` by the
// successful decode above (the abort branch is unreachable).
let raw : Map[String, Json] = match json {
Json::Object(raw) => raw
_ =>
abort(
"BiMap::from_json_with: unreachable - a non-object decoded as a Map",
)
}
// Step 3 — parse_key collapse, last wins (§3.2, P2 ruling: parse_key
// collisions are structurally the same as duplicate text keys). We keep:
// - entries + logicals: text order, for the dedup pass below;
// - final_pairs: logical key -> decoded value; overwriting a logical key
// keeps its FIRST-seen position (core Map semantics) — C2-style position
// stability — so the resulting BiMap order is the order of first
// occurrences of the logical keys (indexmap-aligned);
// - final_text: logical key -> its LAST text key (survivor marker);
// - raw_nodes: per text key, for rendering the incoming pair's right.
let entries = obj.to_array()
let logicals : Array[K] = []
let final_pairs : Map[K, R] = Map([])
let final_text : Map[K, String] = Map([])
let raw_nodes : Array[Json] = []
for i in 0..
raise DuplicateRightValue(
Json::stringify(raw_nodes[i]),
render_key(first_left),
render_key(k),
)
None => seen[v] = k
}
}
}
// Step 5 — build through the public construction path. Keys are unique and
// right values are deduped, so from_array only performs C0 inserts;
// final_pairs iterates in first-seen logical-key order.
let pairs : Array[(K, R)] = []
for k, v in final_pairs {
pairs.push((k, v))
}
BiMap::from_array(pairs)
}
// ---------------------------------------------------------------------------
// BiBTreeMap deserialization
// ---------------------------------------------------------------------------
///|
/// Deserialize a `BiBTreeMap` from a JSON object (String keys; the sorted
/// map's order is self-canonical, so the input key order is irrelevant —
/// SPEC §12.1 item 2). Duplicate text keys are last-wins (§3.2).
///
/// Strict bijection validation, error priority and `Decode` passthrough are
/// identical to `BiMap::from_json`. **Call through the method (P1-1)**:
/// `BiBTreeMap::from_json(...)` — the `@aurasuisui/bimap.from_json` free
/// function covers only `BiMap`.
pub fn[R : FromJson + Compare] BiBTreeMap::from_json(
json : Json,
) -> BiBTreeMap[String, R] raise BiMapDecodeError {
BiBTreeMap::from_json_with(json, identity_parse_key)
}
///|
/// Deserialize a `BiBTreeMap` from a JSON object, mapping each object key
/// through `parse_key : (String) -> K` before inserting. **`parse_key`
/// MUST be total (P1-2)** — see `BiMap::from_json_with`. parse_key
/// collisions are last-wins, like duplicate text keys (§3.2, P2 ruling).
///
/// All conflict semantics are identical to `BiMap::from_json_with` — in
/// particular the dedup pass walks the SURVIVING pairs in text order, so a
/// `DuplicateRightValue` payload names the text-earlier left key as the
/// occupied one. Keys need only `Compare` (no `Hash`); the intermediate
/// dedup structures use `@sorted_map.SortedMap` accordingly.
pub fn[K : Compare, R : FromJson + Compare] BiBTreeMap::from_json_with(
json : Json,
parse_key : (String) -> K,
) -> BiBTreeMap[K, R] raise BiMapDecodeError {
// Steps 1–2 mirror BiMap::from_json_with: whole-object decode first
// (§3.5 error priority, Decode passthrough), then the raw object for
// rendering (guaranteed Object by the successful decode).
let obj : Map[String, R] = @json.from_json(json) catch {
err => raise Decode(err)
}
let raw : Map[String, Json] = match json {
Json::Object(raw) => raw
_ =>
abort(
"BiBTreeMap::from_json_with: unreachable - a non-object decoded as a Map",
)
}
// Step 3 — parse_key collapse, last wins. K has only Compare (no Hash),
// so the intermediate maps are SortedMaps (toolchain ruling 2026-09: do
// NOT add a Hash bound for BiBTreeMap). entries/logicals/raw_nodes keep
// text order for the dedup pass; final_pairs/final_text feed the build.
let entries = obj.to_array()
let logicals : Array[K] = []
let final_pairs : @sorted_map.SortedMap[K, R] = @sorted_map.SortedMap([])
let final_text : @sorted_map.SortedMap[K, String] = @sorted_map.SortedMap([])
let raw_nodes : Array[Json] = []
for i in 0..
raise DuplicateRightValue(
Json::stringify(raw_nodes[i]),
render_key(first_left),
render_key(k),
)
None => seen.set(v, k)
}
}
}
// Step 5 — build through the public construction path (only C0 inserts
// remain). SortedMap yields pairs in ascending logical-key order;
// BiBTreeMap is sorted anyway, so the input order is immaterial.
BiBTreeMap::from_array(final_pairs.to_array())
}