qubit_codec/transcode/transcode_convert_error.rs
1// =============================================================================
2// Copyright (c) 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Errors reported by transcode converters.
9
10use thiserror::Error;
11
12use super::capacity_error::CapacityError;
13use super::transcode_decode_errors::TranscodeDecodeError;
14use super::transcode_domain_error::TranscodeDomainError;
15use super::transcode_encode_error::TranscodeEncodeError;
16use super::transcode_failure::TranscodeFailure;
17use crate::Codec;
18
19/// Convert transcode error for a codec-backed converter.
20///
21/// # Type Parameters
22///
23/// - `D`: Source codec providing decode errors and the intermediate value type.
24/// - `E`: Target codec providing encode errors for the same value type.
25pub type TranscodeConvertErrorOf<D, E> =
26 TranscodeConvertError<<D as Codec>::DecodeError, <E as Codec>::EncodeError, <D as Codec>::Value>;
27
28/// Error reported by a unit-to-unit transcode conversion.
29///
30/// # Type Parameters
31///
32/// - `DE`: Source decoder domain error.
33/// - `EE`: Target encoder domain error.
34/// - `V`: Owned intermediate value retained when encoding is rejected.
35///
36/// For values emitted by the source codec's decode reset or finish phase, the
37/// converter uses placeholder input index 0 in encode contexts and errors.
38/// Such values have no source-unit position, and index 0 cannot distinguish
39/// them from a value decoded at source index 0. Ordinary decoded values retain
40/// their index in the input slice supplied to the decode call that produced
41/// them. A pending value keeps that index across calls; it is not rebased to a
42/// later input slice and is not a whole-stream offset. The retained index may
43/// be outside the bounds of a later input slice. Error phases describe the
44/// underlying codec operation, so encoding a lifecycle-generated value uses
45/// the main encode path even during converter reset or finish.
46///
47/// # Examples
48///
49/// ```
50/// use qubit_codec::TranscodeConvertError;
51///
52/// let error = TranscodeConvertError::<&str, &str, u8>::unencodable(3, 42);
53/// assert_eq!(error.unencodable_ref(), Some((3, Some(&42))));
54/// assert_eq!(error.failure_ref(), None);
55/// ```
56#[derive(Clone, Debug, Eq, Error, Hash, PartialEq)]
57#[must_use]
58pub enum TranscodeConvertError<DE, EE, V> {
59 /// Framework-level transcode failure.
60 #[error(transparent)]
61 Failure(
62 /// Framework contract, capacity, or lifecycle failure.
63 #[from]
64 TranscodeFailure,
65 ),
66
67 /// Source-side domain error.
68 #[error("decode side failed: {0}")]
69 DecodeDomain(
70 /// Owned source error with its lifecycle phase and input context.
71 #[source]
72 TranscodeDomainError<DE>,
73 ),
74
75 /// Target-side domain error.
76 #[error("encode side failed: {0}")]
77 EncodeDomain(
78 /// Owned target error with its lifecycle phase and input context.
79 #[source]
80 TranscodeDomainError<EE>,
81 ),
82
83 /// The decoded intermediate value cannot be encoded by the target codec and
84 /// policy.
85 #[error("unencodable value at input index {input_index}")]
86 Unencodable {
87 /// Source input index of the value being encoded. Lifecycle-generated
88 /// converter values use placeholder 0 because they have no source-unit
89 /// position; ordinary decoded values retain their index in the input
90 /// slice supplied to the decode call that produced them, including
91 /// when kept pending across calls.
92 input_index: usize,
93 /// `Some` retains the decoded intermediate value; `None` means value
94 /// context was unavailable or intentionally omitted.
95 value: Option<V>,
96 },
97}
98
99impl<DE, EE, V> TranscodeConvertError<DE, EE, V> {
100 /// Creates a source reset-phase domain-specific converter error.
101 ///
102 /// # Parameters
103 ///
104 /// - `source`: Owned source error raised during reset.
105 ///
106 /// # Returns
107 ///
108 /// Returns a source-domain reset error.
109 #[inline]
110 pub const fn decode_domain_reset(source: DE) -> Self {
111 Self::DecodeDomain(TranscodeDomainError::reset(source))
112 }
113
114 /// Creates a source main-phase domain-specific converter error.
115 ///
116 /// # Parameters
117 ///
118 /// - `source`: Owned source error raised during conversion.
119 /// - `input_index`: Absolute source position of the failed operation.
120 ///
121 /// # Returns
122 ///
123 /// Returns a source-domain main-phase error at the supplied input index.
124 #[inline]
125 pub const fn decode_domain_main(source: DE, input_index: usize) -> Self {
126 Self::DecodeDomain(TranscodeDomainError::main(source, input_index))
127 }
128
129 /// Creates a source main-phase converter error with decode consumption
130 /// context.
131 ///
132 /// # Parameters
133 ///
134 /// - `source`: Owned source error raised during conversion.
135 /// - `input_index`: Source index for the failed operation. For a
136 /// lifecycle-generated converter value, use placeholder 0 because it has
137 /// no source-unit position; an ordinary decoded value retains its index
138 /// in the slice supplied to the decode call that produced it, even when
139 /// kept pending across calls.
140 /// - `input_consumed`: `Some` records invalid units consumed; `None` means
141 /// unknown.
142 ///
143 /// # Returns
144 ///
145 /// Returns a source-domain main-phase error retaining consumption context.
146 #[inline]
147 pub const fn decode_domain_main_with_consumed(
148 source: DE,
149 input_index: usize,
150 input_consumed: Option<core::num::NonZeroUsize>,
151 ) -> Self {
152 Self::DecodeDomain(TranscodeDomainError::main_with_consumed(
153 source,
154 input_index,
155 input_consumed,
156 ))
157 }
158
159 /// Creates a source finish-phase domain-specific converter error.
160 ///
161 /// # Parameters
162 ///
163 /// - `source`: Owned source error raised during finalization.
164 ///
165 /// # Returns
166 ///
167 /// Returns a source-domain finish error.
168 #[inline]
169 pub const fn decode_domain_finish(source: DE) -> Self {
170 Self::DecodeDomain(TranscodeDomainError::finish(source))
171 }
172
173 /// Creates a target reset-phase domain-specific converter error.
174 ///
175 /// # Parameters
176 ///
177 /// - `source`: Owned target error raised during reset.
178 ///
179 /// # Returns
180 ///
181 /// Returns a target-domain reset error.
182 #[inline]
183 pub const fn encode_domain_reset(source: EE) -> Self {
184 Self::EncodeDomain(TranscodeDomainError::reset(source))
185 }
186
187 /// Creates a target main-phase domain-specific converter error.
188 ///
189 /// # Parameters
190 ///
191 /// - `source`: Owned target error raised during conversion.
192 /// - `input_index`: Source index for the failed operation. For a
193 /// lifecycle-generated converter value, use placeholder 0 because it has
194 /// no source-unit position; an ordinary decoded value retains its index
195 /// in the slice supplied to the decode call that produced it, even when
196 /// kept pending across calls.
197 ///
198 /// # Returns
199 ///
200 /// Returns a target-domain main-phase error at the supplied input index.
201 /// The main phase identifies the underlying encode operation, including
202 /// when a converter invokes it during reset or finish.
203 #[inline]
204 pub const fn encode_domain_main(source: EE, input_index: usize) -> Self {
205 Self::EncodeDomain(TranscodeDomainError::main(source, input_index))
206 }
207
208 /// Creates a target finish-phase domain-specific converter error.
209 ///
210 /// # Parameters
211 ///
212 /// - `source`: Owned target error raised during finalization.
213 ///
214 /// # Returns
215 ///
216 /// Returns a target-domain finish error.
217 #[inline]
218 pub const fn encode_domain_finish(source: EE) -> Self {
219 Self::EncodeDomain(TranscodeDomainError::finish(source))
220 }
221
222 /// Creates an unencodable intermediate-value error with value context.
223 ///
224 /// # Parameters
225 ///
226 /// - `input_index`: Source index of the rejected value. Lifecycle-generated
227 /// converter values use placeholder 0 because they have no source-unit
228 /// position; an ordinary decoded value retains its index in the slice
229 /// supplied to the decode call that produced it, even when kept pending
230 /// across calls.
231 /// - `value`: Intermediate value moved into the error for recovery.
232 ///
233 /// # Returns
234 ///
235 /// Returns an unencodable error owning `Some(value)`.
236 #[inline]
237 pub fn unencodable(input_index: usize, value: V) -> Self {
238 Self::Unencodable {
239 input_index,
240 value: Some(value),
241 }
242 }
243
244 /// Creates an unencodable intermediate-value error without value context.
245 ///
246 /// # Parameters
247 ///
248 /// - `input_index`: Source index of the rejected value. Lifecycle-generated
249 /// converter values use placeholder 0 because they have no source-unit
250 /// position; an ordinary decoded value retains its index in the slice
251 /// supplied to the decode call that produced it, even when kept pending
252 /// across calls.
253 ///
254 /// # Returns
255 ///
256 /// Returns an unencodable error with absent (`None`) value context.
257 #[inline]
258 pub const fn unencodable_without_context(input_index: usize) -> Self {
259 Self::Unencodable {
260 input_index,
261 value: None,
262 }
263 }
264
265 /// Converts an encode error into a converter error while adding fallback
266 /// value context to unencodable errors that lack it.
267 ///
268 /// # Parameters
269 ///
270 /// - `error`: Owned target-side error to convert.
271 /// - `fallback_value`: Value moved into absent context, or dropped if not
272 /// needed.
273 ///
274 /// # Returns
275 ///
276 /// Returns the converted error, adding the fallback only when unencodable
277 /// value context is absent.
278 #[inline]
279 pub fn from_encode_error_with_value(error: TranscodeEncodeError<EE, V>, fallback_value: V) -> Self {
280 match error {
281 TranscodeEncodeError::Unencodable {
282 input_index,
283 value: None,
284 } => Self::Unencodable {
285 input_index,
286 value: Some(fallback_value),
287 },
288 other => Self::from(other),
289 }
290 }
291
292 /// Returns the framework failure carried by this error.
293 ///
294 /// # Returns
295 ///
296 /// Returns `Some` for a framework failure, or `None` for domain and
297 /// unencodable errors.
298 #[inline]
299 pub const fn failure_ref(&self) -> Option<&TranscodeFailure> {
300 match self {
301 Self::Failure(failure) => Some(failure),
302 Self::DecodeDomain(_) | Self::EncodeDomain(_) | Self::Unencodable { .. } => None,
303 }
304 }
305
306 /// Borrows the unencodable value context carried by this error.
307 ///
308 /// # Returns
309 ///
310 /// Returns `Some((index, value))` for an unencodable error and `None`
311 /// otherwise. The inner `Some` borrows retained value context; inner
312 /// `None` means it is absent.
313 #[inline]
314 pub const fn unencodable_ref(&self) -> Option<(usize, Option<&V>)> {
315 match self {
316 Self::Unencodable { input_index, value } => Some((*input_index, value.as_ref())),
317 Self::Failure(_) | Self::DecodeDomain(_) | Self::EncodeDomain(_) => None,
318 }
319 }
320
321 /// Maps the source domain error while preserving other errors.
322 ///
323 /// # Type Parameters
324 ///
325 /// - `F`: One-shot transformation from `DE` to `T`.
326 /// - `T`: Replacement payload type.
327 ///
328 /// # Parameters
329 ///
330 /// - `f`: Invoked only for a source-domain error; otherwise dropped without
331 /// being called.
332 ///
333 /// # Returns
334 ///
335 /// Returns an owned error preserving all other variants and positional
336 /// context.
337 ///
338 /// # Panics
339 ///
340 /// Propagates a panic if the invoked transformation panics.
341 #[inline]
342 pub fn map_decode_domain<F, T>(self, f: F) -> TranscodeConvertError<T, EE, V>
343 where
344 F: FnOnce(DE) -> T,
345 {
346 match self {
347 Self::Failure(failure) => TranscodeConvertError::Failure(failure),
348 Self::DecodeDomain(error) => TranscodeConvertError::DecodeDomain(error.map_source(f)),
349 Self::EncodeDomain(error) => TranscodeConvertError::EncodeDomain(error),
350 Self::Unencodable { input_index, value } => TranscodeConvertError::Unencodable { input_index, value },
351 }
352 }
353
354 /// Maps the target domain error while preserving other errors.
355 ///
356 /// # Type Parameters
357 ///
358 /// - `F`: One-shot transformation from `EE` to `T`.
359 /// - `T`: Replacement payload type.
360 ///
361 /// # Parameters
362 ///
363 /// - `f`: Invoked only for a target-domain error; otherwise dropped without
364 /// being called.
365 ///
366 /// # Returns
367 ///
368 /// Returns an owned error preserving all other variants and positional
369 /// context.
370 ///
371 /// # Panics
372 ///
373 /// Propagates a panic if the invoked transformation panics.
374 #[inline]
375 pub fn map_encode_domain<F, T>(self, f: F) -> TranscodeConvertError<DE, T, V>
376 where
377 F: FnOnce(EE) -> T,
378 {
379 match self {
380 Self::Failure(failure) => TranscodeConvertError::Failure(failure),
381 Self::DecodeDomain(error) => TranscodeConvertError::DecodeDomain(error),
382 Self::EncodeDomain(error) => TranscodeConvertError::EncodeDomain(error.map_source(f)),
383 Self::Unencodable { input_index, value } => TranscodeConvertError::Unencodable { input_index, value },
384 }
385 }
386
387 /// Maps value context carried by unencodable-value errors.
388 ///
389 /// # Type Parameters
390 ///
391 /// - `F`: One-shot transformation from `V` to `W`.
392 /// - `W`: Replacement payload type.
393 ///
394 /// # Parameters
395 ///
396 /// - `f`: Invoked only for present unencodable value context; otherwise
397 /// dropped without being called.
398 ///
399 /// # Returns
400 ///
401 /// Returns an owned error preserving all other variants and positional
402 /// context.
403 ///
404 /// # Panics
405 ///
406 /// Propagates a panic if the invoked transformation panics.
407 #[inline]
408 pub fn map_value<F, W>(self, f: F) -> TranscodeConvertError<DE, EE, W>
409 where
410 F: FnOnce(V) -> W,
411 {
412 match self {
413 Self::Failure(failure) => TranscodeConvertError::Failure(failure),
414 Self::DecodeDomain(error) => TranscodeConvertError::DecodeDomain(error),
415 Self::EncodeDomain(error) => TranscodeConvertError::EncodeDomain(error),
416 Self::Unencodable { input_index, value } => TranscodeConvertError::Unencodable {
417 input_index,
418 value: value.map(f),
419 },
420 }
421 }
422}
423
424impl<DE, EE, V> From<TranscodeDecodeError<DE>> for TranscodeConvertError<DE, EE, V> {
425 /// Converts a source-side decode error into a converter error.
426 ///
427 /// # Parameters
428 ///
429 /// - `error`: Owned failure to convert without discarding its context.
430 ///
431 /// # Returns
432 ///
433 /// Returns the corresponding converter variant preserving the original
434 /// cause.
435 #[inline]
436 fn from(error: TranscodeDecodeError<DE>) -> Self {
437 match error {
438 TranscodeDecodeError::Failure(failure) => Self::Failure(failure),
439 TranscodeDecodeError::Domain(error) => Self::DecodeDomain(error),
440 }
441 }
442}
443
444impl<DE, EE, V> From<TranscodeEncodeError<EE, V>> for TranscodeConvertError<DE, EE, V> {
445 /// Converts a target-side encode error into a converter error.
446 ///
447 /// # Parameters
448 ///
449 /// - `error`: Owned failure to convert without discarding its context.
450 ///
451 /// # Returns
452 ///
453 /// Returns the corresponding converter variant preserving the original
454 /// cause.
455 #[inline]
456 fn from(error: TranscodeEncodeError<EE, V>) -> Self {
457 match error {
458 TranscodeEncodeError::Failure(failure) => Self::Failure(failure),
459 TranscodeEncodeError::Unencodable { input_index, value } => Self::Unencodable { input_index, value },
460 TranscodeEncodeError::Domain(error) => Self::EncodeDomain(error),
461 }
462 }
463}
464
465impl<DE, EE, V> From<CapacityError> for TranscodeConvertError<DE, EE, V> {
466 /// Converts capacity planning errors into transcode framework errors.
467 ///
468 /// # Parameters
469 ///
470 /// - `error`: Owned failure to convert without discarding its context.
471 ///
472 /// # Returns
473 ///
474 /// Returns the corresponding converter variant preserving the original
475 /// cause.
476 #[inline]
477 fn from(error: CapacityError) -> Self {
478 TranscodeFailure::from(error).into()
479 }
480}