vkteams_bot/
error.rs

1use serde::{Deserialize, Serialize};
2use std::env::VarError;
3use std::fmt;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct ApiError {
7    pub description: String,
8}
9
10impl fmt::Display for ApiError {
11    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12        write!(f, "API Error: {}", self.description)
13    }
14}
15
16impl std::error::Error for ApiError {}
17
18#[derive(Debug)]
19pub struct OtlpError {
20    pub message: String,
21}
22
23impl fmt::Display for OtlpError {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        write!(f, "{}", self.message)
26    }
27}
28
29impl std::error::Error for OtlpError {}
30
31impl From<String> for OtlpError {
32    fn from(message: String) -> Self {
33        OtlpError { message }
34    }
35}
36
37impl From<Box<dyn std::error::Error>> for OtlpError {
38    fn from(err: Box<dyn std::error::Error>) -> Self {
39        OtlpError {
40            message: err.to_string(),
41        }
42    }
43}
44
45#[derive(Debug)]
46pub enum BotError {
47    /// API Error
48    Api(ApiError),
49    /// Network Error
50    Network(reqwest::Error),
51    /// gRPC Error
52    #[cfg(feature = "grpc")]
53    Grpc(tonic::transport::Error),
54    /// Serialization/Deserialization Error
55    Serialization(serde_json::Error),
56    /// URL Error
57    Url(url::ParseError),
58    /// File System Error
59    Io(std::io::Error),
60    /// Template Error
61    #[cfg(feature = "templates")]
62    Template(tera::Error),
63    /// Configuration Error
64    Config(String),
65    /// Validation Error
66    Validation(String),
67    /// URL Parameters Error
68    UrlParams(serde_url_params::Error),
69    /// System Error
70    System(String),
71    /// Environment Error
72    Environment(std::env::VarError),
73    /// Otlp Error
74    Otlp(OtlpError),
75}
76
77impl fmt::Display for BotError {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        match self {
80            BotError::Api(e) => write!(f, "API Error: {e}"),
81            BotError::Network(e) => write!(f, "Network Error: {e}"),
82            #[cfg(feature = "grpc")]
83            BotError::Grpc(e) => write!(f, "gRPC Error: {e}"),
84            BotError::Serialization(e) => write!(f, "Serialization Error: {e}"),
85            BotError::Url(e) => write!(f, "URL Error: {e}"),
86            BotError::Io(e) => write!(f, "IO Error: {e}"),
87            #[cfg(feature = "templates")]
88            BotError::Template(e) => write!(f, "Template Error: {e}"),
89            BotError::Config(e) => write!(f, "Config Error: {e}"),
90            BotError::Validation(e) => write!(f, "Validation Error: {e}"),
91            BotError::UrlParams(e) => write!(f, "URL Parameters Error: {e}"),
92            BotError::System(e) => write!(f, "System Error: {e}"),
93            BotError::Environment(e) => write!(f, "Environment Error: {e}"),
94            BotError::Otlp(e) => write!(f, "Otlp Error: {e}"),
95        }
96    }
97}
98
99impl std::error::Error for BotError {
100    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
101        match self {
102            BotError::Api(e) => Some(e),
103            BotError::Network(e) => Some(e),
104            #[cfg(feature = "grpc")]
105            BotError::Grpc(e) => Some(e),
106            BotError::Serialization(e) => Some(e),
107            BotError::Url(e) => Some(e),
108            BotError::Io(e) => Some(e),
109            #[cfg(feature = "templates")]
110            BotError::Template(e) => Some(e),
111            BotError::Config(_) => None,
112            BotError::Validation(_) => None,
113            BotError::UrlParams(e) => Some(e),
114            BotError::System(_) => None,
115            BotError::Environment(e) => Some(e),
116            BotError::Otlp(e) => Some(e),
117        }
118    }
119}
120
121impl From<reqwest::Error> for BotError {
122    fn from(err: reqwest::Error) -> Self {
123        BotError::Network(err)
124    }
125}
126
127impl From<serde_json::Error> for BotError {
128    fn from(err: serde_json::Error) -> Self {
129        BotError::Serialization(err)
130    }
131}
132
133impl From<url::ParseError> for BotError {
134    fn from(err: url::ParseError) -> Self {
135        BotError::Url(err)
136    }
137}
138
139impl From<std::io::Error> for BotError {
140    fn from(err: std::io::Error) -> Self {
141        BotError::Io(err)
142    }
143}
144
145#[cfg(feature = "templates")]
146impl From<tera::Error> for BotError {
147    fn from(err: tera::Error) -> Self {
148        BotError::Template(err)
149    }
150}
151
152impl From<serde_url_params::Error> for BotError {
153    fn from(err: serde_url_params::Error) -> Self {
154        BotError::UrlParams(err)
155    }
156}
157#[cfg(feature = "grpc")]
158impl From<tonic::transport::Error> for BotError {
159    fn from(err: tonic::transport::Error) -> Self {
160        BotError::Grpc(err)
161    }
162}
163
164impl From<toml::de::Error> for BotError {
165    fn from(err: toml::de::Error) -> Self {
166        BotError::Config(err.to_string())
167    }
168}
169
170impl From<VarError> for BotError {
171    fn from(err: VarError) -> Self {
172        BotError::Environment(err)
173    }
174}
175
176impl From<ApiError> for BotError {
177    fn from(err: ApiError) -> Self {
178        BotError::Api(err)
179    }
180}
181
182pub type Result<T> = std::result::Result<T, BotError>;
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use serde_json;
188    use url::ParseError;
189
190    #[test]
191    fn test_api_error_display() {
192        let err = ApiError {
193            description: "fail".to_string(),
194        };
195        assert_eq!(format!("{err}"), "API Error: fail");
196    }
197
198    #[test]
199    fn test_otlp_error_display_and_from() {
200        let err = OtlpError {
201            message: "otlp fail".to_string(),
202        };
203        assert_eq!(format!("{err}"), "otlp fail");
204        let from_str: OtlpError = "msg".to_string().into();
205        assert_eq!(from_str.message, "msg");
206        let boxed: OtlpError =
207            Box::<dyn std::error::Error>::from(std::io::Error::other("err")).into();
208        assert!(boxed.message.contains("err"));
209    }
210
211    #[test]
212    fn test_bot_error_display_and_from() {
213        let api = ApiError {
214            description: "api".to_string(),
215        };
216        let err = BotError::Api(api.clone());
217        assert!(format!("{err}").contains("API Error"));
218        let ser = BotError::Serialization(serde_json::Error::io(std::io::Error::other("ser")));
219        assert!(format!("{ser}").contains("Serialization Error"));
220        let url = BotError::Url(ParseError::EmptyHost);
221        assert!(format!("{url}").contains("URL Error"));
222        let ioerr = BotError::Io(std::io::Error::other("io"));
223        assert!(format!("{ioerr}").contains("IO Error"));
224        let conf = BotError::Config("conf".to_string());
225        assert!(format!("{conf}").contains("Config Error"));
226        let val = BotError::Validation("val".to_string());
227        assert!(format!("{val}").contains("Validation Error"));
228        let sys = BotError::System("sys".to_string());
229        assert!(format!("{sys}").contains("System Error"));
230        let otlp = BotError::Otlp(OtlpError {
231            message: "otlp".to_string(),
232        });
233        assert!(format!("{otlp}").contains("Otlp Error"));
234    }
235
236    #[test]
237    fn test_bot_error_from_impls() {
238        let _b: BotError = serde_json::Error::io(std::io::Error::other("ser")).into();
239        let _b: BotError = url::ParseError::EmptyHost.into();
240        let _b: BotError = std::io::Error::other("io").into();
241        let _b: BotError = serde_url_params::Error::unsupported("params").into();
242        let _b: BotError = toml::from_str::<i32>("not toml").unwrap_err().into();
243        let _b: BotError = std::env::VarError::NotPresent.into();
244        let _b: BotError = ApiError {
245            description: "api".to_string(),
246        }
247        .into();
248    }
249
250    #[test]
251    fn test_api_error_serialization() {
252        let error = ApiError {
253            description: "Test error message".to_string(),
254        };
255
256        // Test serialization
257        let serialized = serde_json::to_string(&error).unwrap();
258        assert!(serialized.contains("Test error message"));
259
260        // Test deserialization
261        let deserialized: ApiError = serde_json::from_str(&serialized).unwrap();
262        assert_eq!(deserialized.description, "Test error message");
263    }
264
265    #[test]
266    fn test_api_error_clone() {
267        let error = ApiError {
268            description: "Cloneable error".to_string(),
269        };
270        let cloned = error.clone();
271        assert_eq!(error.description, cloned.description);
272    }
273
274    #[test]
275    fn test_api_error_as_std_error() {
276        let error = ApiError {
277            description: "Standard error test".to_string(),
278        };
279
280        // Test that it implements std::error::Error
281        let std_err: &dyn std::error::Error = &error;
282        assert!(std_err.source().is_none());
283    }
284
285    #[test]
286    fn test_otlp_error_from_box_error() {
287        let io_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Access denied");
288        let boxed: Box<dyn std::error::Error> = Box::new(io_error);
289        let otlp_error: OtlpError = boxed.into();
290
291        assert!(otlp_error.message.contains("Access denied"));
292    }
293
294    #[test]
295    fn test_otlp_error_as_std_error() {
296        let error = OtlpError {
297            message: "OTLP connection failed".to_string(),
298        };
299
300        // Test that it implements std::error::Error
301        let std_err: &dyn std::error::Error = &error;
302        assert!(std_err.source().is_none());
303    }
304
305    #[tokio::test]
306    async fn test_bot_error_source_chain() {
307        use std::error::Error;
308
309        // Test API error source
310        let api_err = BotError::Api(ApiError {
311            description: "API failed".to_string(),
312        });
313        assert!(api_err.source().is_some());
314
315        // Test Network error source
316        let network_err = BotError::Network(
317            reqwest::ClientBuilder::new()
318                .build()
319                .unwrap()
320                .get("http://invalid-url")
321                .send()
322                .await
323                .unwrap_err(),
324        );
325        assert!(network_err.source().is_some());
326
327        // Test Serialization error source
328        let ser_err = BotError::Serialization(serde_json::Error::io(std::io::Error::other(
329            "serialization",
330        )));
331        assert!(ser_err.source().is_some());
332
333        // Test URL error source
334        let url_err = BotError::Url(url::ParseError::EmptyHost);
335        assert!(url_err.source().is_some());
336
337        // Test IO error source
338        let io_err = BotError::Io(std::io::Error::new(
339            std::io::ErrorKind::NotFound,
340            "file not found",
341        ));
342        assert!(io_err.source().is_some());
343
344        // Test UrlParams error source
345        let params_err = BotError::UrlParams(serde_url_params::Error::unsupported("test"));
346        assert!(params_err.source().is_some());
347
348        // Test OTLP error source
349        let otlp_err = BotError::Otlp(OtlpError {
350            message: "OTLP failed".to_string(),
351        });
352        assert!(otlp_err.source().is_some());
353
354        // Test errors with no source
355        let config_err = BotError::Config("Config error".to_string());
356        assert!(config_err.source().is_none());
357
358        let validation_err = BotError::Validation("Validation error".to_string());
359        assert!(validation_err.source().is_none());
360
361        let system_err = BotError::System("System error".to_string());
362        assert!(system_err.source().is_none());
363    }
364
365    #[test]
366    fn test_bot_error_debug_format() {
367        let errors = vec![
368            BotError::Api(ApiError {
369                description: "Debug API error".to_string(),
370            }),
371            BotError::Config("Debug config error".to_string()),
372            BotError::Validation("Debug validation error".to_string()),
373            BotError::System("Debug system error".to_string()),
374            BotError::Otlp(OtlpError {
375                message: "Debug OTLP error".to_string(),
376            }),
377        ];
378
379        for error in errors {
380            let debug_str = error.to_string();
381            assert!(!debug_str.is_empty());
382            assert!(debug_str.contains("Error"));
383        }
384    }
385
386    #[test]
387    fn test_error_conversion_chain() {
388        // Test VarError conversion chain
389        let var_error = std::env::VarError::NotPresent;
390        let bot_error: BotError = var_error.into();
391        match bot_error {
392            BotError::Environment(msg) => assert!(msg.to_string().contains("not found")),
393            _ => panic!("Expected Config error"),
394        }
395
396        // Test TOML error conversion chain
397        let toml_error = toml::from_str::<i32>("invalid toml").unwrap_err();
398        let bot_error: BotError = toml_error.into();
399        match bot_error {
400            BotError::Config(_) => {} // Expected
401            _ => panic!("Expected Config error"),
402        }
403    }
404
405    #[tokio::test]
406    async fn test_all_error_types_display() {
407        let test_cases = vec![
408            (
409                BotError::Api(ApiError {
410                    description: "API test".to_string(),
411                }),
412                "API Error",
413            ),
414            (
415                BotError::Network(
416                    reqwest::ClientBuilder::new()
417                        .build()
418                        .unwrap()
419                        .get("http://test")
420                        .send()
421                        .await
422                        .unwrap_err(),
423                ),
424                "Network Error",
425            ),
426            (
427                BotError::Serialization(serde_json::Error::io(std::io::Error::other("test"))),
428                "Serialization Error",
429            ),
430            (BotError::Url(url::ParseError::EmptyHost), "URL Error"),
431            (BotError::Io(std::io::Error::other("test")), "IO Error"),
432            (BotError::Config("test".to_string()), "Config Error"),
433            (BotError::Validation("test".to_string()), "Validation Error"),
434            (
435                BotError::UrlParams(serde_url_params::Error::unsupported("test")),
436                "URL Parameters Error",
437            ),
438            (BotError::System("test".to_string()), "System Error"),
439            (
440                BotError::Otlp(OtlpError {
441                    message: "test".to_string(),
442                }),
443                "Otlp Error",
444            ),
445        ];
446
447        for (error, expected_prefix) in test_cases {
448            let display_str = format!("{error}");
449            assert!(
450                display_str.contains(expected_prefix),
451                "Error '{display_str}' should contain '{expected_prefix}'"
452            );
453        }
454    }
455
456    #[cfg(feature = "templates")]
457    #[test]
458    fn test_template_error_conversion() {
459        use tera::Tera;
460
461        let tera = Tera::new("templates/*").unwrap_or_default();
462        let template_error = tera
463            .render("nonexistent", &tera::Context::new())
464            .unwrap_err();
465        let bot_error: BotError = template_error.into();
466
467        match bot_error {
468            BotError::Template(_) => {} // Expected
469            _ => panic!("Expected Template error"),
470        }
471
472        let display_str = format!("{bot_error}");
473        assert!(display_str.contains("Template Error"));
474    }
475
476    #[test]
477    fn test_result_type_alias() {
478        // Test that our Result type alias works correctly
479        fn test_function() -> Result<String> {
480            Ok("success".to_string())
481        }
482
483        fn test_error_function() -> Result<String> {
484            Err(BotError::Validation("test error".to_string()))
485        }
486
487        assert!(test_function().is_ok());
488        assert_eq!(test_function().unwrap(), "success");
489
490        assert!(test_error_function().is_err());
491        match test_error_function().unwrap_err() {
492            BotError::Validation(msg) => assert_eq!(msg, "test error"),
493            _ => panic!("Expected Validation error"),
494        }
495    }
496
497    #[test]
498    fn test_error_message_content() {
499        // Test that error messages contain expected content
500        let api_error = ApiError {
501            description: "Specific API failure".to_string(),
502        };
503        assert!(format!("{api_error}").contains("Specific API failure"));
504
505        let otlp_error = OtlpError {
506            message: "OTLP connection timeout".to_string(),
507        };
508        assert!(format!("{otlp_error}").contains("OTLP connection timeout"));
509
510        let config_error = BotError::Config("Missing required field".to_string());
511        assert!(format!("{config_error}").contains("Missing required field"));
512    }
513}
514
515#[test]
516fn test_api_error_serialization() {
517    let error = ApiError {
518        description: "Test error message".to_string(),
519    };
520
521    // Test serialization
522    let serialized = serde_json::to_string(&error).unwrap();
523    assert!(serialized.contains("Test error message"));
524
525    // Test deserialization
526    let deserialized: ApiError = serde_json::from_str(&serialized).unwrap();
527    assert_eq!(deserialized.description, "Test error message");
528}
529
530#[test]
531fn test_api_error_clone() {
532    let error = ApiError {
533        description: "Cloneable error".to_string(),
534    };
535    let cloned = error.clone();
536    assert_eq!(error.description, cloned.description);
537}
538
539#[test]
540fn test_api_error_as_std_error() {
541    let error = ApiError {
542        description: "Standard error test".to_string(),
543    };
544
545    // Test that it implements std::error::Error
546    let std_err: &dyn std::error::Error = &error;
547    assert!(std_err.source().is_none());
548}
549
550#[test]
551fn test_otlp_error_from_box_error() {
552    let io_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Access denied");
553    let boxed: Box<dyn std::error::Error> = Box::new(io_error);
554    let otlp_error: OtlpError = boxed.into();
555
556    assert!(otlp_error.message.contains("Access denied"));
557}
558
559#[test]
560fn test_otlp_error_as_std_error() {
561    let error = OtlpError {
562        message: "OTLP connection failed".to_string(),
563    };
564
565    // Test that it implements std::error::Error
566    let std_err: &dyn std::error::Error = &error;
567    assert!(std_err.source().is_none());
568}
569
570#[tokio::test]
571async fn test_bot_error_source_chain() {
572    use std::error::Error;
573    // Test API error source
574    let api_err = BotError::Api(ApiError {
575        description: "API failed".to_string(),
576    });
577    assert!(api_err.source().is_some());
578
579    // Test Network error source
580    let network_err = BotError::Network(
581        reqwest::ClientBuilder::new()
582            .build()
583            .unwrap()
584            .get("http://invalid-url")
585            .send()
586            .await
587            .unwrap_err(),
588    );
589    assert!(network_err.source().is_some());
590
591    // Test Serialization error source
592    let ser_err = BotError::Serialization(serde_json::Error::io(std::io::Error::other(
593        "serialization",
594    )));
595    assert!(ser_err.source().is_some());
596
597    // Test URL error source
598    let url_err = BotError::Url(url::ParseError::EmptyHost);
599    assert!(url_err.source().is_some());
600
601    // Test IO error source
602    let io_err = BotError::Io(std::io::Error::new(
603        std::io::ErrorKind::NotFound,
604        "file not found",
605    ));
606    assert!(io_err.source().is_some());
607
608    // Test UrlParams error source
609    let params_err = BotError::UrlParams(serde_url_params::Error::unsupported("test"));
610    assert!(params_err.source().is_some());
611
612    // Test OTLP error source
613    let otlp_err = BotError::Otlp(OtlpError {
614        message: "OTLP failed".to_string(),
615    });
616    assert!(otlp_err.source().is_some());
617
618    // Test errors with no source
619    let config_err = BotError::Config("Config error".to_string());
620    assert!(config_err.source().is_none());
621
622    let validation_err = BotError::Validation("Validation error".to_string());
623    assert!(validation_err.source().is_none());
624
625    let system_err = BotError::System("System error".to_string());
626    assert!(system_err.source().is_none());
627}
628
629#[test]
630fn test_bot_error_debug_format() {
631    let errors = vec![
632        BotError::Api(ApiError {
633            description: "Debug API error".to_string(),
634        }),
635        BotError::Config("Debug config error".to_string()),
636        BotError::Validation("Debug validation error".to_string()),
637        BotError::System("Debug system error".to_string()),
638        BotError::Otlp(OtlpError {
639            message: "Debug OTLP error".to_string(),
640        }),
641    ];
642
643    for error in errors {
644        let debug_str = error.to_string();
645        assert!(!debug_str.is_empty());
646        assert!(debug_str.contains("Error"));
647    }
648}
649
650#[test]
651fn test_error_conversion_chain() {
652    // Test VarError conversion chain
653    let var_error = std::env::VarError::NotPresent;
654    let bot_error: BotError = var_error.into();
655    match bot_error {
656        BotError::Environment(msg) => assert!(msg.to_string().contains("not found")),
657        _ => panic!("Expected Config error"),
658    }
659
660    // Test TOML error conversion chain
661    let toml_error = toml::from_str::<i32>("invalid toml").unwrap_err();
662    let bot_error: BotError = toml_error.into();
663    match bot_error {
664        BotError::Config(_) => {} // Expected
665        _ => panic!("Expected Config error"),
666    }
667}
668
669#[tokio::test]
670async fn test_all_error_types_display() {
671    let test_cases = vec![
672        (
673            BotError::Api(ApiError {
674                description: "API test".to_string(),
675            }),
676            "API Error",
677        ),
678        (
679            BotError::Network(
680                reqwest::ClientBuilder::new()
681                    .build()
682                    .unwrap()
683                    .get("http://test")
684                    .send()
685                    .await
686                    .unwrap_err(),
687            ),
688            "Network Error",
689        ),
690        (
691            BotError::Serialization(serde_json::Error::io(std::io::Error::other("test"))),
692            "Serialization Error",
693        ),
694        (BotError::Url(url::ParseError::EmptyHost), "URL Error"),
695        (BotError::Io(std::io::Error::other("test")), "IO Error"),
696        (BotError::Config("test".to_string()), "Config Error"),
697        (BotError::Validation("test".to_string()), "Validation Error"),
698        (
699            BotError::UrlParams(serde_url_params::Error::unsupported("test")),
700            "URL Parameters Error",
701        ),
702        (BotError::System("test".to_string()), "System Error"),
703        (
704            BotError::Otlp(OtlpError {
705                message: "test".to_string(),
706            }),
707            "Otlp Error",
708        ),
709    ];
710
711    for (error, expected_prefix) in test_cases {
712        let display_str = format!("{error}");
713        assert!(
714            display_str.contains(expected_prefix),
715            "Error '{display_str}' should contain '{expected_prefix}'"
716        );
717    }
718}
719
720#[cfg(feature = "templates")]
721#[test]
722fn test_template_error_conversion() {
723    use tera::Tera;
724
725    let tera = Tera::new("templates/*").unwrap_or_default();
726    let template_error = tera
727        .render("nonexistent", &tera::Context::new())
728        .unwrap_err();
729    let bot_error: BotError = template_error.into();
730
731    match bot_error {
732        BotError::Template(_) => {} // Expected
733        _ => panic!("Expected Template error"),
734    }
735
736    let display_str = format!("{bot_error}");
737    assert!(display_str.contains("Template Error"));
738}
739
740#[test]
741fn test_result_type_alias() {
742    // Test that our Result type alias works correctly
743    fn test_function() -> Result<String> {
744        Ok("success".to_string())
745    }
746
747    fn test_error_function() -> Result<String> {
748        Err(BotError::Validation("test error".to_string()))
749    }
750
751    assert!(test_function().is_ok());
752    assert_eq!(test_function().unwrap(), "success");
753
754    assert!(test_error_function().is_err());
755    match test_error_function().unwrap_err() {
756        BotError::Validation(msg) => assert_eq!(msg, "test error"),
757        _ => panic!("Expected Validation error"),
758    }
759}
760
761#[test]
762fn test_error_message_content() {
763    // Test that error messages contain expected content
764    let api_error = ApiError {
765        description: "Specific API failure".to_string(),
766    };
767    assert!(format!("{api_error}").contains("Specific API failure"));
768
769    let otlp_error = OtlpError {
770        message: "OTLP connection timeout".to_string(),
771    };
772    assert!(format!("{otlp_error}").contains("OTLP connection timeout"));
773
774    let config_error = BotError::Config("Missing required field".to_string());
775    assert!(format!("{config_error}").contains("Missing required field"));
776}