spikard 0.2.1 → 0.2.5
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 +626 -626
- data/ext/spikard_rb/Cargo.toml +17 -17
- data/ext/spikard_rb/extconf.rb +10 -10
- data/ext/spikard_rb/src/lib.rs +6 -6
- data/lib/spikard/app.rb +374 -374
- data/lib/spikard/background.rb +27 -27
- data/lib/spikard/config.rb +396 -396
- data/lib/spikard/converters.rb +85 -85
- data/lib/spikard/handler_wrapper.rb +116 -116
- data/lib/spikard/provide.rb +228 -228
- data/lib/spikard/response.rb +109 -109
- data/lib/spikard/schema.rb +243 -243
- data/lib/spikard/sse.rb +111 -111
- data/lib/spikard/streaming_response.rb +21 -21
- data/lib/spikard/testing.rb +221 -221
- 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 +349 -349
- data/vendor/crates/spikard-core/Cargo.toml +40 -0
- data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -0
- data/vendor/crates/spikard-core/src/bindings/response.rs +133 -0
- data/vendor/crates/spikard-core/src/debug.rs +63 -0
- data/vendor/crates/spikard-core/src/di/container.rs +726 -0
- data/vendor/crates/spikard-core/src/di/dependency.rs +273 -0
- data/vendor/crates/spikard-core/src/di/error.rs +118 -0
- data/vendor/crates/spikard-core/src/di/factory.rs +538 -0
- data/vendor/crates/spikard-core/src/di/graph.rs +545 -0
- data/vendor/crates/spikard-core/src/di/mod.rs +192 -0
- data/vendor/crates/spikard-core/src/di/resolved.rs +411 -0
- data/vendor/crates/spikard-core/src/di/value.rs +283 -0
- data/vendor/crates/spikard-core/src/http.rs +153 -0
- data/vendor/crates/spikard-core/src/lib.rs +28 -0
- data/vendor/crates/spikard-core/src/lifecycle.rs +422 -0
- data/vendor/crates/spikard-core/src/parameters.rs +719 -0
- data/vendor/crates/spikard-core/src/problem.rs +310 -0
- data/vendor/crates/spikard-core/src/request_data.rs +189 -0
- data/vendor/crates/spikard-core/src/router.rs +249 -0
- data/vendor/crates/spikard-core/src/schema_registry.rs +183 -0
- data/vendor/crates/spikard-core/src/type_hints.rs +304 -0
- data/vendor/crates/spikard-core/src/validation.rs +699 -0
- data/vendor/crates/spikard-http/Cargo.toml +58 -0
- data/vendor/crates/spikard-http/src/auth.rs +247 -0
- data/vendor/crates/spikard-http/src/background.rs +249 -0
- data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -0
- data/vendor/crates/spikard-http/src/bindings/response.rs +1 -0
- data/vendor/crates/spikard-http/src/body_metadata.rs +8 -0
- data/vendor/crates/spikard-http/src/cors.rs +490 -0
- data/vendor/crates/spikard-http/src/debug.rs +63 -0
- data/vendor/crates/spikard-http/src/di_handler.rs +423 -0
- data/vendor/crates/spikard-http/src/handler_response.rs +190 -0
- data/vendor/crates/spikard-http/src/handler_trait.rs +228 -0
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +284 -0
- data/vendor/crates/spikard-http/src/lib.rs +529 -0
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +149 -0
- data/vendor/crates/spikard-http/src/lifecycle.rs +428 -0
- data/vendor/crates/spikard-http/src/middleware/mod.rs +285 -0
- data/vendor/crates/spikard-http/src/middleware/multipart.rs +86 -0
- data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +147 -0
- data/vendor/crates/spikard-http/src/middleware/validation.rs +287 -0
- data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -0
- data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +190 -0
- data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +308 -0
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +195 -0
- data/vendor/crates/spikard-http/src/parameters.rs +1 -0
- data/vendor/crates/spikard-http/src/problem.rs +1 -0
- data/vendor/crates/spikard-http/src/query_parser.rs +369 -0
- data/vendor/crates/spikard-http/src/response.rs +399 -0
- data/vendor/crates/spikard-http/src/router.rs +1 -0
- data/vendor/crates/spikard-http/src/schema_registry.rs +1 -0
- data/vendor/crates/spikard-http/src/server/handler.rs +80 -0
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +98 -0
- data/vendor/crates/spikard-http/src/server/mod.rs +805 -0
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +119 -0
- data/vendor/crates/spikard-http/src/sse.rs +447 -0
- data/vendor/crates/spikard-http/src/testing/form.rs +14 -0
- data/vendor/crates/spikard-http/src/testing/multipart.rs +60 -0
- data/vendor/crates/spikard-http/src/testing/test_client.rs +285 -0
- data/vendor/crates/spikard-http/src/testing.rs +377 -0
- data/vendor/crates/spikard-http/src/type_hints.rs +1 -0
- data/vendor/crates/spikard-http/src/validation.rs +1 -0
- data/vendor/crates/spikard-http/src/websocket.rs +324 -0
- data/vendor/crates/spikard-rb/Cargo.toml +42 -0
- data/vendor/crates/spikard-rb/build.rs +8 -0
- data/vendor/crates/spikard-rb/src/background.rs +63 -0
- data/vendor/crates/spikard-rb/src/config.rs +294 -0
- data/vendor/crates/spikard-rb/src/conversion.rs +392 -0
- data/vendor/crates/spikard-rb/src/di.rs +409 -0
- data/vendor/crates/spikard-rb/src/handler.rs +534 -0
- data/vendor/crates/spikard-rb/src/lib.rs +2020 -0
- data/vendor/crates/spikard-rb/src/lifecycle.rs +267 -0
- data/vendor/crates/spikard-rb/src/server.rs +283 -0
- data/vendor/crates/spikard-rb/src/sse.rs +231 -0
- data/vendor/crates/spikard-rb/src/test_client.rs +404 -0
- data/vendor/crates/spikard-rb/src/test_sse.rs +143 -0
- data/vendor/crates/spikard-rb/src/test_websocket.rs +221 -0
- data/vendor/crates/spikard-rb/src/websocket.rs +233 -0
- metadata +80 -2
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
//! HTTP Response types
|
|
2
|
+
//!
|
|
3
|
+
//! Response types for returning custom responses with status codes, headers, and content
|
|
4
|
+
|
|
5
|
+
use serde_json::Value;
|
|
6
|
+
use std::collections::HashMap;
|
|
7
|
+
|
|
8
|
+
/// HTTP Response with custom status code, headers, and content
|
|
9
|
+
#[derive(Debug, Clone)]
|
|
10
|
+
pub struct Response {
|
|
11
|
+
/// Response body content
|
|
12
|
+
pub content: Option<Value>,
|
|
13
|
+
/// HTTP status code (defaults to 200)
|
|
14
|
+
pub status_code: u16,
|
|
15
|
+
/// Response headers
|
|
16
|
+
pub headers: HashMap<String, String>,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
impl Response {
|
|
20
|
+
/// Create a new Response with default status 200
|
|
21
|
+
pub fn new(content: Option<Value>) -> Self {
|
|
22
|
+
Self {
|
|
23
|
+
content,
|
|
24
|
+
status_code: 200,
|
|
25
|
+
headers: HashMap::new(),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Create a response with a specific status code
|
|
30
|
+
pub fn with_status(content: Option<Value>, status_code: u16) -> Self {
|
|
31
|
+
Self {
|
|
32
|
+
content,
|
|
33
|
+
status_code,
|
|
34
|
+
headers: HashMap::new(),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Set a header
|
|
39
|
+
pub fn set_header(&mut self, key: String, value: String) {
|
|
40
|
+
self.headers.insert(key, value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Set a cookie in the response
|
|
44
|
+
#[allow(clippy::too_many_arguments)]
|
|
45
|
+
pub fn set_cookie(
|
|
46
|
+
&mut self,
|
|
47
|
+
key: String,
|
|
48
|
+
value: String,
|
|
49
|
+
max_age: Option<i64>,
|
|
50
|
+
domain: Option<String>,
|
|
51
|
+
path: Option<String>,
|
|
52
|
+
secure: bool,
|
|
53
|
+
http_only: bool,
|
|
54
|
+
same_site: Option<String>,
|
|
55
|
+
) {
|
|
56
|
+
let mut cookie_value = format!("{}={}", key, value);
|
|
57
|
+
|
|
58
|
+
if let Some(age) = max_age {
|
|
59
|
+
cookie_value.push_str(&format!("; Max-Age={}", age));
|
|
60
|
+
}
|
|
61
|
+
if let Some(d) = domain {
|
|
62
|
+
cookie_value.push_str(&format!("; Domain={}", d));
|
|
63
|
+
}
|
|
64
|
+
if let Some(p) = path {
|
|
65
|
+
cookie_value.push_str(&format!("; Path={}", p));
|
|
66
|
+
}
|
|
67
|
+
if secure {
|
|
68
|
+
cookie_value.push_str("; Secure");
|
|
69
|
+
}
|
|
70
|
+
if http_only {
|
|
71
|
+
cookie_value.push_str("; HttpOnly");
|
|
72
|
+
}
|
|
73
|
+
if let Some(ss) = same_site {
|
|
74
|
+
cookie_value.push_str(&format!("; SameSite={}", ss));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
self.headers.insert("set-cookie".to_string(), cookie_value);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
impl Default for Response {
|
|
82
|
+
fn default() -> Self {
|
|
83
|
+
Self::new(None)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[cfg(test)]
|
|
88
|
+
mod tests {
|
|
89
|
+
use super::*;
|
|
90
|
+
use serde_json::json;
|
|
91
|
+
|
|
92
|
+
#[test]
|
|
93
|
+
fn response_new_creates_default_status() {
|
|
94
|
+
let response = Response::new(None);
|
|
95
|
+
assert_eq!(response.status_code, 200);
|
|
96
|
+
assert!(response.headers.is_empty());
|
|
97
|
+
assert!(response.content.is_none());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[test]
|
|
101
|
+
fn response_new_with_content() {
|
|
102
|
+
let content = json!({"key": "value"});
|
|
103
|
+
let response = Response::new(Some(content.clone()));
|
|
104
|
+
assert_eq!(response.status_code, 200);
|
|
105
|
+
assert_eq!(response.content, Some(content));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[test]
|
|
109
|
+
fn response_with_status() {
|
|
110
|
+
let response = Response::with_status(None, 404);
|
|
111
|
+
assert_eq!(response.status_code, 404);
|
|
112
|
+
assert!(response.headers.is_empty());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#[test]
|
|
116
|
+
fn response_with_status_and_content() {
|
|
117
|
+
let content = json!({"error": "not found"});
|
|
118
|
+
let response = Response::with_status(Some(content.clone()), 404);
|
|
119
|
+
assert_eq!(response.status_code, 404);
|
|
120
|
+
assert_eq!(response.content, Some(content));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#[test]
|
|
124
|
+
fn response_set_header() {
|
|
125
|
+
let mut response = Response::new(None);
|
|
126
|
+
response.set_header("X-Custom".to_string(), "custom-value".to_string());
|
|
127
|
+
assert_eq!(response.headers.get("X-Custom"), Some(&"custom-value".to_string()));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#[test]
|
|
131
|
+
fn response_set_multiple_headers() {
|
|
132
|
+
let mut response = Response::new(None);
|
|
133
|
+
response.set_header("Content-Type".to_string(), "application/json".to_string());
|
|
134
|
+
response.set_header("X-Custom".to_string(), "custom-value".to_string());
|
|
135
|
+
assert_eq!(response.headers.len(), 2);
|
|
136
|
+
assert_eq!(
|
|
137
|
+
response.headers.get("Content-Type"),
|
|
138
|
+
Some(&"application/json".to_string())
|
|
139
|
+
);
|
|
140
|
+
assert_eq!(response.headers.get("X-Custom"), Some(&"custom-value".to_string()));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#[test]
|
|
144
|
+
fn response_set_header_overwrites() {
|
|
145
|
+
let mut response = Response::new(None);
|
|
146
|
+
response.set_header("X-Custom".to_string(), "value1".to_string());
|
|
147
|
+
response.set_header("X-Custom".to_string(), "value2".to_string());
|
|
148
|
+
assert_eq!(response.headers.get("X-Custom"), Some(&"value2".to_string()));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#[test]
|
|
152
|
+
fn response_set_cookie_minimal() {
|
|
153
|
+
let mut response = Response::new(None);
|
|
154
|
+
response.set_cookie(
|
|
155
|
+
"session_id".to_string(),
|
|
156
|
+
"abc123".to_string(),
|
|
157
|
+
None,
|
|
158
|
+
None,
|
|
159
|
+
None,
|
|
160
|
+
false,
|
|
161
|
+
false,
|
|
162
|
+
None,
|
|
163
|
+
);
|
|
164
|
+
let cookie = response.headers.get("set-cookie").unwrap();
|
|
165
|
+
assert_eq!(cookie, "session_id=abc123");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#[test]
|
|
169
|
+
fn response_set_cookie_with_max_age() {
|
|
170
|
+
let mut response = Response::new(None);
|
|
171
|
+
response.set_cookie(
|
|
172
|
+
"session".to_string(),
|
|
173
|
+
"token".to_string(),
|
|
174
|
+
Some(3600),
|
|
175
|
+
None,
|
|
176
|
+
None,
|
|
177
|
+
false,
|
|
178
|
+
false,
|
|
179
|
+
None,
|
|
180
|
+
);
|
|
181
|
+
let cookie = response.headers.get("set-cookie").unwrap();
|
|
182
|
+
assert!(cookie.contains("session=token"));
|
|
183
|
+
assert!(cookie.contains("Max-Age=3600"));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#[test]
|
|
187
|
+
fn response_set_cookie_with_domain() {
|
|
188
|
+
let mut response = Response::new(None);
|
|
189
|
+
response.set_cookie(
|
|
190
|
+
"session".to_string(),
|
|
191
|
+
"token".to_string(),
|
|
192
|
+
None,
|
|
193
|
+
Some("example.com".to_string()),
|
|
194
|
+
None,
|
|
195
|
+
false,
|
|
196
|
+
false,
|
|
197
|
+
None,
|
|
198
|
+
);
|
|
199
|
+
let cookie = response.headers.get("set-cookie").unwrap();
|
|
200
|
+
assert!(cookie.contains("Domain=example.com"));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#[test]
|
|
204
|
+
fn response_set_cookie_with_path() {
|
|
205
|
+
let mut response = Response::new(None);
|
|
206
|
+
response.set_cookie(
|
|
207
|
+
"session".to_string(),
|
|
208
|
+
"token".to_string(),
|
|
209
|
+
None,
|
|
210
|
+
None,
|
|
211
|
+
Some("/app".to_string()),
|
|
212
|
+
false,
|
|
213
|
+
false,
|
|
214
|
+
None,
|
|
215
|
+
);
|
|
216
|
+
let cookie = response.headers.get("set-cookie").unwrap();
|
|
217
|
+
assert!(cookie.contains("Path=/app"));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
#[test]
|
|
221
|
+
fn response_set_cookie_secure() {
|
|
222
|
+
let mut response = Response::new(None);
|
|
223
|
+
response.set_cookie(
|
|
224
|
+
"session".to_string(),
|
|
225
|
+
"token".to_string(),
|
|
226
|
+
None,
|
|
227
|
+
None,
|
|
228
|
+
None,
|
|
229
|
+
true,
|
|
230
|
+
false,
|
|
231
|
+
None,
|
|
232
|
+
);
|
|
233
|
+
let cookie = response.headers.get("set-cookie").unwrap();
|
|
234
|
+
assert!(cookie.contains("Secure"));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#[test]
|
|
238
|
+
fn response_set_cookie_http_only() {
|
|
239
|
+
let mut response = Response::new(None);
|
|
240
|
+
response.set_cookie(
|
|
241
|
+
"session".to_string(),
|
|
242
|
+
"token".to_string(),
|
|
243
|
+
None,
|
|
244
|
+
None,
|
|
245
|
+
None,
|
|
246
|
+
false,
|
|
247
|
+
true,
|
|
248
|
+
None,
|
|
249
|
+
);
|
|
250
|
+
let cookie = response.headers.get("set-cookie").unwrap();
|
|
251
|
+
assert!(cookie.contains("HttpOnly"));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#[test]
|
|
255
|
+
fn response_set_cookie_same_site() {
|
|
256
|
+
let mut response = Response::new(None);
|
|
257
|
+
response.set_cookie(
|
|
258
|
+
"session".to_string(),
|
|
259
|
+
"token".to_string(),
|
|
260
|
+
None,
|
|
261
|
+
None,
|
|
262
|
+
None,
|
|
263
|
+
false,
|
|
264
|
+
false,
|
|
265
|
+
Some("Strict".to_string()),
|
|
266
|
+
);
|
|
267
|
+
let cookie = response.headers.get("set-cookie").unwrap();
|
|
268
|
+
assert!(cookie.contains("SameSite=Strict"));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#[test]
|
|
272
|
+
fn response_set_cookie_all_attributes() {
|
|
273
|
+
let mut response = Response::new(None);
|
|
274
|
+
response.set_cookie(
|
|
275
|
+
"session".to_string(),
|
|
276
|
+
"token123".to_string(),
|
|
277
|
+
Some(3600),
|
|
278
|
+
Some("example.com".to_string()),
|
|
279
|
+
Some("/app".to_string()),
|
|
280
|
+
true,
|
|
281
|
+
true,
|
|
282
|
+
Some("Lax".to_string()),
|
|
283
|
+
);
|
|
284
|
+
let cookie = response.headers.get("set-cookie").unwrap();
|
|
285
|
+
assert!(cookie.contains("session=token123"));
|
|
286
|
+
assert!(cookie.contains("Max-Age=3600"));
|
|
287
|
+
assert!(cookie.contains("Domain=example.com"));
|
|
288
|
+
assert!(cookie.contains("Path=/app"));
|
|
289
|
+
assert!(cookie.contains("Secure"));
|
|
290
|
+
assert!(cookie.contains("HttpOnly"));
|
|
291
|
+
assert!(cookie.contains("SameSite=Lax"));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#[test]
|
|
295
|
+
fn response_set_cookie_overwrites_previous() {
|
|
296
|
+
let mut response = Response::new(None);
|
|
297
|
+
response.set_cookie(
|
|
298
|
+
"session".to_string(),
|
|
299
|
+
"old_token".to_string(),
|
|
300
|
+
None,
|
|
301
|
+
None,
|
|
302
|
+
None,
|
|
303
|
+
false,
|
|
304
|
+
false,
|
|
305
|
+
None,
|
|
306
|
+
);
|
|
307
|
+
response.set_cookie(
|
|
308
|
+
"session".to_string(),
|
|
309
|
+
"new_token".to_string(),
|
|
310
|
+
None,
|
|
311
|
+
None,
|
|
312
|
+
None,
|
|
313
|
+
false,
|
|
314
|
+
false,
|
|
315
|
+
None,
|
|
316
|
+
);
|
|
317
|
+
let cookie = response.headers.get("set-cookie").unwrap();
|
|
318
|
+
assert!(cookie.contains("new_token"));
|
|
319
|
+
assert!(!cookie.contains("old_token"));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
#[test]
|
|
323
|
+
fn response_default() {
|
|
324
|
+
let response = Response::default();
|
|
325
|
+
assert_eq!(response.status_code, 200);
|
|
326
|
+
assert!(response.headers.is_empty());
|
|
327
|
+
assert!(response.content.is_none());
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
#[test]
|
|
331
|
+
fn response_cookie_with_special_chars_in_value() {
|
|
332
|
+
let mut response = Response::new(None);
|
|
333
|
+
response.set_cookie(
|
|
334
|
+
"name".to_string(),
|
|
335
|
+
"value%3D123".to_string(),
|
|
336
|
+
None,
|
|
337
|
+
None,
|
|
338
|
+
None,
|
|
339
|
+
false,
|
|
340
|
+
false,
|
|
341
|
+
None,
|
|
342
|
+
);
|
|
343
|
+
let cookie = response.headers.get("set-cookie").unwrap();
|
|
344
|
+
assert_eq!(cookie, "name=value%3D123");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
#[test]
|
|
348
|
+
fn response_same_site_variants() {
|
|
349
|
+
for same_site in &["Strict", "Lax", "None"] {
|
|
350
|
+
let mut response = Response::new(None);
|
|
351
|
+
response.set_cookie(
|
|
352
|
+
"test".to_string(),
|
|
353
|
+
"value".to_string(),
|
|
354
|
+
None,
|
|
355
|
+
None,
|
|
356
|
+
None,
|
|
357
|
+
false,
|
|
358
|
+
false,
|
|
359
|
+
Some(same_site.to_string()),
|
|
360
|
+
);
|
|
361
|
+
let cookie = response.headers.get("set-cookie").unwrap();
|
|
362
|
+
assert!(cookie.contains(&format!("SameSite={}", same_site)));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
#[test]
|
|
367
|
+
fn response_zero_max_age() {
|
|
368
|
+
let mut response = Response::new(None);
|
|
369
|
+
response.set_cookie(
|
|
370
|
+
"session".to_string(),
|
|
371
|
+
"token".to_string(),
|
|
372
|
+
Some(0),
|
|
373
|
+
None,
|
|
374
|
+
None,
|
|
375
|
+
false,
|
|
376
|
+
false,
|
|
377
|
+
None,
|
|
378
|
+
);
|
|
379
|
+
let cookie = response.headers.get("set-cookie").unwrap();
|
|
380
|
+
assert!(cookie.contains("Max-Age=0"));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
#[test]
|
|
384
|
+
fn response_negative_max_age() {
|
|
385
|
+
let mut response = Response::new(None);
|
|
386
|
+
response.set_cookie(
|
|
387
|
+
"session".to_string(),
|
|
388
|
+
"token".to_string(),
|
|
389
|
+
Some(-1),
|
|
390
|
+
None,
|
|
391
|
+
None,
|
|
392
|
+
false,
|
|
393
|
+
false,
|
|
394
|
+
None,
|
|
395
|
+
);
|
|
396
|
+
let cookie = response.headers.get("set-cookie").unwrap();
|
|
397
|
+
assert!(cookie.contains("Max-Age=-1"));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pub use spikard_core::router::*;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pub use spikard_core::schema_registry::*;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
//! ValidatingHandler wrapper that executes request/parameter validation before handler
|
|
2
|
+
|
|
3
|
+
use crate::ProblemDetails;
|
|
4
|
+
use crate::handler_trait::{Handler, HandlerResult, RequestData};
|
|
5
|
+
use crate::parameters::ParameterValidator;
|
|
6
|
+
use crate::validation::SchemaValidator;
|
|
7
|
+
use axum::body::Body;
|
|
8
|
+
use serde_json::Value;
|
|
9
|
+
use std::collections::HashMap;
|
|
10
|
+
use std::future::Future;
|
|
11
|
+
use std::pin::Pin;
|
|
12
|
+
use std::sync::Arc;
|
|
13
|
+
|
|
14
|
+
/// Wrapper that runs request/parameter validation before calling the user handler.
|
|
15
|
+
pub struct ValidatingHandler {
|
|
16
|
+
inner: Arc<dyn Handler>,
|
|
17
|
+
request_validator: Option<Arc<SchemaValidator>>,
|
|
18
|
+
parameter_validator: Option<ParameterValidator>,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
impl ValidatingHandler {
|
|
22
|
+
/// Create a new validating handler wrapping the inner handler with schema validators
|
|
23
|
+
pub fn new(inner: Arc<dyn Handler>, route: &crate::Route) -> Self {
|
|
24
|
+
Self {
|
|
25
|
+
inner,
|
|
26
|
+
request_validator: route.request_validator.clone(),
|
|
27
|
+
parameter_validator: route.parameter_validator.clone(),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl Handler for ValidatingHandler {
|
|
33
|
+
fn call(
|
|
34
|
+
&self,
|
|
35
|
+
req: axum::http::Request<Body>,
|
|
36
|
+
mut request_data: RequestData,
|
|
37
|
+
) -> Pin<Box<dyn Future<Output = HandlerResult> + Send + '_>> {
|
|
38
|
+
let inner = self.inner.clone();
|
|
39
|
+
let request_validator = self.request_validator.clone();
|
|
40
|
+
let parameter_validator = self.parameter_validator.clone();
|
|
41
|
+
|
|
42
|
+
Box::pin(async move {
|
|
43
|
+
if let Some(validator) = request_validator {
|
|
44
|
+
if request_data.body.is_null() && request_data.raw_body.is_some() {
|
|
45
|
+
let raw_bytes = request_data.raw_body.as_ref().unwrap();
|
|
46
|
+
request_data.body = serde_json::from_slice::<Value>(raw_bytes)
|
|
47
|
+
.map_err(|e| (axum::http::StatusCode::BAD_REQUEST, format!("Invalid JSON: {}", e)))?;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if let Err(errors) = validator.validate(&request_data.body) {
|
|
51
|
+
let problem = ProblemDetails::from_validation_error(&errors);
|
|
52
|
+
let body = problem.to_json().unwrap_or_else(|_| "{}".to_string());
|
|
53
|
+
return Err((problem.status_code(), body));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if let Some(validator) = parameter_validator {
|
|
58
|
+
let raw_query_strings: HashMap<String, String> = request_data
|
|
59
|
+
.raw_query_params
|
|
60
|
+
.iter()
|
|
61
|
+
.filter_map(|(k, v)| v.first().map(|value| (k.clone(), value.clone())))
|
|
62
|
+
.collect();
|
|
63
|
+
|
|
64
|
+
if let Err(errors) = validator.validate_and_extract(
|
|
65
|
+
&request_data.query_params,
|
|
66
|
+
&raw_query_strings,
|
|
67
|
+
&request_data.path_params,
|
|
68
|
+
&request_data.headers,
|
|
69
|
+
&request_data.cookies,
|
|
70
|
+
) {
|
|
71
|
+
let problem = ProblemDetails::from_validation_error(&errors);
|
|
72
|
+
let body = problem.to_json().unwrap_or_else(|_| "{}".to_string());
|
|
73
|
+
return Err((problem.status_code(), body));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
inner.call(req, request_data).await
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
//! Lifecycle hooks execution logic
|
|
2
|
+
|
|
3
|
+
use crate::handler_trait::Handler;
|
|
4
|
+
use axum::body::Body;
|
|
5
|
+
use axum::http::StatusCode;
|
|
6
|
+
use std::sync::Arc;
|
|
7
|
+
|
|
8
|
+
/// Execute a handler with lifecycle hooks
|
|
9
|
+
///
|
|
10
|
+
/// This wraps the handler execution with lifecycle hooks at appropriate points:
|
|
11
|
+
/// 1. preValidation hooks (before handler, which does validation)
|
|
12
|
+
/// 2. preHandler hooks (after validation, before handler)
|
|
13
|
+
/// 3. Handler execution
|
|
14
|
+
/// 4. onResponse hooks (after successful handler execution)
|
|
15
|
+
/// 5. onError hooks (if handler or any hook fails)
|
|
16
|
+
pub async fn execute_with_lifecycle_hooks(
|
|
17
|
+
req: axum::http::Request<Body>,
|
|
18
|
+
request_data: crate::handler_trait::RequestData,
|
|
19
|
+
handler: Arc<dyn Handler>,
|
|
20
|
+
hooks: Option<Arc<crate::LifecycleHooks>>,
|
|
21
|
+
) -> Result<axum::http::Response<Body>, (axum::http::StatusCode, String)> {
|
|
22
|
+
use crate::lifecycle::HookResult;
|
|
23
|
+
|
|
24
|
+
let Some(hooks) = hooks else {
|
|
25
|
+
return handler.call(req, request_data).await;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if hooks.is_empty() {
|
|
29
|
+
return handler.call(req, request_data).await;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let req = match hooks.execute_pre_validation(req).await {
|
|
33
|
+
Ok(HookResult::Continue(r)) => r,
|
|
34
|
+
Ok(HookResult::ShortCircuit(response)) => return Ok(response),
|
|
35
|
+
Err(e) => {
|
|
36
|
+
let error_response = axum::http::Response::builder()
|
|
37
|
+
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
38
|
+
.body(Body::from(format!(
|
|
39
|
+
"{{\"error\":\"preValidation hook failed: {}\"}}",
|
|
40
|
+
e
|
|
41
|
+
)))
|
|
42
|
+
.unwrap();
|
|
43
|
+
|
|
44
|
+
return match hooks.execute_on_error(error_response).await {
|
|
45
|
+
Ok(resp) => Ok(resp),
|
|
46
|
+
Err(_) => Ok(axum::http::Response::builder()
|
|
47
|
+
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
48
|
+
.body(Body::from("{\"error\":\"Hook execution failed\"}"))
|
|
49
|
+
.unwrap()),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let req = match hooks.execute_pre_handler(req).await {
|
|
55
|
+
Ok(HookResult::Continue(r)) => r,
|
|
56
|
+
Ok(HookResult::ShortCircuit(response)) => return Ok(response),
|
|
57
|
+
Err(e) => {
|
|
58
|
+
let error_response = axum::http::Response::builder()
|
|
59
|
+
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
60
|
+
.body(Body::from(format!("{{\"error\":\"preHandler hook failed: {}\"}}", e)))
|
|
61
|
+
.unwrap();
|
|
62
|
+
|
|
63
|
+
return match hooks.execute_on_error(error_response).await {
|
|
64
|
+
Ok(resp) => Ok(resp),
|
|
65
|
+
Err(_) => Ok(axum::http::Response::builder()
|
|
66
|
+
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
67
|
+
.body(Body::from("{\"error\":\"Hook execution failed\"}"))
|
|
68
|
+
.unwrap()),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
let response = match handler.call(req, request_data).await {
|
|
74
|
+
Ok(resp) => resp,
|
|
75
|
+
Err((status, message)) => {
|
|
76
|
+
let error_response = axum::http::Response::builder()
|
|
77
|
+
.status(status)
|
|
78
|
+
.body(Body::from(message))
|
|
79
|
+
.unwrap();
|
|
80
|
+
|
|
81
|
+
return match hooks.execute_on_error(error_response).await {
|
|
82
|
+
Ok(resp) => Ok(resp),
|
|
83
|
+
Err(e) => Ok(axum::http::Response::builder()
|
|
84
|
+
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
85
|
+
.body(Body::from(format!("{{\"error\":\"onError hook failed: {}\"}}", e)))
|
|
86
|
+
.unwrap()),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
match hooks.execute_on_response(response).await {
|
|
92
|
+
Ok(resp) => Ok(resp),
|
|
93
|
+
Err(e) => Ok(axum::http::Response::builder()
|
|
94
|
+
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
95
|
+
.body(Body::from(format!("{{\"error\":\"onResponse hook failed: {}\"}}", e)))
|
|
96
|
+
.unwrap()),
|
|
97
|
+
}
|
|
98
|
+
}
|