|
1 | 1 | // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
2 | 2 | // SPDX-License-Identifier: Apache-2.0 |
3 | 3 |
|
4 | | -//! Helpers for decoding `google.protobuf.Struct` values. |
| 4 | +//! Helpers for converting `google.protobuf.Struct` values to and from JSON. |
5 | 5 |
|
6 | 6 | use serde::{Deserialize, Deserializer, de::Error as _}; |
7 | 7 |
|
| 8 | +/// Errors converting JSON values into protobuf well-known types. |
| 9 | +#[derive(Debug, thiserror::Error)] |
| 10 | +pub enum ProtoStructError { |
| 11 | + /// A JSON number cannot be represented by protobuf's double value. |
| 12 | + #[error("JSON number {0} cannot be represented as a protobuf double")] |
| 13 | + UnrepresentableNumber(serde_json::Number), |
| 14 | +} |
| 15 | + |
| 16 | +/// Convert a JSON object into a protobuf Struct. |
| 17 | +pub fn json_object_to_struct( |
| 18 | + object: serde_json::Map<String, serde_json::Value>, |
| 19 | +) -> Result<prost_types::Struct, ProtoStructError> { |
| 20 | + Ok(prost_types::Struct { |
| 21 | + fields: object |
| 22 | + .into_iter() |
| 23 | + .map(|(key, value)| json_value_to_proto(value).map(|value| (key, value))) |
| 24 | + .collect::<Result<_, _>>()?, |
| 25 | + }) |
| 26 | +} |
| 27 | + |
| 28 | +/// Convert a JSON value into a protobuf Value. |
| 29 | +pub fn json_value_to_proto( |
| 30 | + value: serde_json::Value, |
| 31 | +) -> Result<prost_types::Value, ProtoStructError> { |
| 32 | + use prost_types::{ListValue, Value, value::Kind}; |
| 33 | + |
| 34 | + let kind = match value { |
| 35 | + serde_json::Value::Null => Kind::NullValue(0), |
| 36 | + serde_json::Value::Bool(value) => Kind::BoolValue(value), |
| 37 | + serde_json::Value::Number(value) => Kind::NumberValue( |
| 38 | + value |
| 39 | + .as_f64() |
| 40 | + .ok_or_else(|| ProtoStructError::UnrepresentableNumber(value.clone()))?, |
| 41 | + ), |
| 42 | + serde_json::Value::String(value) => Kind::StringValue(value), |
| 43 | + serde_json::Value::Array(values) => Kind::ListValue(ListValue { |
| 44 | + values: values |
| 45 | + .into_iter() |
| 46 | + .map(json_value_to_proto) |
| 47 | + .collect::<Result<_, _>>()?, |
| 48 | + }), |
| 49 | + serde_json::Value::Object(object) => Kind::StructValue(json_object_to_struct(object)?), |
| 50 | + }; |
| 51 | + |
| 52 | + Ok(Value { kind: Some(kind) }) |
| 53 | +} |
| 54 | + |
8 | 55 | /// Convert a protobuf Struct into a JSON object for typed serde decoding. |
9 | 56 | #[must_use] |
10 | 57 | pub fn struct_to_json_object( |
@@ -72,6 +119,24 @@ where |
72 | 119 | mod tests { |
73 | 120 | use super::*; |
74 | 121 |
|
| 122 | + #[test] |
| 123 | + fn json_and_proto_values_round_trip() { |
| 124 | + let json = serde_json::json!({ |
| 125 | + "null": null, |
| 126 | + "bool": true, |
| 127 | + "number": 42.5, |
| 128 | + "string": "value", |
| 129 | + "list": [1.0, {"nested": "value"}], |
| 130 | + }); |
| 131 | + let serde_json::Value::Object(object) = json.clone() else { |
| 132 | + unreachable!(); |
| 133 | + }; |
| 134 | + |
| 135 | + let proto = json_object_to_struct(object).unwrap(); |
| 136 | + |
| 137 | + assert_eq!(struct_to_json_value(&proto), json); |
| 138 | + } |
| 139 | + |
75 | 140 | #[derive(Debug, Default, Deserialize)] |
76 | 141 | #[serde(default)] |
77 | 142 | struct TestConfig { |
|
0 commit comments