qubit_value/json.rs
1// =============================================================================
2// Copyright (c) 2025 - 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8
9//! Natural JSON projection for value containers.
10
11use qubit_datatype::{
12 DataConversionError,
13 DataConversionOptions,
14 DataConverter,
15 DataListConversionError,
16 DataType,
17 InvalidValueReason,
18};
19use serde_json::{
20 Number,
21 Value as JsonValue,
22};
23use std::str::FromStr;
24
25use crate::multi_values::MultiValuesRepr;
26use crate::value::ValueRepr;
27use crate::{
28 MultiValues,
29 Value,
30 ValueContainer,
31 ValueError,
32 ValueResult,
33};
34
35/// Converts a finite float to a JSON number.
36///
37/// # Parameters
38///
39/// * `value` - Finite floating-point value to convert.
40/// * `from` - Runtime type of `value` for conversion diagnostics.
41///
42/// # Returns
43///
44/// The corresponding JSON number.
45///
46/// # Errors
47///
48/// Returns [`DataConversionError`] when `value` is NaN or infinite.
49fn finite_float64(
50 value: f64,
51 from: DataType,
52) -> Result<JsonValue, DataConversionError> {
53 Number::from_f64(value).map(JsonValue::Number).ok_or(
54 DataConversionError::invalid(
55 from,
56 DataType::Json,
57 InvalidValueReason::NonFinite,
58 ),
59 )
60}
61
62fn finite_float32(
63 value: f32,
64 from: DataType,
65) -> Result<JsonValue, DataConversionError> {
66 // Use f32 display output as input here to keep float32 textual precision
67 // stable. Converting through `f64` first can emit a longer/altered decimal
68 // representation, which changes natural JSON bytes for the same `f32`
69 // value.
70 Number::from_str(&value.to_string())
71 .map(JsonValue::Number)
72 .map_err(|_| {
73 DataConversionError::invalid(
74 from,
75 DataType::Json,
76 InvalidValueReason::NonFinite,
77 )
78 })
79}
80
81macro_rules! scalar_to_json {
82 (json_bool, $value:expr, $from:expr, $options:expr) => {
83 Ok(JsonValue::Bool(*$value))
84 };
85 (json_number, $value:expr, $from:expr, $options:expr) => {
86 Ok(JsonValue::from(*$value))
87 };
88 (json_float32, $value:expr, $from:expr, $options:expr) => {
89 finite_float32(*$value, $from)
90 };
91 (json_float64, $value:expr, $from:expr, $options:expr) => {
92 finite_float64(*$value as f64, $from)
93 };
94 (json_string, $value:expr, $from:expr, $options:expr) => {
95 Ok(JsonValue::String($value.to_string()))
96 };
97 (json_duration, $value:expr, $from:expr, $options:expr) => {
98 DataConverter::from(*$value)
99 .to_with::<String>($options)
100 .map(JsonValue::String)
101 };
102 (json_object, $value:expr, $from:expr, $options:expr) => {{
103 let mut entries: Vec<_> = $value.iter().collect();
104 entries.sort_unstable_by(|(left, _), (right, _)| left.cmp(right));
105 let mut object = serde_json::Map::with_capacity(entries.len());
106 for (key, value) in entries {
107 object.insert(key.clone(), JsonValue::String(value.clone()));
108 }
109 Ok(JsonValue::Object(object))
110 }};
111 (json_identity, $value:expr, $from:expr, $options:expr) => {
112 Ok(crate::wire::json::canonicalize_json_value($value))
113 };
114}
115
116macro_rules! value_to_json_match {
117 ($value:expr, $options:expr; $(([$($cfg:meta),*], $variant:ident, $type:ty, $data_type:expr, $materialization:ident, $json_class:ident, $number_projection:ident, $value_doc:literal, $multi_doc:literal)),+ $(,)?) => {{
118 let result: Result<JsonValue, DataConversionError> = match &$value.repr {
119 ValueRepr::Unset(_) => Ok(JsonValue::Null),
120 $($(#[$cfg])* ValueRepr::$variant(value) => {
121 scalar_to_json!($json_class, value, $data_type, $options)
122 },)+
123 };
124 result.map_err(ValueError::from)
125 }};
126}
127
128/// Projects a concrete vector according to the natural JSON cardinality rule.
129///
130/// # Type Parameters
131///
132/// * `T` - Runtime element type.
133/// * `F` - Projection from an element to a JSON value.
134///
135/// # Parameters
136///
137/// * `values` - Concrete values to project.
138/// * `project` - Element projection that can report conversion failures.
139///
140/// # Returns
141///
142/// A JSON array containing the projected values in their original order.
143///
144/// # Errors
145///
146/// Returns [`ValueError`] with a [`DataListConversionError`] identifying the
147/// first source index whose projection fails.
148fn collection_to_json<T, F>(
149 values: &[T],
150 mut project: F,
151) -> ValueResult<JsonValue>
152where
153 F: FnMut(&T) -> Result<JsonValue, DataConversionError>,
154{
155 let mut projected = Vec::with_capacity(values.len());
156 for (source_index, value) in values.iter().enumerate() {
157 match project(value) {
158 Ok(value) => projected.push(value),
159 Err(source) => {
160 return Err(
161 DataListConversionError::new(source_index, source).into()
162 );
163 }
164 }
165 }
166
167 Ok(JsonValue::Array(projected))
168}
169
170macro_rules! multi_values_to_json_match {
171 ($value:expr, $options:expr; $(([$($cfg:meta),*], $variant:ident, $type:ty, $data_type:expr, $materialization:ident, $json_class:ident, $number_projection:ident, $value_doc:literal, $multi_doc:literal)),+ $(,)?) => {
172 match &$value.repr {
173 MultiValuesRepr::Unset(_) => Ok(JsonValue::Null),
174 $($(#[$cfg])* MultiValuesRepr::$variant(values) => {
175 collection_to_json(values, |value| {
176 scalar_to_json!($json_class, value, $data_type, $options)
177 })
178 },)+
179 }
180 };
181}
182
183impl Value {
184 /// Projects this typed value to its natural JSON representation.
185 ///
186 /// This differs from the tagged [`crate::ValueWireV1`] representation: for
187 /// example,
188 /// `Value::Int32(42)` projects to the JSON number `42`.
189 ///
190 /// # Returns
191 ///
192 /// The natural JSON representation of this value.
193 ///
194 /// # Errors
195 ///
196 /// Returns a structured conversion error for values JSON cannot represent,
197 /// including non-finite floating-point values and inexact durations.
198 #[inline(always)]
199 pub fn to_json_value(&self) -> ValueResult<JsonValue> {
200 self.to_json_value_with(DataConversionOptions::default_ref())
201 }
202
203 /// Projects this typed value using explicit conversion options.
204 ///
205 /// # Parameters
206 ///
207 /// * `options` - Controls duration units and precision-loss behavior.
208 ///
209 /// # Returns
210 ///
211 /// The natural JSON representation of this value.
212 ///
213 /// # Errors
214 ///
215 /// Returns a structured conversion error when JSON projection or duration
216 /// formatting violates the requested options.
217 pub fn to_json_value_with(
218 &self,
219 options: &DataConversionOptions,
220 ) -> ValueResult<JsonValue> {
221 for_each_value_type!(value_to_json_match, self, options)
222 }
223}
224
225impl MultiValues {
226 /// Projects this collection to its natural JSON representation.
227 ///
228 /// Unset is `null`; every concrete collection is an array, including empty
229 /// and one-item collections.
230 ///
231 /// # Returns
232 ///
233 /// The natural JSON representation of this collection.
234 ///
235 /// # Errors
236 ///
237 /// Returns a list conversion error containing the zero-based source index
238 /// when an item cannot be represented as JSON.
239 #[inline(always)]
240 pub fn to_json_value(&self) -> ValueResult<JsonValue> {
241 self.to_json_value_with(DataConversionOptions::default_ref())
242 }
243
244 /// Projects this collection using explicit conversion options.
245 ///
246 /// # Parameters
247 ///
248 /// * `options` - Controls duration units and precision-loss behavior.
249 ///
250 /// # Returns
251 ///
252 /// The natural JSON representation of this collection.
253 ///
254 /// # Errors
255 ///
256 /// Returns an indexed list conversion error when an item cannot be
257 /// represented under the requested options.
258 pub fn to_json_value_with(
259 &self,
260 options: &DataConversionOptions,
261 ) -> ValueResult<JsonValue> {
262 for_each_value_type!(multi_values_to_json_match, self, options)
263 }
264}
265
266impl ValueContainer {
267 /// Projects this container while preserving concrete collection shape.
268 ///
269 /// Scalar storage uses the natural scalar projection; concrete collection
270 /// storage always uses a JSON array.
271 ///
272 /// # Returns
273 ///
274 /// The natural JSON representation, except scalar and collection unset
275 /// values both project to `null`.
276 ///
277 /// # Errors
278 ///
279 /// Returns the same structured projection error as the contained value.
280 #[inline(always)]
281 pub fn to_json_value(&self) -> ValueResult<JsonValue> {
282 self.to_json_value_with(DataConversionOptions::default_ref())
283 }
284
285 /// Projects this container using explicit conversion options.
286 ///
287 /// # Parameters
288 ///
289 /// * `options` - Controls duration units and precision-loss behavior.
290 ///
291 /// # Returns
292 ///
293 /// The natural JSON representation, except scalar and collection unset
294 /// values both project to `null`.
295 ///
296 /// # Errors
297 ///
298 /// Returns the same structured projection error as the contained value.
299 #[inline(always)]
300 pub fn to_json_value_with(
301 &self,
302 options: &DataConversionOptions,
303 ) -> ValueResult<JsonValue> {
304 match self {
305 Self::Scalar(value) => value.to_json_value_with(options),
306 Self::Collection(values) => values.to_json_value_with(options),
307 }
308 }
309}