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 std::str::FromStr;
12
13use qubit_budget::json::JsonMeasurement;
14use qubit_datatype::ConversionLimits;
15use qubit_datatype::ConversionPolicy;
16use qubit_datatype::DataConversionError;
17use qubit_datatype::DataConverter;
18use qubit_datatype::DataListConversionError;
19use qubit_datatype::DataType;
20use qubit_datatype::InvalidValueReason;
21use serde_json::Number;
22use serde_json::Value as JsonValue;
23
24use crate::MultiValues;
25use crate::MultiValuesRef;
26use crate::Value;
27use crate::ValueContainer;
28use crate::ValueError;
29use crate::ValueRef;
30use crate::ValueResult;
31
32mod json_children;
33mod measurement_writer;
34mod prepared_projection;
35mod prepared_scalar;
36mod projection_budget;
37
38use json_children::JsonChildren;
39use prepared_projection::PreparedProjection;
40use prepared_scalar::PreparedScalar;
41use projection_budget::ProjectionBudget;
42
43/// Checks source big-number limits before decimal formatting can allocate.
44macro_rules! check_projection_number {
45    (BigInteger, $value:expr, $budget:expr) => {
46        $budget
47            .limits
48            .numeric()
49            .big_integer()
50            .check($value)
51            .map_err(|error| $budget.error(error))?
52    };
53    (BigDecimal, $value:expr, $budget:expr) => {
54        $budget
55            .limits
56            .numeric()
57            .big_decimal()
58            .check($value)
59            .map_err(|error| $budget.error(error))?
60    };
61    ($variant:ident, $value:expr, $budget:expr) => {};
62}
63
64/// Classifies collections requiring a per-element rendering cache.
65macro_rules! cache_projection {
66    (String, $class:ident) => {
67        false
68    };
69    ($variant:ident, json_bool) => {
70        false
71    };
72    ($variant:ident, json_number) => {
73        false
74    };
75    ($variant:ident, json_object) => {
76        false
77    };
78    ($variant:ident, json_identity) => {
79        false
80    };
81    ($variant:ident, $class:ident) => {
82        true
83    };
84}
85
86/// Prepares one scalar while retaining only genuinely required allocations.
87macro_rules! prepare_payload {
88    (String, $class:ident, $value:expr, $view:expr, $from:expr, $budget:expr, $depth:expr) => {{
89        $budget.input($value)?;
90        $budget.admit_output_string($value.len(), $depth)?;
91        Ok(PreparedScalar::Borrowed($view))
92    }};
93    ($variant:ident, json_bool, $value:expr, $view:expr, $from:expr, $budget:expr, $depth:expr) => {{
94        let _ = $value;
95        $budget.admit(JsonMeasurement::Boolean { depth: $depth })?;
96        Ok(PreparedScalar::Borrowed($view))
97    }};
98    ($variant:ident, json_number, $value:expr, $view:expr, $from:expr, $budget:expr, $depth:expr) => {{
99        $budget.display(&$value, $depth, true)?;
100        Ok(PreparedScalar::Borrowed($view))
101    }};
102    ($variant:ident, json_float32, $value:expr, $view:expr, $from:expr, $budget:expr, $depth:expr) => {{
103        let number = Number::from_str(&$value.to_string())
104            .map_err(|_| DataConversionError::invalid($from, DataType::Json, InvalidValueReason::NonFinite))?;
105        $budget.display(&number, $depth, true)?;
106        Ok(PreparedScalar::Number(number))
107    }};
108    ($variant:ident, json_float64, $value:expr, $view:expr, $from:expr, $budget:expr, $depth:expr) => {{
109        let number = Number::from_f64($value)
110            .ok_or_else(|| DataConversionError::invalid($from, DataType::Json, InvalidValueReason::NonFinite))?;
111        $budget.display(&number, $depth, true)?;
112        Ok(PreparedScalar::Number(number))
113    }};
114    ($variant:ident, json_string, $value:expr, $view:expr, $from:expr, $budget:expr, $depth:expr) => {
115        $budget.format(&$value, $depth).map(PreparedScalar::Formatted)
116    };
117    ($variant:ident, json_duration, $value:expr, $view:expr, $from:expr, $budget:expr, $depth:expr) => {{
118        let text = DataConverter::from($value).to_in::<String>(&mut $budget.conversion)?;
119        $budget.admit_output_string(text.len(), $depth)?;
120        Ok(PreparedScalar::Formatted(text))
121    }};
122    ($variant:ident, json_object, $value:expr, $view:expr, $from:expr, $budget:expr, $depth:expr) => {{
123        $budget.admit(JsonMeasurement::Object {
124            depth: $depth,
125            entries: $value.len(),
126        })?;
127        for (key, value) in $value {
128            $budget.text(key, $depth, true)?;
129            $budget.text(value, $depth.saturating_add(1), false)?;
130        }
131        Ok(PreparedScalar::Borrowed($view))
132    }};
133    ($variant:ident, json_identity, $value:expr, $view:expr, $from:expr, $budget:expr, $depth:expr) => {{
134        admit_json($value, $depth, $budget)?;
135        Ok(PreparedScalar::Borrowed($view))
136    }};
137}
138
139/// Uses the owned type table to prepare borrowed scalar payloads.
140macro_rules! prepare_scalar_match {
141    ($view:expr, $budget:expr, $depth:expr; $(([$($cfg:meta),*], $variant:ident, $type:ty, $data_type:expr, $materialization:ident, $json_class:ident, $number_projection:ident, $value_doc:literal, $multi_doc:literal $(, $_wire:tt)*)),+ $(,)?) => {
142        match $view {
143            ValueRef::Unset(_) => {
144                $budget.admit(JsonMeasurement::Null { depth: $depth })?;
145                Ok(PreparedScalar::Borrowed($view))
146            }
147            $($(#[$cfg])* ValueRef::$variant(value) => {
148                check_projection_number!($variant, value, $budget);
149                prepare_payload!($variant, $json_class, value, $view, $data_type, $budget, $depth)
150            },)+
151        }
152    };
153}
154
155/// Admits one scalar before allocating a cached rich rendering.
156fn prepare_scalar<'a>(
157    view: ValueRef<'a>,
158    budget: &mut ProjectionBudget<'_>,
159    depth: usize,
160) -> ValueResult<PreparedScalar<'a>> {
161    budget.item()?;
162    for_each_value_type!(prepare_scalar_match, view, budget, depth)
163}
164
165/// Determines whether a homogeneous collection needs a cache before iteration.
166macro_rules! collection_cache_match {
167    ($view:expr; $(([$($cfg:meta),*], $variant:ident, $type:ty, $data_type:expr, $materialization:ident, $json_class:ident, $number_projection:ident, $value_doc:literal, $multi_doc:literal $(, $_wire:tt)*)),+ $(,)?) => {
168        match $view {
169            MultiValuesRef::Unset(_) => false,
170            $($(#[$cfg])* MultiValuesRef::$variant(_) => cache_projection!($variant, $json_class),)+
171        }
172    };
173}
174
175/// Admits the full collection before materializing any final JSON output.
176fn prepare_collection<'a>(
177    view: MultiValuesRef<'a>,
178    budget: &mut ProjectionBudget<'_>,
179) -> ValueResult<PreparedProjection<'a>> {
180    if matches!(view, MultiValuesRef::Unset(_)) {
181        budget.item()?;
182        budget.admit(JsonMeasurement::Null { depth: 1 })?;
183        return Ok(PreparedProjection::BorrowedCollection(view));
184    }
185    budget.admit(JsonMeasurement::Array {
186        depth: 1,
187        items: view.len(),
188    })?;
189    let cache = for_each_value_type!(collection_cache_match, view);
190    let mut prepared = Vec::new();
191    for index in 0..view.len() {
192        budget.source_index = Some(index);
193        let item = view.get(index).expect("index is inside the source collection");
194        let scalar = prepare_scalar(item, budget, 2).map_err(|error| match error {
195            ValueError::Conversion(source) => DataListConversionError::new(index, source).into(),
196            error => error,
197        })?;
198        if cache {
199            prepared.push(scalar);
200        }
201    }
202    Ok(if cache {
203        PreparedProjection::Collection(prepared)
204    } else {
205        PreparedProjection::BorrowedCollection(view)
206    })
207}
208
209/// Traverses nested JSON iteratively, charging keys and leaf text before
210/// cloning.
211fn admit_json(value: &JsonValue, depth: usize, budget: &mut ProjectionBudget<'_>) -> ValueResult<()> {
212    let mut frames = Vec::<JsonChildren<'_>>::new();
213    let mut next = Some((None, value, depth));
214    while let Some((key, value, depth)) = next.take() {
215        if let Some(key) = key {
216            budget.text(key, depth, true)?;
217        }
218        match value {
219            JsonValue::Null => budget.admit(JsonMeasurement::Null { depth })?,
220            JsonValue::Bool(_) => budget.admit(JsonMeasurement::Boolean { depth })?,
221            JsonValue::Number(value) => budget.display(value, depth, true)?,
222            JsonValue::String(value) => budget.text(value, depth, false)?,
223            JsonValue::Array(values) => {
224                budget.admit(JsonMeasurement::Array {
225                    depth,
226                    items: values.len(),
227                })?;
228                frames.push(JsonChildren::Array(values.iter(), depth.saturating_add(1)));
229            }
230            JsonValue::Object(values) => {
231                budget.admit(JsonMeasurement::Object {
232                    depth,
233                    entries: values.len(),
234                })?;
235                frames.push(JsonChildren::Object(values.iter(), depth.saturating_add(1)));
236            }
237        }
238        while let Some(frame) = frames.last_mut() {
239            if let Some(child) = frame.next() {
240                next = Some(child);
241                break;
242            }
243            frames.pop();
244        }
245    }
246    Ok(())
247}
248
249/// Projects a scalar after complete bounded preparation.
250///
251/// Returns conversion or resource errors before final JSON allocation.
252pub(crate) fn value_to_json_value_with(
253    value: &Value,
254    policy: &ConversionPolicy,
255    limits: &ConversionLimits,
256) -> ValueResult<JsonValue> {
257    let mut budget = ProjectionBudget::new(value.data_type(), policy, limits);
258    Ok(PreparedProjection::Scalar(prepare_scalar(value.view(), &mut budget, 1)?).materialize())
259}
260
261/// Projects a collection after complete bounded preparation, preserving its
262/// shape.
263pub(crate) fn multi_values_to_json_value_with(
264    values: &MultiValues,
265    policy: &ConversionPolicy,
266    limits: &ConversionLimits,
267) -> ValueResult<JsonValue> {
268    let mut budget = ProjectionBudget::new(values.data_type(), policy, limits);
269    Ok(prepare_collection(values.view(), &mut budget)?.materialize())
270}
271
272/// Projects scalar or collection storage without conflating cardinalities.
273pub(crate) fn value_container_to_json_value_with(
274    container: &ValueContainer,
275    policy: &ConversionPolicy,
276    limits: &ConversionLimits,
277) -> ValueResult<JsonValue> {
278    match container {
279        ValueContainer::Scalar(value) => value_to_json_value_with(value, policy, limits),
280        ValueContainer::Collection(values) => multi_values_to_json_value_with(values, policy, limits),
281    }
282}