Skip to main content

incurs_codemode/
codec.rs

1use base64::Engine;
2use serde_json::{Map, Value, json};
3
4const TAG: &str = "__codemode_type";
5
6/// A value that preserves JavaScript-only primitives across JSON and RPC.
7#[derive(Debug, Clone, PartialEq)]
8pub enum CodeValue {
9    /// JavaScript `undefined`.
10    Undefined,
11    /// A regular JSON value.
12    Json(Value),
13    /// An arbitrary-precision integer represented in base 10.
14    BigInt(String),
15    /// Raw bytes.
16    Binary(Vec<u8>),
17}
18
19/// Encodes a Code Mode value into a JSON-safe tagged representation.
20pub fn encode_value(value: &CodeValue) -> Value {
21    match value {
22        CodeValue::Undefined => json!({ TAG: "undefined" }),
23        CodeValue::Json(value) => encode_json(value),
24        CodeValue::BigInt(value) => json!({ TAG: "bigint", "value": value }),
25        CodeValue::Binary(value) => json!({
26            TAG: "binary",
27            "value": base64::engine::general_purpose::STANDARD.encode(value),
28        }),
29    }
30}
31
32/// Decodes a JSON-safe tagged representation into a Code Mode value.
33pub fn decode_value(value: Value) -> Result<CodeValue, String> {
34    if let Value::Object(map) = &value
35        && let Some(Value::String(kind)) = map.get(TAG)
36    {
37        return match kind.as_str() {
38            "undefined" => Ok(CodeValue::Undefined),
39            "bigint" => string_value(map).map(CodeValue::BigInt),
40            "binary" => base64::engine::general_purpose::STANDARD
41                .decode(string_value(map)?)
42                .map(CodeValue::Binary)
43                .map_err(|error| error.to_string()),
44            _ => Ok(CodeValue::Json(decode_json(value)?)),
45        };
46    }
47    Ok(CodeValue::Json(decode_json(value)?))
48}
49
50fn string_value(map: &Map<String, Value>) -> Result<String, String> {
51    map.get("value")
52        .and_then(Value::as_str)
53        .map(str::to_string)
54        .ok_or_else(|| "Tagged Code Mode value is missing a string value".to_string())
55}
56
57fn encode_json(value: &Value) -> Value {
58    match value {
59        Value::Array(values) => Value::Array(values.iter().map(encode_json).collect()),
60        Value::Object(values) => Value::Object(
61            values
62                .iter()
63                .map(|(key, value)| (key.clone(), encode_json(value)))
64                .collect(),
65        ),
66        value => value.clone(),
67    }
68}
69
70fn decode_json(value: Value) -> Result<Value, String> {
71    match value {
72        Value::Array(values) => values
73            .into_iter()
74            .map(decode_json)
75            .collect::<Result<Vec<_>, _>>()
76            .map(Value::Array),
77        Value::Object(values) => values
78            .into_iter()
79            .map(|(key, value)| decode_json(value).map(|value| (key, value)))
80            .collect::<Result<Map<_, _>, _>>()
81            .map(Value::Object),
82        value => Ok(value),
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn round_trips_extended_values() {
92        for value in [
93            CodeValue::Undefined,
94            CodeValue::BigInt("9007199254740993".to_string()),
95            CodeValue::Binary(vec![0, 1, 255]),
96            CodeValue::Json(json!({"ok": [true, null]})),
97        ] {
98            assert_eq!(decode_value(encode_value(&value)).unwrap(), value);
99        }
100    }
101}