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                ErrorCode::odata_errors_invalid_cursor_v1().as_problem("invalid cursor")
32            }
33
34            CursorInvalidBase64 => ErrorCode::odata_errors_invalid_cursor_v1()
35                .as_problem("invalid cursor: invalid base64url encoding"),
36
37            CursorInvalidJson => ErrorCode::odata_errors_invalid_cursor_v1()
38                .as_problem("invalid cursor: malformed JSON"),
39
40            CursorInvalidVersion => ErrorCode::odata_errors_invalid_cursor_v1()
41                .as_problem("invalid cursor: unsupported version"),
42
43            CursorInvalidKeys => ErrorCode::odata_errors_invalid_cursor_v1()
44                .as_problem("invalid cursor: empty or invalid keys"),
45
46            CursorInvalidFields => ErrorCode::odata_errors_invalid_cursor_v1()
47                .as_problem("invalid cursor: empty or invalid fields"),
48
49            CursorInvalidDirection => ErrorCode::odata_errors_invalid_cursor_v1()
50                .as_problem("invalid cursor: invalid sort direction"),
51
52            // Pagination validation errors → 422
53            OrderMismatch => ErrorCode::odata_errors_invalid_orderby_v1()
54                .as_problem("Order mismatch between cursor and query"),
55
56            FilterMismatch => ErrorCode::odata_errors_invalid_filter_v1()
57                .as_problem("Filter mismatch between cursor and query"),
58
59            InvalidLimit => {
60                ErrorCode::odata_errors_invalid_filter_v1().as_problem("Invalid limit parameter")
61            }
62
63            OrderWithCursor => ErrorCode::odata_errors_invalid_cursor_v1()
64                .as_problem("Cannot specify both $orderby and cursor parameters"),
65
66            // Database errors → 500 (should be caught earlier)
67            Db(_msg) => {
68                // Use filter error as safe default for unexpected DB errors
69                ErrorCode::odata_errors_internal_v1()
70                    .as_problem("An internal error occurred while processing the OData query")
71            }
72
73            // Configuration errors → 500 (feature not enabled)
74            ParsingUnavailable(msg) => ErrorCode::odata_errors_internal_v1()
75                .as_problem(format!("OData parsing unavailable: {msg}")),
76        }
77    }
78}
79
80#[cfg(test)]
81#[cfg_attr(coverage_nightly, coverage(off))]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn test_filter_error_converts_to_problem() {
87        use http::StatusCode;
88
89        let err = Error::InvalidFilter("malformed".to_owned());
90        let problem: Problem = err.into();
91
92        assert_eq!(problem.status, StatusCode::UNPROCESSABLE_ENTITY);
93        assert_eq!(problem.title, "Invalid Filter");
94        assert!(problem.detail.contains("malformed"));
95        assert!(problem.code.contains("odata"));
96        assert!(problem.code.contains("invalid_filter"));
97    }
98
99    #[test]
100    fn test_orderby_error_converts_to_problem() {
101        use http::StatusCode;
102
103        let err = Error::InvalidOrderByField("unknown".to_owned());
104        let problem: Problem = err.into();
105
106        assert_eq!(problem.status, StatusCode::UNPROCESSABLE_ENTITY);
107        assert_eq!(problem.title, "Invalid OrderBy");
108        assert!(problem.code.contains("odata"));
109        assert!(problem.code.contains("invalid_orderby"));
110    }
111
112    #[test]
113    fn test_cursor_error_converts_to_problem() {
114        use http::StatusCode;
115
116        let err = Error::CursorInvalidBase64;
117        let problem: Problem = err.into();
118
119        assert_eq!(problem.status, StatusCode::UNPROCESSABLE_ENTITY);
120        assert_eq!(problem.title, "Invalid Cursor");
121        assert!(problem.code.contains("odata"));
122        assert!(problem.code.contains("invalid_cursor"));
123    }
124}