rustica/error/context.rs
1//! # Error Context Management and Functional Pipelines
2//!
3//! This module provides utilities for managing error context and creating
4//! functional error handling pipelines. It includes context accumulation,
5//! error transformation chains, and composable error handling patterns.
6
7use crate::error::{
8 ComposableResult,
9 types::{BoxedComposableResult, ComposableError, IntoErrorContext},
10};
11use smallvec::SmallVec;
12use std::fmt::Display;
13
14/// Adds context to any error type, creating a ComposableError.
15///
16/// This function provides a convenient way to add contextual information
17/// to any error, wrapping it in a ComposableError structure that supports
18/// context accumulation and error chaining.
19///
20/// # Type Parameters
21///
22/// * `E`: The original error type
23/// * `C`: The context type (must implement IntoErrorContext)
24///
25/// # Arguments
26///
27/// * `error`: The error to add context to
28/// * `context`: The context information to add
29///
30/// # Examples
31///
32/// ```rust
33/// use rustica::error::with_context;
34///
35/// let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file.txt");
36/// let contextual_error = with_context(io_error, "Failed to load configuration");
37///
38/// assert!(contextual_error.context().len() > 0);
39/// ```
40#[inline]
41pub fn with_context<E, C>(error: E, context: C) -> ComposableError<E>
42where
43 C: IntoErrorContext,
44{
45 ComposableError::new(error).with_context(context)
46}
47
48/// Adds context to a Result, converting errors to ComposableError.
49///
50/// This function transforms a `Result<T, E>` into a `Result<T, ComposableError<E>>`,
51/// adding the specified context to any error that occurs. Success values pass
52/// through unchanged.
53///
54/// # Type Parameters
55///
56/// * `T`: The success type
57/// * `E`: The original error type
58/// * `C`: The context type (must implement IntoErrorContext)
59///
60/// # Arguments
61///
62/// * `result`: The Result to add context to
63/// * `context`: The context information to add
64///
65/// # Examples
66///
67/// ```rust
68/// use rustica::error::with_context_result;
69///
70/// fn parse_number(s: &str) -> Result<i32, std::num::ParseIntError> {
71/// s.parse()
72/// }
73///
74/// let result = parse_number("not_a_number");
75/// let contextual = with_context_result(result, "Failed to parse user input");
76///
77/// match contextual {
78/// Ok(_) => panic!("Expected error"),
79/// Err(composable) => {
80/// assert_eq!(composable.context().len(), 1);
81/// assert!(composable.context()[0].contains("Failed to parse user input"));
82/// }
83/// }
84/// ```
85#[inline]
86pub fn with_context_result<T, E, C>(result: Result<T, E>, context: C) -> BoxedComposableResult<T, E>
87where
88 C: IntoErrorContext,
89{
90 result.map_err(|e| Box::new(with_context(e, context)))
91}
92
93/// Creates a context function that can be applied lazily.
94///
95/// This function returns a closure that, when called with an error,
96/// adds the specified context. This is useful for creating reusable
97/// context transformations and building error handling pipelines.
98///
99/// # Type Parameters
100///
101/// * `C`: The context type (must implement IntoErrorContext)
102///
103/// # Arguments
104///
105/// * `context`: The context information to add
106///
107/// # Examples
108///
109/// ```rust
110/// use rustica::error::context_fn;
111///
112/// let add_db_context = context_fn("Database operation failed");
113///
114/// let error = "Connection refused";
115/// let contextual_error = add_db_context(error);
116///
117/// assert_eq!(contextual_error.context().len(), 1);
118/// assert!(contextual_error.context()[0].contains("Database operation failed"));
119/// ```
120#[inline]
121pub fn context_fn<E, C>(context: C) -> impl Fn(E) -> ComposableError<E>
122where
123 C: IntoErrorContext + Clone,
124{
125 move |error| with_context(error, context.clone())
126}
127
128/// A functional error handling pipeline builder.
129///
130/// `ErrorPipeline` allows you to build composable error handling chains
131/// that can transform, contextualize, and recover from errors in a
132/// functional style. Each operation returns a new pipeline that can
133/// be further composed.
134///
135/// # Type Parameters
136///
137/// * `T`: The success type
138/// * `E`: The current error type
139///
140/// # Examples
141///
142/// ```rust
143/// use rustica::error::{ErrorPipeline, ComposableError};
144///
145/// let result: Result<i32, &str> = Err("parse error");
146///
147/// let processed = ErrorPipeline::new(result)
148/// .with_context("Failed to process input")
149/// .map_error(|e| format!("Error: {}", e))
150/// .recover(|_| Ok(42))
151/// .finish();
152///
153/// assert_eq!(processed, Ok(42));
154/// ```
155pub struct ErrorPipeline<T, E> {
156 result: Result<T, E>,
157 pending_contexts: SmallVec<[String; 4]>,
158}
159
160impl<T, E> ErrorPipeline<T, E> {
161 /// Creates a new error pipeline from a Result.
162 ///
163 /// # Arguments
164 ///
165 /// * `result`: The initial Result to process
166 ///
167 /// # Examples
168 ///
169 /// ```rust
170 /// use rustica::error::ErrorPipeline;
171 ///
172 /// let result: Result<i32, &str> = Ok(42);
173 /// let pipeline = ErrorPipeline::new(result);
174 /// ```
175 #[inline]
176 pub fn new(result: Result<T, E>) -> Self {
177 Self {
178 result,
179 pending_contexts: SmallVec::new(),
180 }
181 }
182
183 /// Adds context to errors in the pipeline.
184 ///
185 /// This buffers the context without transforming the error type,
186 /// enabling efficient deep pipeline operations. Contexts are only
187 /// applied when the pipeline is finished.
188 ///
189 /// # Arguments
190 ///
191 /// * `context`: The context to add
192 ///
193 /// # Examples
194 ///
195 /// ```rust
196 /// use rustica::error::ErrorPipeline;
197 ///
198 /// let result: Result<i32, &str> = Err("failed");
199 /// let pipeline = ErrorPipeline::new(result)
200 /// .with_context("Operation failed")
201 /// .with_context("Database error");
202 /// ```
203 #[inline]
204 pub fn with_context<C>(mut self, context: C) -> Self
205 where
206 C: IntoErrorContext,
207 {
208 if self.result.is_ok() {
209 return self;
210 }
211
212 let ctx_str = context.into_error_context().message().to_string();
213 self.pending_contexts.push(ctx_str);
214 self
215 }
216
217 /// Maps the error type to a new type.
218 ///
219 /// This applies a transformation function to any error in the pipeline,
220 /// allowing for error type conversions and transformations.
221 ///
222 /// # Type Parameters
223 ///
224 /// * `F`: The mapping function type
225 /// * `NewE`: The new error type
226 ///
227 /// # Arguments
228 ///
229 /// * `f`: The function to apply to errors
230 ///
231 /// # Examples
232 ///
233 /// ```rust
234 /// use rustica::error::ErrorPipeline;
235 ///
236 /// let result: Result<i32, i32> = Err(404);
237 /// let pipeline = ErrorPipeline::new(result)
238 /// .map_error(|code| format!("HTTP Error: {}", code));
239 /// ```
240 #[inline]
241 pub fn map_error<F, NewE>(self, f: F) -> ErrorPipeline<T, NewE>
242 where
243 F: FnOnce(E) -> NewE,
244 {
245 ErrorPipeline {
246 result: self.result.map_err(f),
247 pending_contexts: self.pending_contexts,
248 }
249 }
250
251 /// Applies a recovery function to errors.
252 ///
253 /// This allows the pipeline to recover from errors by providing
254 /// alternative values or alternative computations.
255 ///
256 /// # Type Parameters
257 ///
258 /// * `F`: The recovery function type
259 ///
260 /// # Arguments
261 ///
262 /// * `recovery`: The function to apply for error recovery
263 ///
264 /// # Examples
265 ///
266 /// ```rust
267 /// use rustica::error::ErrorPipeline;
268 ///
269 /// let result: Result<i32, &str> = Err("failed");
270 /// let pipeline = ErrorPipeline::new(result)
271 /// .recover(|_error| Ok(0)); // Provide default value
272 /// ```
273 #[inline]
274 pub fn recover<F>(self, recovery: F) -> ErrorPipeline<T, E>
275 where
276 F: FnOnce(E) -> Result<T, E>,
277 {
278 ErrorPipeline {
279 result: self.result.or_else(recovery),
280 pending_contexts: self.pending_contexts,
281 }
282 }
283
284 /// Chains another operation that might fail.
285 ///
286 /// This is similar to `Result::and_then`, allowing you to chain
287 /// operations that might produce errors.
288 ///
289 /// # Type Parameters
290 ///
291 /// * `U`: The new success type
292 /// * `F`: The chaining function type
293 ///
294 /// # Arguments
295 ///
296 /// * `f`: The function to apply to success values
297 ///
298 /// # Examples
299 ///
300 /// ```rust
301 /// use rustica::error::ErrorPipeline;
302 ///
303 /// let result: Result<i32, &str> = Ok(42);
304 /// let pipeline = ErrorPipeline::new(result)
305 /// .and_then(|x| if x > 0 { Ok(x * 2) } else { Err("negative") });
306 /// ```
307 #[inline]
308 pub fn and_then<U, F>(self, f: F) -> ErrorPipeline<U, E>
309 where
310 F: FnOnce(T) -> Result<U, E>,
311 {
312 ErrorPipeline {
313 result: self.result.and_then(f),
314 pending_contexts: self.pending_contexts,
315 }
316 }
317
318 /// Maps the success value to a new type.
319 ///
320 /// This applies a transformation function to any success value
321 /// in the pipeline, leaving errors unchanged.
322 ///
323 /// # Type Parameters
324 ///
325 /// * `U`: The new success type
326 /// * `F`: The mapping function type
327 ///
328 /// # Arguments
329 ///
330 /// * `f`: The function to apply to success values
331 ///
332 /// # Examples
333 ///
334 /// ```rust
335 /// use rustica::error::ErrorPipeline;
336 ///
337 /// let result: Result<i32, &str> = Ok(42);
338 /// let pipeline = ErrorPipeline::new(result)
339 /// .map(|x| x.to_string());
340 /// ```
341 #[inline]
342 pub fn map<U, F>(self, f: F) -> ErrorPipeline<U, E>
343 where
344 F: FnOnce(T) -> U,
345 {
346 ErrorPipeline {
347 result: self.result.map(f),
348 pending_contexts: self.pending_contexts,
349 }
350 }
351
352 /// Finishes the pipeline and returns the final result with applied contexts.
353 ///
354 /// This is the terminal operation of the pipeline that applies
355 /// all buffered contexts to any error and returns a **boxed** `ComposableError`.
356 /// This avoids clippy warnings about large error types and is recommended
357 /// for most use cases.
358 ///
359 /// If you want to avoid boxing (e.g., when `E` is small), use [`finish_without_box()`](Self::finish_without_box).
360 ///
361 /// # Examples
362 ///
363 /// ```rust
364 /// use rustica::error::ErrorPipeline;
365 ///
366 /// let result: Result<i32, &str> = Ok(42);
367 /// let final_result = ErrorPipeline::new(result)
368 /// .map(|x| x * 2)
369 /// .with_context("Processing data")
370 /// .finish();
371 ///
372 /// assert_eq!(final_result, Ok(84));
373 /// ```
374 #[inline]
375 pub fn finish(self) -> BoxedComposableResult<T, E> {
376 match self.result {
377 Ok(v) => Ok(v),
378 Err(e) => {
379 let composable = ComposableError::new(e).with_contexts(self.pending_contexts);
380 Err(Box::new(composable))
381 },
382 }
383 }
384
385 /// Finishes the pipeline and returns the final result without boxing.
386 ///
387 /// This is an alternative to [`finish()`](Self::finish) that returns an
388 /// **unboxed** `ComposableError`. Use this when:
389 /// - The error type `E` is small (e.g., `i32`, `&str`)
390 /// - You want to avoid heap allocation
391 /// - You're okay with clippy warnings about large error types
392 ///
393 /// For most use cases, prefer [`finish()`](Self::finish) to avoid clippy warnings.
394 ///
395 /// # Examples
396 ///
397 /// ```rust
398 /// use rustica::error::{ErrorPipeline, ComposableResult};
399 ///
400 /// let result: Result<i32, i32> = Err(404);
401 /// let final_result: ComposableResult<i32, i32> = ErrorPipeline::new(result)
402 /// .with_context("Request failed")
403 /// .finish_without_box();
404 ///
405 /// match final_result {
406 /// Ok(_) => panic!("Expected error"),
407 /// Err(e) => assert_eq!(e.core_error(), &404),
408 /// }
409 /// ```
410 #[inline]
411 #[allow(clippy::result_large_err)]
412 pub fn finish_without_box(self) -> ComposableResult<T, E> {
413 match self.result {
414 Ok(v) => Ok(v),
415 Err(e) => {
416 let composable = ComposableError::new(e).with_contexts(self.pending_contexts);
417 Err(composable)
418 },
419 }
420 }
421}
422
423/// Creates an error handling pipeline from a Result.
424///
425/// This is a convenience function that creates an `ErrorPipeline`
426/// from a Result, enabling functional error handling patterns.
427///
428/// # Type Parameters
429///
430/// * `T`: The success type
431/// * `E`: The error type
432///
433/// # Arguments
434///
435/// * `result`: The Result to create a pipeline from
436///
437/// # Examples
438///
439/// ```rust
440/// use rustica::error::error_pipeline;
441///
442/// let result: Result<i32, &str> = Err("failed");
443/// let processed = error_pipeline(result)
444/// .with_context("Operation failed")
445/// .recover(|_| Ok(0))
446/// .finish();
447///
448/// assert_eq!(processed, Ok(0));
449/// ```
450#[inline]
451pub fn error_pipeline<T, E>(result: Result<T, E>) -> ErrorPipeline<T, E> {
452 ErrorPipeline::new(result)
453}
454
455/// Accumulates context from multiple sources into a single error.
456///
457/// This function takes an error and multiple context sources,
458/// creating a ComposableError with all context information
459/// accumulated in order.
460///
461/// # Type Parameters
462///
463/// * `E`: The error type
464/// * `I`: The iterator type for contexts
465/// * `C`: The context item type
466///
467/// # Arguments
468///
469/// * `error`: The base error
470/// * `contexts`: An iterator of context information
471///
472/// # Examples
473///
474/// ```rust
475/// use rustica::error::accumulate_context;
476///
477/// let error = "core error";
478/// let contexts = vec!["step 1 failed", "step 2 failed", "operation failed"];
479/// let accumulated = accumulate_context(error, contexts);
480///
481/// assert_eq!(accumulated.context().len(), 3);
482/// ```
483pub fn accumulate_context<E, I, C>(error: E, contexts: I) -> ComposableError<E>
484where
485 I: IntoIterator<Item = C>,
486 C: IntoErrorContext,
487{
488 let context_strings: Vec<String> = contexts
489 .into_iter()
490 .map(|c| c.into_error_context().message().to_string())
491 .collect();
492
493 ComposableError::new(error).with_contexts(context_strings)
494}
495
496/// Creates a context accumulator function.
497///
498/// This returns a function that can accumulate multiple contexts
499/// onto an error. The returned function can be reused for multiple
500/// errors with the same context pattern.
501///
502/// # Type Parameters
503///
504/// * `I`: The iterator type for contexts
505/// * `C`: The context item type
506///
507/// # Arguments
508///
509/// * `contexts`: The contexts to accumulate
510///
511/// # Examples
512///
513/// ```rust
514/// use rustica::error::context_accumulator;
515///
516/// let contexts = vec!["database error", "user operation failed"];
517/// let accumulator = context_accumulator(contexts);
518///
519/// let error1 = "connection timeout";
520/// let error2 = "query failed";
521///
522/// let contextual1 = accumulator(error1);
523/// let contextual2 = accumulator(error2);
524///
525/// // Both errors now have the same context stack
526/// assert_eq!(contextual1.context().len(), 2);
527/// assert_eq!(contextual2.context().len(), 2);
528/// ```
529pub fn context_accumulator<E, I, C>(contexts: I) -> impl Fn(E) -> ComposableError<E>
530where
531 I: IntoIterator<Item = C> + Clone,
532 C: IntoErrorContext + Clone,
533{
534 move |error| accumulate_context(error, contexts.clone())
535}
536
537/// Formats an error with its full context chain.
538///
539/// This function creates a human-readable string representation
540/// of an error and all its context information, formatted as
541/// a chain from most recent context to core error.
542///
543/// # Type Parameters
544///
545/// * `E`: The error type (must implement Display)
546///
547/// # Arguments
548///
549/// * `error`: The ComposableError to format
550///
551/// # Examples
552///
553/// ```rust
554/// use rustica::error::{ComposableError, format_error_chain};
555///
556/// let error = ComposableError::new("file not found")
557/// .with_context("failed to load config".to_string())
558/// .with_context("application startup failed".to_string());
559///
560/// let formatted = format_error_chain(&error);
561/// assert!(formatted.contains("application startup failed"));
562/// assert!(formatted.contains("failed to load config"));
563/// assert!(formatted.contains("file not found"));
564/// ```
565pub fn format_error_chain<E>(error: &ComposableError<E>) -> String
566where
567 E: Display,
568{
569 error.error_chain()
570}
571
572/// Extracts all context information from a ComposableError.
573///
574/// This function returns a vector of all context strings in the
575/// order they were added (most recent first).
576///
577/// # Type Parameters
578///
579/// * `E`: The error type
580///
581/// # Arguments
582///
583/// * `error`: The ComposableError to extract context from
584///
585/// # Examples
586///
587/// ```rust
588/// use rustica::error::{ComposableError, extract_context};
589///
590/// let error = ComposableError::new("error")
591/// .with_context("context 1".to_string())
592/// .with_context("context 2".to_string());
593///
594/// let contexts = extract_context(&error);
595/// assert_eq!(contexts.len(), 2);
596/// assert_eq!(contexts[0], "context 2"); // Most recent first
597/// assert_eq!(contexts[1], "context 1");
598/// ```
599pub fn extract_context<E>(error: &ComposableError<E>) -> Vec<String> {
600 error.context()
601}