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