1use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6pub const PROBLEM_JSON_CONTENT_TYPE: &str = "application/problem+json";
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct ValidationIssue {
12 pub instance_pointer: String,
14 pub schema_pointer: String,
16 pub keyword: String,
18 pub message: String,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ProblemDetails {
25 #[serde(rename = "type")]
27 pub problem_type: String,
28 pub title: String,
30 pub status: u16,
32 pub detail: String,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub instance: Option<String>,
37 #[serde(skip_serializing_if = "Vec::is_empty", default)]
39 pub errors: Vec<ValidationIssue>,
40}
41
42impl ProblemDetails {
43 #[must_use]
45 pub fn validation_error(status: u16, issues: Vec<ValidationIssue>) -> Self {
46 Self {
47 problem_type: "about:blank".to_owned(),
48 title: title_for_status(status).to_owned(),
49 status,
50 detail: "Request validation failed".to_owned(),
51 instance: None,
52 errors: issues,
53 }
54 }
55
56 #[must_use]
58 pub fn not_found(detail: &str) -> Self {
59 Self {
60 problem_type: "about:blank".to_owned(),
61 title: "Not Found".to_owned(),
62 status: 404,
63 detail: detail.to_owned(),
64 instance: None,
65 errors: Vec::new(),
66 }
67 }
68
69 #[must_use]
71 pub fn unsupported_media_type(detail: &str) -> Self {
72 Self {
73 problem_type: "about:blank".to_owned(),
74 title: "Unsupported Media Type".to_owned(),
75 status: 415,
76 detail: detail.to_owned(),
77 instance: None,
78 errors: Vec::new(),
79 }
80 }
81
82 #[must_use]
84 pub fn payload_too_large(detail: &str) -> Self {
85 Self {
86 problem_type: "about:blank".to_owned(),
87 title: "Payload Too Large".to_owned(),
88 status: 413,
89 detail: detail.to_owned(),
90 instance: None,
91 errors: Vec::new(),
92 }
93 }
94}
95
96const fn title_for_status(status: u16) -> &'static str {
98 match status {
99 400 => "Bad Request",
100 401 => "Unauthorized",
101 403 => "Forbidden",
102 404 => "Not Found",
103 405 => "Method Not Allowed",
104 406 => "Not Acceptable",
105 409 => "Conflict",
106 413 => "Payload Too Large",
107 415 => "Unsupported Media Type",
108 422 => "Unprocessable Entity",
109 429 => "Too Many Requests",
110 500 => "Internal Server Error",
111 502 => "Bad Gateway",
112 503 => "Service Unavailable",
113 _ => "Unknown Error",
114 }
115}
116
117#[derive(Debug, Error)]
119pub enum SpecMockCoreError {
120 #[error("schema compilation failed: {0}")]
122 Schema(String),
123 #[error("data generation failed: {0}")]
125 Faker(String),
126 #[error("$ref resolution failed: {0}")]
128 Ref(String),
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn validation_error_serialization_roundtrip() {
137 let issues = vec![ValidationIssue {
138 instance_pointer: "/name".to_owned(),
139 schema_pointer: "/properties/name/minLength".to_owned(),
140 keyword: "minLength".to_owned(),
141 message: "must be at least 1 character".to_owned(),
142 }];
143
144 let problem = ProblemDetails::validation_error(400, issues);
145 let json = serde_json::to_string(&problem).expect("serialize");
146 let roundtrip: ProblemDetails = serde_json::from_str(&json).expect("deserialize");
147
148 assert_eq!(roundtrip.status, 400);
149 assert_eq!(roundtrip.title, "Bad Request");
150 assert_eq!(roundtrip.problem_type, "about:blank");
151 assert_eq!(roundtrip.detail, "Request validation failed");
152 assert!(roundtrip.instance.is_none());
153 assert_eq!(roundtrip.errors.len(), 1);
154 assert_eq!(roundtrip.errors[0].instance_pointer, "/name");
155 }
156
157 #[test]
158 fn validation_error_json_shape() {
159 let problem = ProblemDetails::validation_error(422, vec![]);
160 let value = serde_json::to_value(&problem).expect("to_value");
161
162 assert_eq!(value["type"], "about:blank");
164 assert_eq!(value["status"], 422);
165 assert_eq!(value["title"], "Unprocessable Entity");
166 assert!(value.get("errors").is_none());
168 }
169
170 #[test]
171 fn not_found_has_correct_fields() {
172 let problem = ProblemDetails::not_found("no such path: /pets/99");
173 assert_eq!(problem.status, 404);
174 assert_eq!(problem.title, "Not Found");
175 assert_eq!(problem.detail, "no such path: /pets/99");
176 assert!(problem.errors.is_empty());
177 }
178
179 #[test]
180 fn unsupported_media_type_has_correct_fields() {
181 let problem = ProblemDetails::unsupported_media_type("expected application/json");
182 assert_eq!(problem.status, 415);
183 assert_eq!(problem.title, "Unsupported Media Type");
184 }
185
186 #[test]
187 fn payload_too_large_has_correct_fields() {
188 let problem = ProblemDetails::payload_too_large("body exceeds 1 MB");
189 assert_eq!(problem.status, 413);
190 assert_eq!(problem.title, "Payload Too Large");
191 }
192
193 #[test]
194 fn deserialize_without_optional_fields() {
195 let json = r#"{"type":"about:blank","title":"Not Found","status":404,"detail":"gone"}"#;
196 let problem: ProblemDetails = serde_json::from_str(json).expect("deserialize");
197 assert_eq!(problem.status, 404);
198 assert!(problem.instance.is_none());
199 assert!(problem.errors.is_empty());
200 }
201
202 #[test]
203 fn content_type_constant() {
204 assert_eq!(PROBLEM_JSON_CONTENT_TYPE, "application/problem+json");
205 }
206}