use std::error::Error;
use std::fmt::Display;
use std::fmt::Write;
use qubit_budget::BudgetError;
use qubit_budget::BudgetedStringError;
use qubit_budget::MeasuredBudgetError;
use qubit_budget::QuantityConversionError;
use qubit_budget::ResourceBudget;
use qubit_budget::ResourceQuantity;
use qubit_budget::json::JsonMeasurement;
use qubit_budget::json::JsonValueBudget;
use qubit_budget::json::JsonValueLimits;
use qubit_datatype::ConversionLimits;
use qubit_datatype::ConversionPolicy;
use qubit_datatype::ConversionResource;
use qubit_datatype::ConversionSession;
use qubit_datatype::DataConversionError;
use qubit_datatype::DataConversionErrorKind;
use qubit_datatype::DataType;
use qubit_datatype::InvalidValueReason;
use crate::ValueError;
use crate::ValueResult;
use crate::json::internal::MeasurementWriter;
pub(in crate::json) struct ProjectionBudget<'a> {
pub(in crate::json) limits: &'a ConversionLimits,
pub(in crate::json) conversion: ConversionSession<'a>,
pub(in crate::json) data_type: DataType,
pub(in crate::json) source_index: Option<usize>,
items: ResourceBudget<ConversionResource, u64>,
input: ResourceBudget<ConversionResource, u64>,
output: ResourceBudget<ConversionResource, u64>,
structure: JsonValueBudget<ConversionResource, u64>,
}
impl<'a> ProjectionBudget<'a> {
#[must_use]
pub(in crate::json) fn new(
data_type: DataType,
policy: &'a ConversionPolicy,
limits: &'a ConversionLimits,
) -> Self {
let operation = limits.operation();
let structure_limits = limits
.structured()
.value()
.structure_limits()
.to_builder()
.nodes_limit(*operation.structured_nodes_limit())
.build();
let structure = JsonValueBudget::new(
JsonValueLimits::builder()
.structure_limits(structure_limits)
.payload_bytes_limit(*operation.structured_payload_bytes_limit())
.build(),
);
Self {
limits,
conversion: ConversionSession::new(policy, limits),
data_type,
source_index: None,
items: ResourceBudget::from_limit(*operation.items_limit()),
input: ResourceBudget::from_limit(*operation.input_bytes_limit()),
output: ResourceBudget::from_limit(*operation.output_bytes_limit()),
structure,
}
}
#[inline]
pub(in crate::json) fn error(&self, source: MeasuredBudgetError<ConversionResource, u64>) -> ValueError {
ValueError::JsonProjectionLimit {
data_type: self.data_type,
source_index: self.source_index,
source,
}
}
#[inline]
pub(in crate::json) fn item(&mut self) -> ValueResult<()> {
self.items.try_consume(1).map_err(|error| self.error(error.into()))
}
pub(in crate::json) fn admit(&mut self, measurement: JsonMeasurement) -> ValueResult<()> {
let result = {
let mut transaction = self.structure.transaction();
transaction.try_admit(measurement).and_then(|()| transaction.commit())
};
result.map_err(|error| self.error(error))
}
#[inline]
pub(in crate::json) fn input(&mut self, text: &str) -> ValueResult<()> {
self.input
.try_consume_usize(text.len())
.map_err(|error| self.error(error))
}
pub(in crate::json) fn text(&mut self, text: &str, depth: usize, key: bool) -> ValueResult<()> {
self.input(text)?;
if !key {
let amount = u64::try_from_usize(text.len()).map_err(|source| {
self.error(MeasuredBudgetError::quantity(
ConversionResource::StructuredTextBytes,
source,
))
})?;
self.limits
.structured()
.max_text_bytes_limit()
.check(amount)
.map_err(|error| self.error(error.into()))?;
}
let measurement = if key {
JsonMeasurement::Key { bytes: text.len() }
} else {
JsonMeasurement::String {
depth,
bytes: text.len(),
}
};
self.admit(measurement)?;
self.output
.try_consume_usize(text.len())
.map_err(|error| self.error(error))
}
pub(in crate::json) fn admit_output_string(&mut self, bytes: usize, depth: usize) -> ValueResult<()> {
let amount = u64::try_from_usize(bytes).map_err(|source| {
self.error(MeasuredBudgetError::quantity(
ConversionResource::StructuredTextBytes,
source,
))
})?;
self.limits
.structured()
.max_text_bytes_limit()
.check(amount)
.map_err(|error| self.error(error.into()))?;
self.admit(JsonMeasurement::String { depth, bytes })?;
self.output.try_consume_usize(bytes).map_err(|error| self.error(error))
}
pub(in crate::json) fn display<T: Display + ?Sized>(
&mut self,
value: &T,
depth: usize,
number: bool,
) -> ValueResult<()> {
let mut writer = MeasurementWriter::new(self.rendering_budget(depth, number)?);
let formatted = write!(&mut writer, "{value}");
let bytes = writer.finish().map_err(|error| self.error(error))?;
formatted.map_err(|_| self.format_error())?;
let bytes = usize::try_from_u64(bytes)
.map_err(|source| self.error(MeasuredBudgetError::quantity(ConversionResource::OutputBytes, source)))?;
self.admit(if number {
JsonMeasurement::Number { depth, bytes }
} else {
JsonMeasurement::String { depth, bytes }
})?;
self.output.try_consume_usize(bytes).map_err(|error| self.error(error))
}
pub(in crate::json) fn format<T: Display + ?Sized>(&mut self, value: &T, depth: usize) -> ValueResult<String> {
let text = self
.rendering_budget(depth, false)?
.try_write_string(|writer| write!(writer.as_fmt(), "{value}"))
.map_err(|error| match error {
BudgetedStringError::Budget(error) => self.error(error.into()),
BudgetedStringError::Quantity { resource, source } => {
self.error(MeasuredBudgetError::quantity(resource, source))
}
_ => self.format_error(),
})?;
self.admit_output_string(text.len(), depth)?;
Ok(text)
}
pub(in crate::json) fn duration_conversion_error(&self, error: DataConversionError) -> ValueError {
let measured = match error.kind() {
DataConversionErrorKind::LimitExceeded => {
let source = find_error_source::<BudgetError<ConversionResource, u64>>(&error).cloned();
source.map(MeasuredBudgetError::Budget)
}
DataConversionErrorKind::Quantity => {
let Some(resource) = error.quantity_resource() else {
return ValueError::Conversion(error);
};
let source = find_error_source::<QuantityConversionError>(&error).cloned();
source.map(|source| MeasuredBudgetError::quantity(resource, source))
}
_ => return ValueError::Conversion(error),
};
match measured {
Some(source) if Some(*source.resource()) == error.resource() => self.error(source),
_ => ValueError::Conversion(error),
}
}
fn rendering_budget(&mut self, depth: usize, number: bool) -> ValueResult<ResourceBudget<ConversionResource, u64>> {
let probe = if number {
JsonMeasurement::Number { depth, bytes: 0 }
} else {
JsonMeasurement::String { depth, bytes: 0 }
};
let result = self.structure.transaction().try_admit(probe);
result.map_err(|error| self.error(error))?;
let mut rendering = ResourceBudget::from_limit(*self.limits.operation().output_bytes_limit());
rendering
.try_consume(self.output.used())
.map_err(|error| self.error(error.into()))?;
let mut payload = ResourceBudget::from_limit(*self.limits.operation().structured_payload_bytes_limit());
payload
.try_consume(self.structure.used_payload_bytes().unwrap_or(0))
.map_err(|error| self.error(error.into()))?;
let text_limit = ResourceBudget::from_limit(*self.limits.structured().max_text_bytes_limit());
for candidate in [payload, text_limit] {
if candidate.remaining() < rendering.remaining() {
rendering = candidate;
}
}
Ok(rendering)
}
#[inline]
fn format_error(&self) -> ValueError {
ValueError::Conversion(DataConversionError::invalid(
self.data_type,
DataType::Json,
InvalidValueReason::OutOfRange,
))
}
}
#[must_use]
fn find_error_source<T: Error + 'static>(error: &dyn Error) -> Option<&T> {
let mut source = error.source();
while let Some(current) = source {
if let Some(source) = current.downcast_ref::<T>() {
return Some(source);
}
source = current.source();
}
None
}