use std::str::FromStr;
use qubit_budget::json::JsonMeasurement;
use qubit_datatype::ConversionLimits;
use qubit_datatype::ConversionPolicy;
use qubit_datatype::DataConversionError;
use qubit_datatype::DataConverter;
use qubit_datatype::DataListConversionError;
use qubit_datatype::DataType;
use qubit_datatype::InvalidValueReason;
use serde_json::Number;
use serde_json::Value as JsonValue;
use crate::MultiValues;
use crate::MultiValuesRef;
use crate::Value;
use crate::ValueContainer;
use crate::ValueError;
use crate::ValueRef;
use crate::ValueResult;
mod internal;
mod projection_budget;
use self::internal::json_children::JsonChildren;
use self::internal::prepared_projection::PreparedProjection;
use self::internal::prepared_scalar::PreparedScalar;
use self::projection_budget::ProjectionBudget;
macro_rules! check_projection_number {
(BigInteger, $value:expr, $budget:expr) => {
$budget
.limits
.numeric()
.big_integer()
.check($value)
.map_err(|error| $budget.error(error))?
};
(BigDecimal, $value:expr, $budget:expr) => {
$budget
.limits
.numeric()
.big_decimal()
.check($value)
.map_err(|error| $budget.error(error))?
};
($variant:ident, $value:expr, $budget:expr) => {};
}
macro_rules! cache_projection {
(String, $class:ident) => {
false
};
($variant:ident, json_bool) => {
false
};
($variant:ident, json_number) => {
false
};
($variant:ident, json_object) => {
false
};
($variant:ident, json_identity) => {
false
};
($variant:ident, $class:ident) => {
true
};
}
macro_rules! prepare_payload {
(String, $class:ident, $value:expr, $view:expr, $from:expr, $budget:expr, $depth:expr) => {{
$budget.input($value)?;
$budget.admit_output_string($value.len(), $depth)?;
Ok(PreparedScalar::Borrowed($view))
}};
($variant:ident, json_bool, $value:expr, $view:expr, $from:expr, $budget:expr, $depth:expr) => {{
let _ = $value;
$budget.admit(JsonMeasurement::Boolean { depth: $depth })?;
Ok(PreparedScalar::Borrowed($view))
}};
($variant:ident, json_number, $value:expr, $view:expr, $from:expr, $budget:expr, $depth:expr) => {{
$budget.display(&$value, $depth, true)?;
Ok(PreparedScalar::Borrowed($view))
}};
($variant:ident, json_float32, $value:expr, $view:expr, $from:expr, $budget:expr, $depth:expr) => {{
let number = Number::from_str(&$value.to_string()).map_err(|_| {
DataConversionError::invalid($from, DataType::Json, InvalidValueReason::NonFinite)
})?;
$budget.display(&number, $depth, true)?;
Ok(PreparedScalar::Number(number))
}};
($variant:ident, json_float64, $value:expr, $view:expr, $from:expr, $budget:expr, $depth:expr) => {{
let number = Number::from_f64($value).ok_or_else(|| {
DataConversionError::invalid($from, DataType::Json, InvalidValueReason::NonFinite)
})?;
$budget.display(&number, $depth, true)?;
Ok(PreparedScalar::Number(number))
}};
($variant:ident, json_string, $value:expr, $view:expr, $from:expr, $budget:expr, $depth:expr) => {
$budget
.format(&$value, $depth)
.map(PreparedScalar::Formatted)
};
($variant:ident, json_duration, $value:expr, $view:expr, $from:expr, $budget:expr, $depth:expr) => {{
let text = DataConverter::from($value)
.to_in::<String>(&mut $budget.conversion)
.map_err(|error| $budget.duration_conversion_error(error))?;
$budget.admit_output_string(text.len(), $depth)?;
Ok(PreparedScalar::Formatted(text))
}};
($variant:ident, json_object, $value:expr, $view:expr, $from:expr, $budget:expr, $depth:expr) => {{
$budget.admit(JsonMeasurement::Object {
depth: $depth,
entries: $value.len(),
})?;
for (key, value) in $value {
$budget.text(key, $depth, true)?;
$budget.text(value, $depth.saturating_add(1), false)?;
}
Ok(PreparedScalar::Borrowed($view))
}};
($variant:ident, json_identity, $value:expr, $view:expr, $from:expr, $budget:expr, $depth:expr) => {{
admit_json($value, $depth, $budget)?;
Ok(PreparedScalar::Borrowed($view))
}};
}
macro_rules! prepare_scalar_match {
($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)*)),+ $(,)?) => {
match $view {
ValueRef::Unset(_) => {
$budget.admit(JsonMeasurement::Null { depth: $depth })?;
Ok(PreparedScalar::Borrowed($view))
}
$($(#[$cfg])* ValueRef::$variant(value) => {
check_projection_number!($variant, value, $budget);
prepare_payload!($variant, $json_class, value, $view, $data_type, $budget, $depth)
},)+
}
};
}
fn prepare_scalar<'a>(
view: ValueRef<'a>,
budget: &mut ProjectionBudget<'_>,
depth: usize,
) -> ValueResult<PreparedScalar<'a>> {
budget.item()?;
for_each_value_type!(prepare_scalar_match, view, budget, depth)
}
macro_rules! collection_cache_match {
($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)*)),+ $(,)?) => {
match $view {
MultiValuesRef::Unset(_) => false,
$($(#[$cfg])* MultiValuesRef::$variant(_) => cache_projection!($variant, $json_class),)+
}
};
}
fn prepare_collection<'a>(
view: MultiValuesRef<'a>,
budget: &mut ProjectionBudget<'_>,
) -> ValueResult<PreparedProjection<'a>> {
if matches!(view, MultiValuesRef::Unset(_)) {
budget.item()?;
budget.admit(JsonMeasurement::Null { depth: 1 })?;
return Ok(PreparedProjection::BorrowedCollection(view));
}
budget.admit(JsonMeasurement::Array {
depth: 1,
items: view.len(),
})?;
let cache = for_each_value_type!(collection_cache_match, view);
let mut prepared = Vec::new();
for index in 0..view.len() {
budget.source_index = Some(index);
let item = view
.get(index)
.expect("index is inside the source collection");
let scalar = prepare_scalar(item, budget, 2).map_err(|error| match error {
ValueError::Conversion(source) => DataListConversionError::new(index, source).into(),
error => error,
})?;
if cache {
prepared.push(scalar);
}
}
Ok(if cache {
PreparedProjection::Collection(prepared)
} else {
PreparedProjection::BorrowedCollection(view)
})
}
fn admit_json(
value: &JsonValue,
depth: usize,
budget: &mut ProjectionBudget<'_>,
) -> ValueResult<()> {
let mut frames = Vec::<JsonChildren<'_>>::new();
let mut next = Some((None, value, depth));
while let Some((key, value, depth)) = next.take() {
if let Some(key) = key {
budget.text(key, depth, true)?;
}
match value {
JsonValue::Null => budget.admit(JsonMeasurement::Null { depth })?,
JsonValue::Bool(_) => budget.admit(JsonMeasurement::Boolean { depth })?,
JsonValue::Number(value) => {
budget.display(value, depth, true)?;
validate_projected_json_number(value)?;
}
JsonValue::String(value) => budget.text(value, depth, false)?,
JsonValue::Array(values) => {
budget.admit(JsonMeasurement::Array {
depth,
items: values.len(),
})?;
frames.push(JsonChildren::Array(values.iter(), depth.saturating_add(1)));
}
JsonValue::Object(values) => {
budget.admit(JsonMeasurement::Object {
depth,
entries: values.len(),
})?;
frames.push(JsonChildren::Object(values.iter(), depth.saturating_add(1)));
}
}
while let Some(frame) = frames.last_mut() {
if let Some(child) = frame.next() {
next = Some(child);
break;
}
frames.pop();
}
}
Ok(())
}
fn validate_projected_json_number(number: &Number) -> ValueResult<()> {
if number.as_i64().is_some() || number.as_u64().is_some() {
return Ok(());
}
let text = number.to_string();
let fractional = text.bytes().any(|byte| matches!(byte, b'.' | b'e' | b'E'));
let reason = if fractional {
if text.parse::<f64>().is_ok_and(f64::is_finite) {
return Ok(());
}
InvalidValueReason::NonFinite
} else {
let fits = if text.starts_with('-') {
text.parse::<i64>().is_ok()
} else {
text.parse::<u64>().is_ok()
};
if fits {
return Ok(());
}
InvalidValueReason::OutOfRange
};
Err(DataConversionError::invalid(DataType::Json, DataType::Json, reason).into())
}
pub(crate) fn value_to_json_value_with(
value: &Value,
policy: &ConversionPolicy,
limits: &ConversionLimits,
) -> ValueResult<JsonValue> {
let mut budget = ProjectionBudget::new(value.data_type(), policy, limits);
Ok(PreparedProjection::Scalar(prepare_scalar(value.view(), &mut budget, 1)?).materialize())
}
pub(crate) fn multi_values_to_json_value_with(
values: &MultiValues,
policy: &ConversionPolicy,
limits: &ConversionLimits,
) -> ValueResult<JsonValue> {
let mut budget = ProjectionBudget::new(values.data_type(), policy, limits);
Ok(prepare_collection(values.view(), &mut budget)?.materialize())
}
#[inline]
pub(crate) fn value_container_to_json_value_with(
container: &ValueContainer,
policy: &ConversionPolicy,
limits: &ConversionLimits,
) -> ValueResult<JsonValue> {
match container {
ValueContainer::Scalar(value) => value_to_json_value_with(value, policy, limits),
ValueContainer::Collection(values) => {
multi_values_to_json_value_with(values, policy, limits)
}
}
}