Skip to main content

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}