Skip to main content

modkit_odata/
problem_mapping.rs

1//! Mapping from `OData` errors to Problem (pure data)
2//!
3//! This provides a baseline conversion from `OData` errors to RFC 9457 Problem
4//! without HTTP framework dependencies. The HTTP layer in `modkit` adds
5//! instance paths and trace IDs before the Problem is converted to an HTTP response.
6
7use crate::Error;
8use crate::errors::ErrorCode;
9use modkit_errors::problem::Problem;
10
11impl From<Error> for Problem {
12    fn from(err: Error) -> Self {
13        use Error::{
14            CursorInvalidBase64, CursorInvalidDirection, CursorInvalidFields, CursorInvalidJson,
15            CursorInvalidKeys, CursorInvalidVersion, Db, FilterMismatch, InvalidCursor,
16            InvalidFilter, InvalidLimit, InvalidOrderByField, OrderMismatch, OrderWithCursor,
17            ParsingUnavailable,
18        };
19
20        match err {
21            // Filter parsing errors → 422
22            InvalidFilter(msg) => ErrorCode::odata_errors_invalid_filter_v1()
23                .as_problem(format!("Invalid $filter: {msg}")),
24
25            // OrderBy parsing and validation errors → 422
26            InvalidOrderByField(field) => ErrorCode::odata_errors_invalid_orderby_v1()
27                .as_problem(format!("Unsupported $orderby field: {field}")),
28
29            // All cursor-related errors → 422
30            InvalidCursor
31            | CursorInvalidBase64
32            | CursorInvalidJson
33            | CursorInvalidVersion
34            | CursorInvalidKeys
35            | CursorInvalidFields
36            | CursorInvalidDirection => {
37                ErrorCode::odata_errors_invalid_cursor_v1().as_problem(err.to_string())
38            }
39
40            // Pagination validation errors → 422
41            OrderMismatch => ErrorCode::odata_errors_invalid_orderby_v1()
42                .as_problem("Order mismatch between cursor and query"),
43
44            FilterMismatch => ErrorCode::odata_errors_invalid_filter_v1()
45                .as_problem("Filter mismatch between cursor and query"),
46
47            InvalidLimit => {
48                ErrorCode::odata_errors_invalid_filter_v1().as_problem("Invalid limit parameter")
49            }
50
51            OrderWithCursor => ErrorCode::odata_errors_invalid_cursor_v1()
52                .as_problem("Cannot specify both $orderby and cursor parameters"),
53
54            // Database errors → 500 (should be caught earlier)
55            Db(_msg) => {
56                // Use filter error as safe default for unexpected DB errors
57                ErrorCode::odata_errors_internal_v1()
58                    .as_problem("An internal error occurred while processing the OData query")
59            }
60
61            // Configuration errors → 500 (feature not enabled)
62            ParsingUnavailable(msg) => ErrorCode::odata_errors_internal_v1()
63                .as_problem(format!("OData parsing unavailable: {msg}")),
64        }
65    }
66}
67
68#[cfg(test)]
69#[cfg_attr(coverage_nightly, coverage(off))]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn test_filter_error_converts_to_problem() {
75        use http::StatusCode;
76
77        let err = Error::InvalidFilter("malformed".to_owned());
78        let problem: Problem = err.into();
79
80        assert_eq!(problem.status, StatusCode::UNPROCESSABLE_ENTITY);
81        assert_eq!(problem.title, "Invalid Filter");
82        assert!(problem.detail.contains("malformed"));
83        assert!(problem.code.contains("odata"));
84        assert!(problem.code.contains("invalid_filter"));
85    }
86
87    #[test]
88    fn test_orderby_error_converts_to_problem() {
89        use http::StatusCode;
90
91        let err = Error::InvalidOrderByField("unknown".to_owned());
92        let problem: Problem = err.into();
93
94        assert_eq!(problem.status, StatusCode::UNPROCESSABLE_ENTITY);
95        assert_eq!(problem.title, "Invalid OrderBy");
96        assert!(problem.code.contains("odata"));
97        assert!(problem.code.contains("invalid_orderby"));
98    }
99
100    #[test]
101    fn test_cursor_error_converts_to_problem() {
102        use http::StatusCode;
103
104        let err = Error::CursorInvalidBase64;
105        let problem: Problem = err.into();
106
107        assert_eq!(problem.status, StatusCode::UNPROCESSABLE_ENTITY);
108        assert_eq!(problem.title, "Invalid Cursor");
109        assert!(problem.code.contains("odata"));
110        assert!(problem.code.contains("invalid_cursor"));
111    }
112}