fleetforge_common/
prost_json.rs

1use std::collections::BTreeMap;
2
3use anyhow::{anyhow, Result};
4use prost_types::{value::Kind, ListValue, Struct, Value};
5use serde_json::{Map, Number};
6
7/// Converts a Protobuf `Struct` into a `serde_json::Value`.
8pub fn prost_struct_to_json(structure: &Struct) -> serde_json::Value {
9    let mut map = Map::new();
10    for (key, value) in &structure.fields {
11        map.insert(key.clone(), prost_value_to_json(value));
12    }
13    serde_json::Value::Object(map)
14}
15
16/// Converts a Protobuf `Value` into a `serde_json::Value`.
17pub fn prost_value_to_json(value: &Value) -> serde_json::Value {
18    match value.kind.as_ref() {
19        Some(Kind::NullValue(_)) | None => serde_json::Value::Null,
20        Some(Kind::NumberValue(v)) => Number::from_f64(*v)
21            .map(serde_json::Value::Number)
22            .unwrap_or(serde_json::Value::Null),
23        Some(Kind::StringValue(v)) => serde_json::Value::String(v.clone()),
24        Some(Kind::BoolValue(v)) => serde_json::Value::Bool(*v),
25        Some(Kind::StructValue(struct_value)) => prost_struct_to_json(struct_value),
26        Some(Kind::ListValue(list)) => serde_json::Value::Array(
27            list.values
28                .iter()
29                .map(prost_value_to_json)
30                .collect::<Vec<_>>(),
31        ),
32    }
33}
34
35/// Converts a JSON value into a Protobuf `Struct`.
36pub fn json_to_prost_struct(value: &serde_json::Value) -> Result<Struct> {
37    let obj = value
38        .as_object()
39        .ok_or_else(|| anyhow!("inputs must be a JSON object"))?;
40
41    let mut fields = BTreeMap::new();
42    for (key, val) in obj {
43        fields.insert(key.clone(), json_to_prost_value(val)?);
44    }
45
46    Ok(Struct { fields })
47}
48
49/// Converts a JSON value into a Protobuf `Value`.
50pub fn json_to_prost_value(value: &serde_json::Value) -> Result<Value> {
51    let kind = match value {
52        serde_json::Value::Null => Kind::NullValue(0),
53        serde_json::Value::Bool(v) => Kind::BoolValue(*v),
54        serde_json::Value::Number(num) => Kind::NumberValue(
55            num.as_f64()
56                .ok_or_else(|| anyhow!("numeric values must fit into f64 for protobuf Value"))?,
57        ),
58        serde_json::Value::String(v) => Kind::StringValue(v.clone()),
59        serde_json::Value::Array(values) => {
60            let converted = values
61                .iter()
62                .map(json_to_prost_value)
63                .collect::<Result<Vec<_>>>()?;
64            Kind::ListValue(ListValue { values: converted })
65        }
66        serde_json::Value::Object(map) => {
67            let mut fields = BTreeMap::new();
68            for (key, val) in map {
69                fields.insert(key.clone(), json_to_prost_value(val)?);
70            }
71            Kind::StructValue(Struct { fields })
72        }
73    };
74
75    Ok(Value { kind: Some(kind) })
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn round_trips_struct() {
84        let json = serde_json::json!({
85            "foo": true,
86            "bar": [1, "two"]
87        });
88        let struct_value = json_to_prost_struct(&json).expect("convert to struct");
89        let json_round = prost_struct_to_json(&struct_value);
90        assert_eq!(json, json_round);
91    }
92
93    #[test]
94    fn round_trips_nested_and_edge_numbers() {
95        let json = serde_json::json!({
96            "empty": {},
97            "list": [
98                {"value": 1},
99                {"value": 2.5},
100                {"value": -12345678901234567890i128.to_string()},
101                ["nested", {"deep": [null, true, 3.141592653589793]}]
102            ],
103            "scalars": {
104                "int": 2147483647,
105                "float": 1.0 / 3.0,
106                "large_float": 1.0e308,
107                "zero": 0,
108                "string": "hello"
109            }
110        });
111
112        let struct_value = json_to_prost_struct(&json).expect("convert to struct");
113        let json_round = prost_struct_to_json(&struct_value);
114
115        // Floats transit through protobuf Value (f64). Assert approximate equality for the values we know may lose precision,
116        // while exact comparison holds for the full JSON tree structure after parsing.
117        assert_eq!(json_round.get("empty").unwrap(), &serde_json::json!({}));
118        assert_eq!(json_round.get("list").unwrap().as_array().unwrap().len(), 4);
119        assert_eq!(json_round["list"][3][0], serde_json::json!("nested"));
120        assert!(
121            (json_round["scalars"]["float"].as_f64().unwrap() - (1.0 / 3.0)).abs() < f64::EPSILON
122        );
123        assert_eq!(
124            json_round["scalars"]["large_float"].as_f64().unwrap(),
125            1.0e308
126        );
127        assert_eq!(json_round["scalars"]["string"], serde_json::json!("hello"));
128    }
129}