Skip to main content

velesdb_memory/context/
wire.rs

1//! JSON-tree helpers for the id wire contract shared by every JS-facing
2//! binding (Node, WASM) of the `context` types: a `u64` id crosses as a
3//! decimal string, because JS `number` loses precision above 2^53.
4//!
5//! Node and WASM independently need the exact same tree walk over a
6//! serialized [`CompiledContext`](super::CompiledContext) — one to turn
7//! outgoing ids into strings, the other to turn incoming strings back into
8//! numbers before deserializing. Living here once (instead of copy-pasted
9//! per binding) means [`ID_KEYS`] has a single source of truth: a future id
10//! field added to a `context` type only needs updating in one place, not
11//! silently missed in whichever binding a copy-paste forgot.
12//!
13//! Deliberately `String`-erred, not binding-specific: this crate depends on
14//! neither `napi` nor `wasm-bindgen`, so each binding maps the `String`
15//! error to its own error type at the call site.
16
17use serde_json::Value;
18
19/// Object keys whose `u64` values (or arrays of them) must cross to JS as
20/// decimal strings. Token counts stay numbers: they are bounded far below
21/// 2^53 by the budget caps.
22pub const ID_KEYS: &[&str] = &["fragment_id", "content_hash", "memory_id", "fragment_ids"];
23
24/// Recursively rewrite every [`ID_KEYS`] field of a serialized `context`
25/// wire value into its decimal-string form.
26pub fn stringify_id_fields(value: &mut Value) {
27    match value {
28        Value::Object(map) => {
29            for (key, entry) in map.iter_mut() {
30                if ID_KEYS.contains(&key.as_str()) {
31                    stringify_ids_in(entry);
32                } else {
33                    stringify_id_fields(entry);
34                }
35            }
36        }
37        Value::Array(items) => items.iter_mut().for_each(stringify_id_fields),
38        _ => {}
39    }
40}
41
42/// Rewrite one id value (or an array of them) into decimal strings.
43fn stringify_ids_in(value: &mut Value) {
44    match value {
45        Value::Number(number) => {
46            if let Some(id) = number.as_u64() {
47                *value = Value::String(id.to_string());
48            }
49        }
50        Value::Array(items) => items.iter_mut().for_each(stringify_ids_in),
51        _ => {}
52    }
53}
54
55/// The inverse of [`stringify_id_fields`]: recursively rewrite every
56/// [`ID_KEYS`] field given in decimal-string form back into the numeric form
57/// the domain types deserialize. Non-string id values pass through
58/// untouched (serde reports them with its own error).
59///
60/// Deliberately stricter than serde: an [`ID_KEYS`]-named field with a
61/// non-numeric string is rejected here even where serde would have dropped
62/// it as an unknown field — a rejected typo beats a silently ignored one,
63/// and an id key can never be user data on these wire shapes.
64///
65/// # Errors
66/// Returns the offending text if an [`ID_KEYS`] field holds a string that
67/// does not parse as a decimal `u64`.
68pub fn parse_id_fields(value: &mut Value) -> Result<(), String> {
69    match value {
70        Value::Object(map) => {
71            for (key, entry) in map.iter_mut() {
72                if ID_KEYS.contains(&key.as_str()) {
73                    parse_ids_in(entry)?;
74                } else {
75                    parse_id_fields(entry)?;
76                }
77            }
78        }
79        Value::Array(items) => {
80            for item in items {
81                parse_id_fields(item)?;
82            }
83        }
84        _ => {}
85    }
86    Ok(())
87}
88
89/// Rewrite one id value (or an array of them) from decimal string to number.
90fn parse_ids_in(value: &mut Value) -> Result<(), String> {
91    match value {
92        Value::String(text) => {
93            *value = Value::Number(parse_u64(text)?.into());
94        }
95        Value::Array(items) => {
96            for item in items {
97                parse_ids_in(item)?;
98            }
99        }
100        _ => {}
101    }
102    Ok(())
103}
104
105/// Accept `fragments[].id` in decimal-string form by rewriting it to the
106/// numeric wire form. The other [`ID_KEYS`] never appear in a compile
107/// *request*, only in the output — a blanket rule over every `id` key would
108/// corrupt caller metadata that happens to use that name.
109///
110/// # Errors
111/// Returns the offending text if a fragment's `id` does not parse as a
112/// decimal `u64`.
113pub fn parse_fragment_id_strings(request: &mut Value) -> Result<(), String> {
114    let Some(fragments) = request.get_mut("fragments").and_then(Value::as_array_mut) else {
115        return Ok(());
116    };
117    for fragment in fragments {
118        let Some(id) = fragment.get_mut("id") else {
119            continue;
120        };
121        if let Value::String(text) = id {
122            *id = Value::Number(parse_u64(text)?.into());
123        }
124    }
125    Ok(())
126}
127
128pub(crate) fn parse_u64(text: &str) -> Result<u64, String> {
129    text.parse()
130        .map_err(|_| format!("invalid id '{text}' (expected a decimal u64 string)"))
131}
132
133/// Serde `deserialize_with` for [`super::model::ContextFragment::id`]:
134/// accepts either a JSON number or a decimal string, so a caller who
135/// received an id as a string (e.g. echoing back a `fragment_id` handed out
136/// under [`super::model::CompilePolicy::ids_as_strings`], or any client that
137/// serializes `u64` as a JS-safe string by convention) can resubmit it
138/// as-is. Reuses [`parse_u64`] — the same decimal-parsing rule
139/// [`parse_fragment_id_strings`] and [`parse_ids_in`] already apply,
140/// centralized once. Matched by hand over a [`Value`] (not an untagged
141/// enum) so a rejected value gets a message naming it and the accepted
142/// forms, instead of serde's opaque "did not match any variant".
143///
144/// # Errors
145/// Returns a deserialize error naming the offending value if it is neither
146/// a `u64` JSON number nor a decimal-`u64` string.
147pub(crate) fn deserialize_optional_id<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
148where
149    D: serde::Deserializer<'de>,
150{
151    use serde::de::Error;
152    use serde::Deserialize;
153
154    let expected = "expected a u64 number or a decimal u64 string";
155    Option::<Value>::deserialize(deserializer)?
156        .map(|value| match value {
157            Value::Number(number) => number
158                .as_u64()
159                .ok_or_else(|| Error::custom(format!("invalid id {number} ({expected})"))),
160            Value::String(text) => parse_u64(&text).map_err(Error::custom),
161            other => Err(Error::custom(format!("invalid id {other} ({expected})"))),
162        })
163        .transpose()
164}
165
166#[cfg(test)]
167#[path = "wire_tests.rs"]
168mod tests;