spikard 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +674 -674
- data/ext/spikard_rb/Cargo.toml +17 -17
- data/ext/spikard_rb/extconf.rb +13 -10
- data/ext/spikard_rb/src/lib.rs +6 -6
- data/lib/spikard/app.rb +405 -405
- data/lib/spikard/background.rb +27 -27
- data/lib/spikard/config.rb +396 -396
- data/lib/spikard/converters.rb +13 -13
- data/lib/spikard/handler_wrapper.rb +113 -113
- data/lib/spikard/provide.rb +214 -214
- data/lib/spikard/response.rb +173 -173
- data/lib/spikard/schema.rb +243 -243
- data/lib/spikard/sse.rb +111 -111
- data/lib/spikard/streaming_response.rb +44 -44
- data/lib/spikard/testing.rb +256 -256
- data/lib/spikard/upload_file.rb +131 -131
- data/lib/spikard/version.rb +5 -5
- data/lib/spikard/websocket.rb +59 -59
- data/lib/spikard.rb +43 -43
- data/sig/spikard.rbs +366 -366
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -63
- data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -132
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -752
- data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -194
- data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -246
- data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -401
- data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -238
- data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -24
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -292
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -616
- data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -305
- data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -248
- data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -351
- data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -454
- data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -383
- data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -280
- data/vendor/crates/spikard-core/Cargo.toml +40 -40
- data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -3
- data/vendor/crates/spikard-core/src/bindings/response.rs +133 -133
- data/vendor/crates/spikard-core/src/debug.rs +127 -127
- data/vendor/crates/spikard-core/src/di/container.rs +702 -702
- data/vendor/crates/spikard-core/src/di/dependency.rs +273 -273
- data/vendor/crates/spikard-core/src/di/error.rs +118 -118
- data/vendor/crates/spikard-core/src/di/factory.rs +534 -534
- data/vendor/crates/spikard-core/src/di/graph.rs +506 -506
- data/vendor/crates/spikard-core/src/di/mod.rs +192 -192
- data/vendor/crates/spikard-core/src/di/resolved.rs +405 -405
- data/vendor/crates/spikard-core/src/di/value.rs +281 -281
- data/vendor/crates/spikard-core/src/errors.rs +69 -69
- data/vendor/crates/spikard-core/src/http.rs +415 -415
- data/vendor/crates/spikard-core/src/lib.rs +29 -29
- data/vendor/crates/spikard-core/src/lifecycle.rs +1186 -1186
- data/vendor/crates/spikard-core/src/metadata.rs +389 -389
- data/vendor/crates/spikard-core/src/parameters.rs +2525 -2525
- data/vendor/crates/spikard-core/src/problem.rs +344 -344
- data/vendor/crates/spikard-core/src/request_data.rs +1154 -1154
- data/vendor/crates/spikard-core/src/router.rs +510 -510
- data/vendor/crates/spikard-core/src/schema_registry.rs +183 -183
- data/vendor/crates/spikard-core/src/type_hints.rs +304 -304
- data/vendor/crates/spikard-core/src/validation/error_mapper.rs +696 -688
- data/vendor/crates/spikard-core/src/validation/mod.rs +457 -457
- data/vendor/crates/spikard-http/Cargo.toml +62 -64
- data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -148
- data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -92
- data/vendor/crates/spikard-http/src/auth.rs +296 -296
- data/vendor/crates/spikard-http/src/background.rs +1860 -1860
- data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -3
- data/vendor/crates/spikard-http/src/bindings/response.rs +1 -1
- data/vendor/crates/spikard-http/src/body_metadata.rs +8 -8
- data/vendor/crates/spikard-http/src/cors.rs +1005 -1005
- data/vendor/crates/spikard-http/src/debug.rs +128 -128
- data/vendor/crates/spikard-http/src/di_handler.rs +1668 -1668
- data/vendor/crates/spikard-http/src/handler_response.rs +901 -901
- data/vendor/crates/spikard-http/src/handler_trait.rs +838 -830
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +290 -290
- data/vendor/crates/spikard-http/src/lib.rs +534 -534
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +230 -230
- data/vendor/crates/spikard-http/src/lifecycle.rs +1193 -1193
- data/vendor/crates/spikard-http/src/middleware/mod.rs +560 -540
- data/vendor/crates/spikard-http/src/middleware/multipart.rs +912 -912
- data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +513 -513
- data/vendor/crates/spikard-http/src/middleware/validation.rs +768 -735
- data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -309
- data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +535 -535
- data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1363 -1363
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +665 -665
- data/vendor/crates/spikard-http/src/query_parser.rs +793 -793
- data/vendor/crates/spikard-http/src/response.rs +720 -720
- data/vendor/crates/spikard-http/src/server/handler.rs +1650 -1650
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +234 -234
- data/vendor/crates/spikard-http/src/server/mod.rs +1593 -1502
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +789 -770
- data/vendor/crates/spikard-http/src/server/routing_factory.rs +629 -599
- data/vendor/crates/spikard-http/src/sse.rs +1409 -1409
- data/vendor/crates/spikard-http/src/testing/form.rs +52 -52
- data/vendor/crates/spikard-http/src/testing/multipart.rs +64 -60
- data/vendor/crates/spikard-http/src/testing/test_client.rs +311 -283
- data/vendor/crates/spikard-http/src/testing.rs +406 -377
- data/vendor/crates/spikard-http/src/websocket.rs +1404 -1375
- data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -832
- data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -309
- data/vendor/crates/spikard-http/tests/common/mod.rs +26 -26
- data/vendor/crates/spikard-http/tests/di_integration.rs +192 -192
- data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -5
- data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -1093
- data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -656
- data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -314
- data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -620
- data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -663
- data/vendor/crates/spikard-rb/Cargo.toml +48 -48
- data/vendor/crates/spikard-rb/build.rs +199 -199
- data/vendor/crates/spikard-rb/src/background.rs +63 -63
- data/vendor/crates/spikard-rb/src/config/mod.rs +5 -5
- data/vendor/crates/spikard-rb/src/config/server_config.rs +285 -285
- data/vendor/crates/spikard-rb/src/conversion.rs +554 -554
- data/vendor/crates/spikard-rb/src/di/builder.rs +100 -100
- data/vendor/crates/spikard-rb/src/di/mod.rs +375 -375
- data/vendor/crates/spikard-rb/src/handler.rs +618 -618
- data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -3
- data/vendor/crates/spikard-rb/src/lib.rs +1806 -1810
- data/vendor/crates/spikard-rb/src/lifecycle.rs +275 -275
- data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -5
- data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +442 -447
- data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -5
- data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -324
- data/vendor/crates/spikard-rb/src/server.rs +305 -308
- data/vendor/crates/spikard-rb/src/sse.rs +231 -231
- data/vendor/crates/spikard-rb/src/testing/client.rs +538 -551
- data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -7
- data/vendor/crates/spikard-rb/src/testing/sse.rs +143 -143
- data/vendor/crates/spikard-rb/src/testing/websocket.rs +608 -635
- data/vendor/crates/spikard-rb/src/websocket.rs +377 -374
- metadata +15 -1
|
@@ -1,401 +1,401 @@
|
|
|
1
|
-
//! Shared error response formatting
|
|
2
|
-
//!
|
|
3
|
-
//! This module consolidates error response building across all language bindings,
|
|
4
|
-
//! eliminating duplicate `structured_error()` functions in Node, Ruby, and PHP bindings.
|
|
5
|
-
|
|
6
|
-
use axum::http::StatusCode;
|
|
7
|
-
use serde_json::Value;
|
|
8
|
-
use spikard_core::errors::StructuredError;
|
|
9
|
-
use spikard_core::problem::ProblemDetails;
|
|
10
|
-
use spikard_core::validation::ValidationError;
|
|
11
|
-
|
|
12
|
-
/// Builder for creating standardized error responses across all bindings
|
|
13
|
-
pub struct ErrorResponseBuilder;
|
|
14
|
-
|
|
15
|
-
impl ErrorResponseBuilder {
|
|
16
|
-
/// Create a structured error response with status code and error details
|
|
17
|
-
///
|
|
18
|
-
/// Returns a tuple of (StatusCode, JSON body as String)
|
|
19
|
-
///
|
|
20
|
-
/// # Arguments
|
|
21
|
-
/// * `status` - HTTP status code
|
|
22
|
-
/// * `code` - Machine-readable error code
|
|
23
|
-
/// * `message` - Human-readable error message
|
|
24
|
-
///
|
|
25
|
-
/// # Example
|
|
26
|
-
/// ```
|
|
27
|
-
/// use axum::http::StatusCode;
|
|
28
|
-
/// use spikard_bindings_shared::ErrorResponseBuilder;
|
|
29
|
-
///
|
|
30
|
-
/// let (status, body) = ErrorResponseBuilder::structured_error(
|
|
31
|
-
/// StatusCode::BAD_REQUEST,
|
|
32
|
-
/// "invalid_input",
|
|
33
|
-
/// "Missing required field",
|
|
34
|
-
/// );
|
|
35
|
-
/// ```
|
|
36
|
-
pub fn structured_error(status: StatusCode, code: &str, message: impl Into<String>) -> (StatusCode, String) {
|
|
37
|
-
let payload = StructuredError::simple(code.to_string(), message.into());
|
|
38
|
-
let body = serde_json::to_string(&payload)
|
|
39
|
-
.unwrap_or_else(|_| r#"{"error":"serialization_failed","code":"internal_error","details":{}}"#.to_string());
|
|
40
|
-
(status, body)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/// Create an error response with additional details
|
|
44
|
-
///
|
|
45
|
-
/// Returns a tuple of (StatusCode, JSON body as String)
|
|
46
|
-
///
|
|
47
|
-
/// # Arguments
|
|
48
|
-
/// * `status` - HTTP status code
|
|
49
|
-
/// * `code` - Machine-readable error code
|
|
50
|
-
/// * `message` - Human-readable error message
|
|
51
|
-
/// * `details` - Structured details about the error
|
|
52
|
-
///
|
|
53
|
-
/// # Example
|
|
54
|
-
/// ```
|
|
55
|
-
/// use axum::http::StatusCode;
|
|
56
|
-
/// use serde_json::json;
|
|
57
|
-
/// use spikard_bindings_shared::ErrorResponseBuilder;
|
|
58
|
-
///
|
|
59
|
-
/// let details = json!({
|
|
60
|
-
/// "field": "email",
|
|
61
|
-
/// "reason": "invalid_format"
|
|
62
|
-
/// });
|
|
63
|
-
/// let (status, body) = ErrorResponseBuilder::with_details(
|
|
64
|
-
/// StatusCode::BAD_REQUEST,
|
|
65
|
-
/// "validation_error",
|
|
66
|
-
/// "Invalid email format",
|
|
67
|
-
/// details,
|
|
68
|
-
/// );
|
|
69
|
-
/// ```
|
|
70
|
-
pub fn with_details(
|
|
71
|
-
status: StatusCode,
|
|
72
|
-
code: &str,
|
|
73
|
-
message: impl Into<String>,
|
|
74
|
-
details: Value,
|
|
75
|
-
) -> (StatusCode, String) {
|
|
76
|
-
let payload = StructuredError::new(code.to_string(), message.into(), details);
|
|
77
|
-
let body = serde_json::to_string(&payload)
|
|
78
|
-
.unwrap_or_else(|_| r#"{"error":"serialization_failed","code":"internal_error","details":{}}"#.to_string());
|
|
79
|
-
(status, body)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/// Create an error response from a StructuredError
|
|
83
|
-
///
|
|
84
|
-
/// Returns a tuple of (StatusCode, JSON body as String)
|
|
85
|
-
///
|
|
86
|
-
/// # Arguments
|
|
87
|
-
/// * `error` - The structured error
|
|
88
|
-
///
|
|
89
|
-
/// # Note
|
|
90
|
-
/// Uses INTERNAL_SERVER_ERROR as the default status code. Override with
|
|
91
|
-
/// `structured_error()` or `with_details()` for specific status codes.
|
|
92
|
-
pub fn from_structured_error(error: StructuredError) -> (StatusCode, String) {
|
|
93
|
-
let status = StatusCode::INTERNAL_SERVER_ERROR;
|
|
94
|
-
let body = serde_json::to_string(&error)
|
|
95
|
-
.unwrap_or_else(|_| r#"{"error":"serialization_failed","code":"internal_error","details":{}}"#.to_string());
|
|
96
|
-
(status, body)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/// Create a validation error response
|
|
100
|
-
///
|
|
101
|
-
/// Converts ValidationError to RFC 9457 Problem Details format.
|
|
102
|
-
/// Returns (StatusCode::UNPROCESSABLE_ENTITY, JSON body)
|
|
103
|
-
///
|
|
104
|
-
/// # Arguments
|
|
105
|
-
/// * `validation_error` - The validation error containing one or more details
|
|
106
|
-
///
|
|
107
|
-
/// # Example
|
|
108
|
-
/// ```
|
|
109
|
-
/// use spikard_core::validation::{ValidationError, ValidationErrorDetail};
|
|
110
|
-
/// use serde_json::Value;
|
|
111
|
-
/// use spikard_bindings_shared::ErrorResponseBuilder;
|
|
112
|
-
///
|
|
113
|
-
/// let validation_error = ValidationError {
|
|
114
|
-
/// errors: vec![
|
|
115
|
-
/// ValidationErrorDetail {
|
|
116
|
-
/// error_type: "missing".to_string(),
|
|
117
|
-
/// loc: vec!["body".to_string(), "username".to_string()],
|
|
118
|
-
/// msg: "Field required".to_string(),
|
|
119
|
-
/// input: Value::String("".to_string()),
|
|
120
|
-
/// ctx: None,
|
|
121
|
-
/// },
|
|
122
|
-
/// ],
|
|
123
|
-
/// };
|
|
124
|
-
///
|
|
125
|
-
/// let (status, body) = ErrorResponseBuilder::validation_error(&validation_error);
|
|
126
|
-
/// ```
|
|
127
|
-
pub fn validation_error(validation_error: &ValidationError) -> (StatusCode, String) {
|
|
128
|
-
let problem = ProblemDetails::from_validation_error(validation_error);
|
|
129
|
-
let status = problem.status_code();
|
|
130
|
-
let body = serde_json::to_string(&problem).unwrap_or_else(|_| {
|
|
131
|
-
r#"{"title":"Validation Failed","type":"https://spikard.dev/errors/validation-error","status":422}"#
|
|
132
|
-
.to_string()
|
|
133
|
-
});
|
|
134
|
-
(status, body)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/// Create an RFC 9457 Problem Details response
|
|
138
|
-
///
|
|
139
|
-
/// Returns a tuple of (StatusCode, JSON body as String)
|
|
140
|
-
///
|
|
141
|
-
/// # Arguments
|
|
142
|
-
/// * `problem` - The Problem Details object
|
|
143
|
-
///
|
|
144
|
-
/// # Example
|
|
145
|
-
/// ```
|
|
146
|
-
/// use axum::http::StatusCode;
|
|
147
|
-
/// use spikard_core::problem::ProblemDetails;
|
|
148
|
-
/// use spikard_bindings_shared::ErrorResponseBuilder;
|
|
149
|
-
///
|
|
150
|
-
/// let problem = ProblemDetails::not_found("User with id 123 not found");
|
|
151
|
-
/// let (status, body) = ErrorResponseBuilder::problem_details_response(&problem);
|
|
152
|
-
/// ```
|
|
153
|
-
pub fn problem_details_response(problem: &ProblemDetails) -> (StatusCode, String) {
|
|
154
|
-
let status = problem.status_code();
|
|
155
|
-
let body = serde_json::to_string(problem).unwrap_or_else(|_| {
|
|
156
|
-
r#"{"title":"Internal Server Error","type":"https://spikard.dev/errors/internal-server-error","status":500}"#
|
|
157
|
-
.to_string()
|
|
158
|
-
});
|
|
159
|
-
(status, body)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/// Create a generic bad request error
|
|
163
|
-
///
|
|
164
|
-
/// Returns (StatusCode::BAD_REQUEST, JSON body)
|
|
165
|
-
pub fn bad_request(message: impl Into<String>) -> (StatusCode, String) {
|
|
166
|
-
Self::structured_error(StatusCode::BAD_REQUEST, "bad_request", message)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/// Create a generic internal server error
|
|
170
|
-
///
|
|
171
|
-
/// Returns (StatusCode::INTERNAL_SERVER_ERROR, JSON body)
|
|
172
|
-
pub fn internal_error(message: impl Into<String>) -> (StatusCode, String) {
|
|
173
|
-
Self::structured_error(StatusCode::INTERNAL_SERVER_ERROR, "internal_error", message)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/// Create an unauthorized error
|
|
177
|
-
///
|
|
178
|
-
/// Returns (StatusCode::UNAUTHORIZED, JSON body)
|
|
179
|
-
pub fn unauthorized(message: impl Into<String>) -> (StatusCode, String) {
|
|
180
|
-
Self::structured_error(StatusCode::UNAUTHORIZED, "unauthorized", message)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/// Create a forbidden error
|
|
184
|
-
///
|
|
185
|
-
/// Returns (StatusCode::FORBIDDEN, JSON body)
|
|
186
|
-
pub fn forbidden(message: impl Into<String>) -> (StatusCode, String) {
|
|
187
|
-
Self::structured_error(StatusCode::FORBIDDEN, "forbidden", message)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/// Create a not found error
|
|
191
|
-
///
|
|
192
|
-
/// Returns (StatusCode::NOT_FOUND, JSON body)
|
|
193
|
-
pub fn not_found(message: impl Into<String>) -> (StatusCode, String) {
|
|
194
|
-
Self::structured_error(StatusCode::NOT_FOUND, "not_found", message)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/// Create a method not allowed error
|
|
198
|
-
///
|
|
199
|
-
/// Returns (StatusCode::METHOD_NOT_ALLOWED, JSON body)
|
|
200
|
-
pub fn method_not_allowed(message: impl Into<String>) -> (StatusCode, String) {
|
|
201
|
-
Self::structured_error(StatusCode::METHOD_NOT_ALLOWED, "method_not_allowed", message)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/// Create an unprocessable entity error (validation failed)
|
|
205
|
-
///
|
|
206
|
-
/// Returns (StatusCode::UNPROCESSABLE_ENTITY, JSON body)
|
|
207
|
-
pub fn unprocessable_entity(message: impl Into<String>) -> (StatusCode, String) {
|
|
208
|
-
Self::structured_error(StatusCode::UNPROCESSABLE_ENTITY, "unprocessable_entity", message)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/// Create a conflict error
|
|
212
|
-
///
|
|
213
|
-
/// Returns (StatusCode::CONFLICT, JSON body)
|
|
214
|
-
pub fn conflict(message: impl Into<String>) -> (StatusCode, String) {
|
|
215
|
-
Self::structured_error(StatusCode::CONFLICT, "conflict", message)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/// Create a service unavailable error
|
|
219
|
-
///
|
|
220
|
-
/// Returns (StatusCode::SERVICE_UNAVAILABLE, JSON body)
|
|
221
|
-
pub fn service_unavailable(message: impl Into<String>) -> (StatusCode, String) {
|
|
222
|
-
Self::structured_error(StatusCode::SERVICE_UNAVAILABLE, "service_unavailable", message)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/// Create a request timeout error
|
|
226
|
-
///
|
|
227
|
-
/// Returns (StatusCode::REQUEST_TIMEOUT, JSON body)
|
|
228
|
-
pub fn request_timeout(message: impl Into<String>) -> (StatusCode, String) {
|
|
229
|
-
Self::structured_error(StatusCode::REQUEST_TIMEOUT, "request_timeout", message)
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
#[cfg(test)]
|
|
234
|
-
mod tests {
|
|
235
|
-
use super::*;
|
|
236
|
-
use serde_json::json;
|
|
237
|
-
use spikard_core::validation::ValidationErrorDetail;
|
|
238
|
-
|
|
239
|
-
#[test]
|
|
240
|
-
fn test_structured_error() {
|
|
241
|
-
let (status, body) =
|
|
242
|
-
ErrorResponseBuilder::structured_error(StatusCode::BAD_REQUEST, "invalid_input", "Missing required field");
|
|
243
|
-
assert_eq!(status, StatusCode::BAD_REQUEST);
|
|
244
|
-
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
245
|
-
assert_eq!(parsed["error"], "Missing required field");
|
|
246
|
-
assert_eq!(parsed["code"], "invalid_input");
|
|
247
|
-
assert!(parsed["details"].is_object());
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
#[test]
|
|
251
|
-
fn test_with_details() {
|
|
252
|
-
let details = json!({
|
|
253
|
-
"field": "email",
|
|
254
|
-
"reason": "invalid_format"
|
|
255
|
-
});
|
|
256
|
-
let (status, body) = ErrorResponseBuilder::with_details(
|
|
257
|
-
StatusCode::BAD_REQUEST,
|
|
258
|
-
"validation_error",
|
|
259
|
-
"Invalid email format",
|
|
260
|
-
details.clone(),
|
|
261
|
-
);
|
|
262
|
-
assert_eq!(status, StatusCode::BAD_REQUEST);
|
|
263
|
-
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
264
|
-
assert_eq!(parsed["code"], "validation_error");
|
|
265
|
-
assert_eq!(parsed["error"], "Invalid email format");
|
|
266
|
-
assert_eq!(parsed["details"]["field"], "email");
|
|
267
|
-
assert_eq!(parsed["details"]["reason"], "invalid_format");
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
#[test]
|
|
271
|
-
fn test_from_structured_error() {
|
|
272
|
-
let error = StructuredError::simple("test_error", "Something went wrong");
|
|
273
|
-
let (status, body) = ErrorResponseBuilder::from_structured_error(error);
|
|
274
|
-
assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
|
|
275
|
-
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
276
|
-
assert_eq!(parsed["code"], "test_error");
|
|
277
|
-
assert_eq!(parsed["error"], "Something went wrong");
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
#[test]
|
|
281
|
-
fn test_validation_error() {
|
|
282
|
-
let validation_error = ValidationError {
|
|
283
|
-
errors: vec![ValidationErrorDetail {
|
|
284
|
-
error_type: "missing".to_string(),
|
|
285
|
-
loc: vec!["body".to_string(), "username".to_string()],
|
|
286
|
-
msg: "Field required".to_string(),
|
|
287
|
-
input: Value::String("".to_string()),
|
|
288
|
-
ctx: None,
|
|
289
|
-
}],
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
let (status, body) = ErrorResponseBuilder::validation_error(&validation_error);
|
|
293
|
-
assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
|
|
294
|
-
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
295
|
-
assert_eq!(parsed["title"], "Request Validation Failed");
|
|
296
|
-
assert_eq!(parsed["status"], 422);
|
|
297
|
-
assert!(parsed["errors"].is_array());
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
#[test]
|
|
301
|
-
fn test_problem_details_response() {
|
|
302
|
-
let problem = ProblemDetails::not_found("User with id 123 not found");
|
|
303
|
-
let (status, body) = ErrorResponseBuilder::problem_details_response(&problem);
|
|
304
|
-
assert_eq!(status, StatusCode::NOT_FOUND);
|
|
305
|
-
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
306
|
-
assert_eq!(parsed["type"], "https://spikard.dev/errors/not-found");
|
|
307
|
-
assert_eq!(parsed["title"], "Resource Not Found");
|
|
308
|
-
assert_eq!(parsed["status"], 404);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
#[test]
|
|
312
|
-
fn test_bad_request() {
|
|
313
|
-
let (status, body) = ErrorResponseBuilder::bad_request("Invalid data");
|
|
314
|
-
assert_eq!(status, StatusCode::BAD_REQUEST);
|
|
315
|
-
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
316
|
-
assert_eq!(parsed["code"], "bad_request");
|
|
317
|
-
assert_eq!(parsed["error"], "Invalid data");
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
#[test]
|
|
321
|
-
fn test_internal_error() {
|
|
322
|
-
let (status, body) = ErrorResponseBuilder::internal_error("Something went wrong");
|
|
323
|
-
assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
|
|
324
|
-
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
325
|
-
assert_eq!(parsed["code"], "internal_error");
|
|
326
|
-
assert_eq!(parsed["error"], "Something went wrong");
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
#[test]
|
|
330
|
-
fn test_unauthorized() {
|
|
331
|
-
let (status, body) = ErrorResponseBuilder::unauthorized("Authentication required");
|
|
332
|
-
assert_eq!(status, StatusCode::UNAUTHORIZED);
|
|
333
|
-
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
334
|
-
assert_eq!(parsed["code"], "unauthorized");
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
#[test]
|
|
338
|
-
fn test_forbidden() {
|
|
339
|
-
let (status, body) = ErrorResponseBuilder::forbidden("Access denied");
|
|
340
|
-
assert_eq!(status, StatusCode::FORBIDDEN);
|
|
341
|
-
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
342
|
-
assert_eq!(parsed["code"], "forbidden");
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
#[test]
|
|
346
|
-
fn test_not_found() {
|
|
347
|
-
let (status, body) = ErrorResponseBuilder::not_found("Resource not found");
|
|
348
|
-
assert_eq!(status, StatusCode::NOT_FOUND);
|
|
349
|
-
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
350
|
-
assert_eq!(parsed["code"], "not_found");
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
#[test]
|
|
354
|
-
fn test_method_not_allowed() {
|
|
355
|
-
let (status, body) = ErrorResponseBuilder::method_not_allowed("Method POST not allowed");
|
|
356
|
-
assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED);
|
|
357
|
-
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
358
|
-
assert_eq!(parsed["code"], "method_not_allowed");
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
#[test]
|
|
362
|
-
fn test_unprocessable_entity() {
|
|
363
|
-
let (status, body) = ErrorResponseBuilder::unprocessable_entity("Validation failed");
|
|
364
|
-
assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
|
|
365
|
-
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
366
|
-
assert_eq!(parsed["code"], "unprocessable_entity");
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
#[test]
|
|
370
|
-
fn test_conflict() {
|
|
371
|
-
let (status, body) = ErrorResponseBuilder::conflict("Resource already exists");
|
|
372
|
-
assert_eq!(status, StatusCode::CONFLICT);
|
|
373
|
-
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
374
|
-
assert_eq!(parsed["code"], "conflict");
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
#[test]
|
|
378
|
-
fn test_service_unavailable() {
|
|
379
|
-
let (status, body) = ErrorResponseBuilder::service_unavailable("Service temporarily unavailable");
|
|
380
|
-
assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);
|
|
381
|
-
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
382
|
-
assert_eq!(parsed["code"], "service_unavailable");
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
#[test]
|
|
386
|
-
fn test_request_timeout() {
|
|
387
|
-
let (status, body) = ErrorResponseBuilder::request_timeout("Request timed out");
|
|
388
|
-
assert_eq!(status, StatusCode::REQUEST_TIMEOUT);
|
|
389
|
-
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
390
|
-
assert_eq!(parsed["code"], "request_timeout");
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
#[test]
|
|
394
|
-
fn test_serialization_fallback() {
|
|
395
|
-
let details = serde_json::Map::new();
|
|
396
|
-
let (_status, body) =
|
|
397
|
-
ErrorResponseBuilder::with_details(StatusCode::BAD_REQUEST, "test", "Test error", Value::Object(details));
|
|
398
|
-
|
|
399
|
-
assert!(serde_json::from_str::<Value>(&body).is_ok());
|
|
400
|
-
}
|
|
401
|
-
}
|
|
1
|
+
//! Shared error response formatting
|
|
2
|
+
//!
|
|
3
|
+
//! This module consolidates error response building across all language bindings,
|
|
4
|
+
//! eliminating duplicate `structured_error()` functions in Node, Ruby, and PHP bindings.
|
|
5
|
+
|
|
6
|
+
use axum::http::StatusCode;
|
|
7
|
+
use serde_json::Value;
|
|
8
|
+
use spikard_core::errors::StructuredError;
|
|
9
|
+
use spikard_core::problem::ProblemDetails;
|
|
10
|
+
use spikard_core::validation::ValidationError;
|
|
11
|
+
|
|
12
|
+
/// Builder for creating standardized error responses across all bindings
|
|
13
|
+
pub struct ErrorResponseBuilder;
|
|
14
|
+
|
|
15
|
+
impl ErrorResponseBuilder {
|
|
16
|
+
/// Create a structured error response with status code and error details
|
|
17
|
+
///
|
|
18
|
+
/// Returns a tuple of (StatusCode, JSON body as String)
|
|
19
|
+
///
|
|
20
|
+
/// # Arguments
|
|
21
|
+
/// * `status` - HTTP status code
|
|
22
|
+
/// * `code` - Machine-readable error code
|
|
23
|
+
/// * `message` - Human-readable error message
|
|
24
|
+
///
|
|
25
|
+
/// # Example
|
|
26
|
+
/// ```
|
|
27
|
+
/// use axum::http::StatusCode;
|
|
28
|
+
/// use spikard_bindings_shared::ErrorResponseBuilder;
|
|
29
|
+
///
|
|
30
|
+
/// let (status, body) = ErrorResponseBuilder::structured_error(
|
|
31
|
+
/// StatusCode::BAD_REQUEST,
|
|
32
|
+
/// "invalid_input",
|
|
33
|
+
/// "Missing required field",
|
|
34
|
+
/// );
|
|
35
|
+
/// ```
|
|
36
|
+
pub fn structured_error(status: StatusCode, code: &str, message: impl Into<String>) -> (StatusCode, String) {
|
|
37
|
+
let payload = StructuredError::simple(code.to_string(), message.into());
|
|
38
|
+
let body = serde_json::to_string(&payload)
|
|
39
|
+
.unwrap_or_else(|_| r#"{"error":"serialization_failed","code":"internal_error","details":{}}"#.to_string());
|
|
40
|
+
(status, body)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Create an error response with additional details
|
|
44
|
+
///
|
|
45
|
+
/// Returns a tuple of (StatusCode, JSON body as String)
|
|
46
|
+
///
|
|
47
|
+
/// # Arguments
|
|
48
|
+
/// * `status` - HTTP status code
|
|
49
|
+
/// * `code` - Machine-readable error code
|
|
50
|
+
/// * `message` - Human-readable error message
|
|
51
|
+
/// * `details` - Structured details about the error
|
|
52
|
+
///
|
|
53
|
+
/// # Example
|
|
54
|
+
/// ```
|
|
55
|
+
/// use axum::http::StatusCode;
|
|
56
|
+
/// use serde_json::json;
|
|
57
|
+
/// use spikard_bindings_shared::ErrorResponseBuilder;
|
|
58
|
+
///
|
|
59
|
+
/// let details = json!({
|
|
60
|
+
/// "field": "email",
|
|
61
|
+
/// "reason": "invalid_format"
|
|
62
|
+
/// });
|
|
63
|
+
/// let (status, body) = ErrorResponseBuilder::with_details(
|
|
64
|
+
/// StatusCode::BAD_REQUEST,
|
|
65
|
+
/// "validation_error",
|
|
66
|
+
/// "Invalid email format",
|
|
67
|
+
/// details,
|
|
68
|
+
/// );
|
|
69
|
+
/// ```
|
|
70
|
+
pub fn with_details(
|
|
71
|
+
status: StatusCode,
|
|
72
|
+
code: &str,
|
|
73
|
+
message: impl Into<String>,
|
|
74
|
+
details: Value,
|
|
75
|
+
) -> (StatusCode, String) {
|
|
76
|
+
let payload = StructuredError::new(code.to_string(), message.into(), details);
|
|
77
|
+
let body = serde_json::to_string(&payload)
|
|
78
|
+
.unwrap_or_else(|_| r#"{"error":"serialization_failed","code":"internal_error","details":{}}"#.to_string());
|
|
79
|
+
(status, body)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Create an error response from a StructuredError
|
|
83
|
+
///
|
|
84
|
+
/// Returns a tuple of (StatusCode, JSON body as String)
|
|
85
|
+
///
|
|
86
|
+
/// # Arguments
|
|
87
|
+
/// * `error` - The structured error
|
|
88
|
+
///
|
|
89
|
+
/// # Note
|
|
90
|
+
/// Uses INTERNAL_SERVER_ERROR as the default status code. Override with
|
|
91
|
+
/// `structured_error()` or `with_details()` for specific status codes.
|
|
92
|
+
pub fn from_structured_error(error: StructuredError) -> (StatusCode, String) {
|
|
93
|
+
let status = StatusCode::INTERNAL_SERVER_ERROR;
|
|
94
|
+
let body = serde_json::to_string(&error)
|
|
95
|
+
.unwrap_or_else(|_| r#"{"error":"serialization_failed","code":"internal_error","details":{}}"#.to_string());
|
|
96
|
+
(status, body)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Create a validation error response
|
|
100
|
+
///
|
|
101
|
+
/// Converts ValidationError to RFC 9457 Problem Details format.
|
|
102
|
+
/// Returns (StatusCode::UNPROCESSABLE_ENTITY, JSON body)
|
|
103
|
+
///
|
|
104
|
+
/// # Arguments
|
|
105
|
+
/// * `validation_error` - The validation error containing one or more details
|
|
106
|
+
///
|
|
107
|
+
/// # Example
|
|
108
|
+
/// ```
|
|
109
|
+
/// use spikard_core::validation::{ValidationError, ValidationErrorDetail};
|
|
110
|
+
/// use serde_json::Value;
|
|
111
|
+
/// use spikard_bindings_shared::ErrorResponseBuilder;
|
|
112
|
+
///
|
|
113
|
+
/// let validation_error = ValidationError {
|
|
114
|
+
/// errors: vec![
|
|
115
|
+
/// ValidationErrorDetail {
|
|
116
|
+
/// error_type: "missing".to_string(),
|
|
117
|
+
/// loc: vec!["body".to_string(), "username".to_string()],
|
|
118
|
+
/// msg: "Field required".to_string(),
|
|
119
|
+
/// input: Value::String("".to_string()),
|
|
120
|
+
/// ctx: None,
|
|
121
|
+
/// },
|
|
122
|
+
/// ],
|
|
123
|
+
/// };
|
|
124
|
+
///
|
|
125
|
+
/// let (status, body) = ErrorResponseBuilder::validation_error(&validation_error);
|
|
126
|
+
/// ```
|
|
127
|
+
pub fn validation_error(validation_error: &ValidationError) -> (StatusCode, String) {
|
|
128
|
+
let problem = ProblemDetails::from_validation_error(validation_error);
|
|
129
|
+
let status = problem.status_code();
|
|
130
|
+
let body = serde_json::to_string(&problem).unwrap_or_else(|_| {
|
|
131
|
+
r#"{"title":"Validation Failed","type":"https://spikard.dev/errors/validation-error","status":422}"#
|
|
132
|
+
.to_string()
|
|
133
|
+
});
|
|
134
|
+
(status, body)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Create an RFC 9457 Problem Details response
|
|
138
|
+
///
|
|
139
|
+
/// Returns a tuple of (StatusCode, JSON body as String)
|
|
140
|
+
///
|
|
141
|
+
/// # Arguments
|
|
142
|
+
/// * `problem` - The Problem Details object
|
|
143
|
+
///
|
|
144
|
+
/// # Example
|
|
145
|
+
/// ```
|
|
146
|
+
/// use axum::http::StatusCode;
|
|
147
|
+
/// use spikard_core::problem::ProblemDetails;
|
|
148
|
+
/// use spikard_bindings_shared::ErrorResponseBuilder;
|
|
149
|
+
///
|
|
150
|
+
/// let problem = ProblemDetails::not_found("User with id 123 not found");
|
|
151
|
+
/// let (status, body) = ErrorResponseBuilder::problem_details_response(&problem);
|
|
152
|
+
/// ```
|
|
153
|
+
pub fn problem_details_response(problem: &ProblemDetails) -> (StatusCode, String) {
|
|
154
|
+
let status = problem.status_code();
|
|
155
|
+
let body = serde_json::to_string(problem).unwrap_or_else(|_| {
|
|
156
|
+
r#"{"title":"Internal Server Error","type":"https://spikard.dev/errors/internal-server-error","status":500}"#
|
|
157
|
+
.to_string()
|
|
158
|
+
});
|
|
159
|
+
(status, body)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// Create a generic bad request error
|
|
163
|
+
///
|
|
164
|
+
/// Returns (StatusCode::BAD_REQUEST, JSON body)
|
|
165
|
+
pub fn bad_request(message: impl Into<String>) -> (StatusCode, String) {
|
|
166
|
+
Self::structured_error(StatusCode::BAD_REQUEST, "bad_request", message)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// Create a generic internal server error
|
|
170
|
+
///
|
|
171
|
+
/// Returns (StatusCode::INTERNAL_SERVER_ERROR, JSON body)
|
|
172
|
+
pub fn internal_error(message: impl Into<String>) -> (StatusCode, String) {
|
|
173
|
+
Self::structured_error(StatusCode::INTERNAL_SERVER_ERROR, "internal_error", message)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// Create an unauthorized error
|
|
177
|
+
///
|
|
178
|
+
/// Returns (StatusCode::UNAUTHORIZED, JSON body)
|
|
179
|
+
pub fn unauthorized(message: impl Into<String>) -> (StatusCode, String) {
|
|
180
|
+
Self::structured_error(StatusCode::UNAUTHORIZED, "unauthorized", message)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// Create a forbidden error
|
|
184
|
+
///
|
|
185
|
+
/// Returns (StatusCode::FORBIDDEN, JSON body)
|
|
186
|
+
pub fn forbidden(message: impl Into<String>) -> (StatusCode, String) {
|
|
187
|
+
Self::structured_error(StatusCode::FORBIDDEN, "forbidden", message)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Create a not found error
|
|
191
|
+
///
|
|
192
|
+
/// Returns (StatusCode::NOT_FOUND, JSON body)
|
|
193
|
+
pub fn not_found(message: impl Into<String>) -> (StatusCode, String) {
|
|
194
|
+
Self::structured_error(StatusCode::NOT_FOUND, "not_found", message)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/// Create a method not allowed error
|
|
198
|
+
///
|
|
199
|
+
/// Returns (StatusCode::METHOD_NOT_ALLOWED, JSON body)
|
|
200
|
+
pub fn method_not_allowed(message: impl Into<String>) -> (StatusCode, String) {
|
|
201
|
+
Self::structured_error(StatusCode::METHOD_NOT_ALLOWED, "method_not_allowed", message)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/// Create an unprocessable entity error (validation failed)
|
|
205
|
+
///
|
|
206
|
+
/// Returns (StatusCode::UNPROCESSABLE_ENTITY, JSON body)
|
|
207
|
+
pub fn unprocessable_entity(message: impl Into<String>) -> (StatusCode, String) {
|
|
208
|
+
Self::structured_error(StatusCode::UNPROCESSABLE_ENTITY, "unprocessable_entity", message)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/// Create a conflict error
|
|
212
|
+
///
|
|
213
|
+
/// Returns (StatusCode::CONFLICT, JSON body)
|
|
214
|
+
pub fn conflict(message: impl Into<String>) -> (StatusCode, String) {
|
|
215
|
+
Self::structured_error(StatusCode::CONFLICT, "conflict", message)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/// Create a service unavailable error
|
|
219
|
+
///
|
|
220
|
+
/// Returns (StatusCode::SERVICE_UNAVAILABLE, JSON body)
|
|
221
|
+
pub fn service_unavailable(message: impl Into<String>) -> (StatusCode, String) {
|
|
222
|
+
Self::structured_error(StatusCode::SERVICE_UNAVAILABLE, "service_unavailable", message)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// Create a request timeout error
|
|
226
|
+
///
|
|
227
|
+
/// Returns (StatusCode::REQUEST_TIMEOUT, JSON body)
|
|
228
|
+
pub fn request_timeout(message: impl Into<String>) -> (StatusCode, String) {
|
|
229
|
+
Self::structured_error(StatusCode::REQUEST_TIMEOUT, "request_timeout", message)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#[cfg(test)]
|
|
234
|
+
mod tests {
|
|
235
|
+
use super::*;
|
|
236
|
+
use serde_json::json;
|
|
237
|
+
use spikard_core::validation::ValidationErrorDetail;
|
|
238
|
+
|
|
239
|
+
#[test]
|
|
240
|
+
fn test_structured_error() {
|
|
241
|
+
let (status, body) =
|
|
242
|
+
ErrorResponseBuilder::structured_error(StatusCode::BAD_REQUEST, "invalid_input", "Missing required field");
|
|
243
|
+
assert_eq!(status, StatusCode::BAD_REQUEST);
|
|
244
|
+
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
245
|
+
assert_eq!(parsed["error"], "Missing required field");
|
|
246
|
+
assert_eq!(parsed["code"], "invalid_input");
|
|
247
|
+
assert!(parsed["details"].is_object());
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#[test]
|
|
251
|
+
fn test_with_details() {
|
|
252
|
+
let details = json!({
|
|
253
|
+
"field": "email",
|
|
254
|
+
"reason": "invalid_format"
|
|
255
|
+
});
|
|
256
|
+
let (status, body) = ErrorResponseBuilder::with_details(
|
|
257
|
+
StatusCode::BAD_REQUEST,
|
|
258
|
+
"validation_error",
|
|
259
|
+
"Invalid email format",
|
|
260
|
+
details.clone(),
|
|
261
|
+
);
|
|
262
|
+
assert_eq!(status, StatusCode::BAD_REQUEST);
|
|
263
|
+
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
264
|
+
assert_eq!(parsed["code"], "validation_error");
|
|
265
|
+
assert_eq!(parsed["error"], "Invalid email format");
|
|
266
|
+
assert_eq!(parsed["details"]["field"], "email");
|
|
267
|
+
assert_eq!(parsed["details"]["reason"], "invalid_format");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
#[test]
|
|
271
|
+
fn test_from_structured_error() {
|
|
272
|
+
let error = StructuredError::simple("test_error", "Something went wrong");
|
|
273
|
+
let (status, body) = ErrorResponseBuilder::from_structured_error(error);
|
|
274
|
+
assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
|
|
275
|
+
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
276
|
+
assert_eq!(parsed["code"], "test_error");
|
|
277
|
+
assert_eq!(parsed["error"], "Something went wrong");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
#[test]
|
|
281
|
+
fn test_validation_error() {
|
|
282
|
+
let validation_error = ValidationError {
|
|
283
|
+
errors: vec![ValidationErrorDetail {
|
|
284
|
+
error_type: "missing".to_string(),
|
|
285
|
+
loc: vec!["body".to_string(), "username".to_string()],
|
|
286
|
+
msg: "Field required".to_string(),
|
|
287
|
+
input: Value::String("".to_string()),
|
|
288
|
+
ctx: None,
|
|
289
|
+
}],
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
let (status, body) = ErrorResponseBuilder::validation_error(&validation_error);
|
|
293
|
+
assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
|
|
294
|
+
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
295
|
+
assert_eq!(parsed["title"], "Request Validation Failed");
|
|
296
|
+
assert_eq!(parsed["status"], 422);
|
|
297
|
+
assert!(parsed["errors"].is_array());
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
#[test]
|
|
301
|
+
fn test_problem_details_response() {
|
|
302
|
+
let problem = ProblemDetails::not_found("User with id 123 not found");
|
|
303
|
+
let (status, body) = ErrorResponseBuilder::problem_details_response(&problem);
|
|
304
|
+
assert_eq!(status, StatusCode::NOT_FOUND);
|
|
305
|
+
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
306
|
+
assert_eq!(parsed["type"], "https://spikard.dev/errors/not-found");
|
|
307
|
+
assert_eq!(parsed["title"], "Resource Not Found");
|
|
308
|
+
assert_eq!(parsed["status"], 404);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
#[test]
|
|
312
|
+
fn test_bad_request() {
|
|
313
|
+
let (status, body) = ErrorResponseBuilder::bad_request("Invalid data");
|
|
314
|
+
assert_eq!(status, StatusCode::BAD_REQUEST);
|
|
315
|
+
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
316
|
+
assert_eq!(parsed["code"], "bad_request");
|
|
317
|
+
assert_eq!(parsed["error"], "Invalid data");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
#[test]
|
|
321
|
+
fn test_internal_error() {
|
|
322
|
+
let (status, body) = ErrorResponseBuilder::internal_error("Something went wrong");
|
|
323
|
+
assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
|
|
324
|
+
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
325
|
+
assert_eq!(parsed["code"], "internal_error");
|
|
326
|
+
assert_eq!(parsed["error"], "Something went wrong");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
#[test]
|
|
330
|
+
fn test_unauthorized() {
|
|
331
|
+
let (status, body) = ErrorResponseBuilder::unauthorized("Authentication required");
|
|
332
|
+
assert_eq!(status, StatusCode::UNAUTHORIZED);
|
|
333
|
+
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
334
|
+
assert_eq!(parsed["code"], "unauthorized");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
#[test]
|
|
338
|
+
fn test_forbidden() {
|
|
339
|
+
let (status, body) = ErrorResponseBuilder::forbidden("Access denied");
|
|
340
|
+
assert_eq!(status, StatusCode::FORBIDDEN);
|
|
341
|
+
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
342
|
+
assert_eq!(parsed["code"], "forbidden");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
#[test]
|
|
346
|
+
fn test_not_found() {
|
|
347
|
+
let (status, body) = ErrorResponseBuilder::not_found("Resource not found");
|
|
348
|
+
assert_eq!(status, StatusCode::NOT_FOUND);
|
|
349
|
+
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
350
|
+
assert_eq!(parsed["code"], "not_found");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
#[test]
|
|
354
|
+
fn test_method_not_allowed() {
|
|
355
|
+
let (status, body) = ErrorResponseBuilder::method_not_allowed("Method POST not allowed");
|
|
356
|
+
assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED);
|
|
357
|
+
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
358
|
+
assert_eq!(parsed["code"], "method_not_allowed");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
#[test]
|
|
362
|
+
fn test_unprocessable_entity() {
|
|
363
|
+
let (status, body) = ErrorResponseBuilder::unprocessable_entity("Validation failed");
|
|
364
|
+
assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
|
|
365
|
+
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
366
|
+
assert_eq!(parsed["code"], "unprocessable_entity");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
#[test]
|
|
370
|
+
fn test_conflict() {
|
|
371
|
+
let (status, body) = ErrorResponseBuilder::conflict("Resource already exists");
|
|
372
|
+
assert_eq!(status, StatusCode::CONFLICT);
|
|
373
|
+
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
374
|
+
assert_eq!(parsed["code"], "conflict");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
#[test]
|
|
378
|
+
fn test_service_unavailable() {
|
|
379
|
+
let (status, body) = ErrorResponseBuilder::service_unavailable("Service temporarily unavailable");
|
|
380
|
+
assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);
|
|
381
|
+
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
382
|
+
assert_eq!(parsed["code"], "service_unavailable");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
#[test]
|
|
386
|
+
fn test_request_timeout() {
|
|
387
|
+
let (status, body) = ErrorResponseBuilder::request_timeout("Request timed out");
|
|
388
|
+
assert_eq!(status, StatusCode::REQUEST_TIMEOUT);
|
|
389
|
+
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
390
|
+
assert_eq!(parsed["code"], "request_timeout");
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
#[test]
|
|
394
|
+
fn test_serialization_fallback() {
|
|
395
|
+
let details = serde_json::Map::new();
|
|
396
|
+
let (_status, body) =
|
|
397
|
+
ErrorResponseBuilder::with_details(StatusCode::BAD_REQUEST, "test", "Test error", Value::Object(details));
|
|
398
|
+
|
|
399
|
+
assert!(serde_json::from_str::<Value>(&body).is_ok());
|
|
400
|
+
}
|
|
401
|
+
}
|