spikard 0.8.2 → 0.8.3
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/ext/spikard_rb/Cargo.lock +6 -6
- data/ext/spikard_rb/Cargo.toml +1 -1
- data/lib/spikard/version.rb +1 -1
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +9 -1
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +61 -23
- data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +16 -0
- data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +1 -1
- data/vendor/crates/spikard-bindings-shared/src/error_response.rs +22 -19
- data/vendor/crates/spikard-bindings-shared/src/grpc_metadata.rs +14 -12
- data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +15 -6
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +6 -0
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +42 -36
- data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +6 -1
- data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +18 -6
- data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +28 -10
- data/vendor/crates/spikard-core/Cargo.toml +9 -1
- data/vendor/crates/spikard-core/src/bindings/response.rs +6 -9
- data/vendor/crates/spikard-core/src/debug.rs +2 -2
- data/vendor/crates/spikard-core/src/di/container.rs +1 -1
- data/vendor/crates/spikard-core/src/di/error.rs +1 -1
- data/vendor/crates/spikard-core/src/di/factory.rs +7 -3
- data/vendor/crates/spikard-core/src/di/graph.rs +1 -0
- data/vendor/crates/spikard-core/src/di/resolved.rs +23 -0
- data/vendor/crates/spikard-core/src/di/value.rs +1 -0
- data/vendor/crates/spikard-core/src/errors.rs +3 -0
- data/vendor/crates/spikard-core/src/http.rs +19 -18
- data/vendor/crates/spikard-core/src/lifecycle.rs +42 -18
- data/vendor/crates/spikard-core/src/parameters.rs +61 -35
- data/vendor/crates/spikard-core/src/problem.rs +18 -4
- data/vendor/crates/spikard-core/src/request_data.rs +9 -8
- data/vendor/crates/spikard-core/src/router.rs +20 -6
- data/vendor/crates/spikard-core/src/schema_registry.rs +23 -8
- data/vendor/crates/spikard-core/src/type_hints.rs +11 -5
- data/vendor/crates/spikard-core/src/validation/error_mapper.rs +29 -15
- data/vendor/crates/spikard-core/src/validation/mod.rs +45 -32
- data/vendor/crates/spikard-http/Cargo.toml +8 -1
- data/vendor/crates/spikard-rb/Cargo.toml +9 -1
- data/vendor/crates/spikard-rb/build.rs +1 -0
- data/vendor/crates/spikard-rb/src/lib.rs +58 -0
- data/vendor/crates/spikard-rb/src/lifecycle.rs +2 -2
- data/vendor/crates/spikard-rb-macros/Cargo.toml +9 -1
- data/vendor/crates/spikard-rb-macros/src/lib.rs +4 -5
- metadata +1 -1
|
@@ -29,6 +29,10 @@ pub enum HookResult {
|
|
|
29
29
|
/// Trait for implementing lifecycle hooks in language bindings
|
|
30
30
|
pub trait LifecycleHook: Send + Sync {
|
|
31
31
|
/// Execute the lifecycle hook
|
|
32
|
+
///
|
|
33
|
+
/// # Errors
|
|
34
|
+
///
|
|
35
|
+
/// Returns an error if hook execution fails.
|
|
32
36
|
fn execute(&self, context: serde_json::Value) -> Result<HookResult, String>;
|
|
33
37
|
|
|
34
38
|
/// Get the hook type
|
|
@@ -43,6 +47,7 @@ pub struct LifecycleConfig {
|
|
|
43
47
|
|
|
44
48
|
impl LifecycleConfig {
|
|
45
49
|
/// Create a new lifecycle configuration
|
|
50
|
+
#[must_use]
|
|
46
51
|
pub fn new() -> Self {
|
|
47
52
|
Self {
|
|
48
53
|
hooks: std::collections::HashMap::new(),
|
|
@@ -55,6 +60,7 @@ impl LifecycleConfig {
|
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
/// Get hooks for a specific type
|
|
63
|
+
#[must_use]
|
|
58
64
|
pub fn get_hooks(&self, hook_type: LifecycleHookType) -> Vec<Arc<dyn LifecycleHook>> {
|
|
59
65
|
self.hooks.get(&hook_type).cloned().unwrap_or_default()
|
|
60
66
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
//! `LanguageLifecycleHook` to provide language-specific hook invocation, while
|
|
11
11
|
//! `LifecycleExecutor` handles the common logic:
|
|
12
12
|
//!
|
|
13
|
-
//! - Hook result type handling (Continue vs ShortCircuit)
|
|
13
|
+
//! - Hook result type handling (Continue vs `ShortCircuit`)
|
|
14
14
|
//! - Response/Request building from hook results
|
|
15
15
|
//! - Error handling and conversion
|
|
16
16
|
//!
|
|
@@ -73,7 +73,8 @@ pub struct RequestModifications {
|
|
|
73
73
|
|
|
74
74
|
impl HookResultData {
|
|
75
75
|
/// Create a Continue result (pass through)
|
|
76
|
-
|
|
76
|
+
#[must_use]
|
|
77
|
+
pub const fn continue_execution() -> Self {
|
|
77
78
|
Self {
|
|
78
79
|
continue_execution: true,
|
|
79
80
|
status_code: None,
|
|
@@ -84,7 +85,8 @@ impl HookResultData {
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
/// Create a short-circuit response result
|
|
87
|
-
|
|
88
|
+
#[must_use]
|
|
89
|
+
pub const fn short_circuit(status_code: u16, body: Vec<u8>, headers: Option<HashMap<String, String>>) -> Self {
|
|
88
90
|
Self {
|
|
89
91
|
continue_execution: false,
|
|
90
92
|
status_code: Some(status_code),
|
|
@@ -95,7 +97,8 @@ impl HookResultData {
|
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
/// Create a request modification result
|
|
98
|
-
|
|
100
|
+
#[must_use]
|
|
101
|
+
pub const fn modify_request(modifications: RequestModifications) -> Self {
|
|
99
102
|
Self {
|
|
100
103
|
continue_execution: true,
|
|
101
104
|
status_code: None,
|
|
@@ -117,6 +120,10 @@ pub trait LanguageLifecycleHook: Send + Sync {
|
|
|
117
120
|
/// Prepare hook data from the incoming request/response
|
|
118
121
|
///
|
|
119
122
|
/// This should convert axum HTTP types to language-specific representations.
|
|
123
|
+
///
|
|
124
|
+
/// # Errors
|
|
125
|
+
///
|
|
126
|
+
/// Returns an error if hook data preparation fails.
|
|
120
127
|
fn prepare_hook_data(&self, req: &Request<Body>) -> Result<Self::HookData, String>;
|
|
121
128
|
|
|
122
129
|
/// Invoke the language hook and return normalized result data
|
|
@@ -139,13 +146,17 @@ pub struct LifecycleExecutor<L: LanguageLifecycleHook> {
|
|
|
139
146
|
|
|
140
147
|
impl<L: LanguageLifecycleHook> LifecycleExecutor<L> {
|
|
141
148
|
/// Create a new executor for the given hook
|
|
142
|
-
pub fn new(hook: Arc<L>) -> Self {
|
|
149
|
+
pub const fn new(hook: Arc<L>) -> Self {
|
|
143
150
|
Self { hook }
|
|
144
151
|
}
|
|
145
152
|
|
|
146
153
|
/// Execute a request hook, handling Continue/ShortCircuit semantics
|
|
147
154
|
///
|
|
148
155
|
/// Returns either the modified request or a short-circuit response.
|
|
156
|
+
///
|
|
157
|
+
/// # Errors
|
|
158
|
+
///
|
|
159
|
+
/// Returns an error if hook execution or modification fails.
|
|
149
160
|
pub async fn execute_request_hook(
|
|
150
161
|
&self,
|
|
151
162
|
req: Request<Body>,
|
|
@@ -154,12 +165,12 @@ impl<L: LanguageLifecycleHook> LifecycleExecutor<L> {
|
|
|
154
165
|
let result = self.hook.invoke_hook(hook_data).await?;
|
|
155
166
|
|
|
156
167
|
if !result.continue_execution {
|
|
157
|
-
let response =
|
|
168
|
+
let response = Self::build_response_from_hook_result(&result)?;
|
|
158
169
|
return Ok(Err(response));
|
|
159
170
|
}
|
|
160
171
|
|
|
161
172
|
if let Some(modifications) = result.request_modifications {
|
|
162
|
-
let modified_req =
|
|
173
|
+
let modified_req = Self::apply_request_modifications(req, modifications)?;
|
|
163
174
|
Ok(Ok(modified_req))
|
|
164
175
|
} else {
|
|
165
176
|
Ok(Ok(req))
|
|
@@ -170,6 +181,11 @@ impl<L: LanguageLifecycleHook> LifecycleExecutor<L> {
|
|
|
170
181
|
///
|
|
171
182
|
/// Response hooks can only continue or modify the response,
|
|
172
183
|
/// never short-circuit.
|
|
184
|
+
/// Execute the lifecycle hook on an outgoing response
|
|
185
|
+
///
|
|
186
|
+
/// # Errors
|
|
187
|
+
///
|
|
188
|
+
/// Returns an error if hook execution or response building fails.
|
|
173
189
|
pub async fn execute_response_hook(&self, resp: Response<Body>) -> Result<Response<Body>, String> {
|
|
174
190
|
let (parts, body) = resp.into_parts();
|
|
175
191
|
let body_bytes = extract_body(body).await?;
|
|
@@ -178,7 +194,7 @@ impl<L: LanguageLifecycleHook> LifecycleExecutor<L> {
|
|
|
178
194
|
.method("GET")
|
|
179
195
|
.uri("/")
|
|
180
196
|
.body(Body::empty())
|
|
181
|
-
.map_err(|e| format!("Failed to build dummy request: {}"
|
|
197
|
+
.map_err(|e| format!("Failed to build dummy request: {e}"))?;
|
|
182
198
|
|
|
183
199
|
let hook_data = self.hook.prepare_hook_data(&dummy_req)?;
|
|
184
200
|
let result = self.hook.invoke_hook(hook_data).await?;
|
|
@@ -198,7 +214,7 @@ impl<L: LanguageLifecycleHook> LifecycleExecutor<L> {
|
|
|
198
214
|
}
|
|
199
215
|
}
|
|
200
216
|
|
|
201
|
-
for (name, value) in parts.headers
|
|
217
|
+
for (name, value) in &parts.headers {
|
|
202
218
|
let key_str = name.as_str().to_lowercase();
|
|
203
219
|
if !header_mod_keys.contains(&key_str) {
|
|
204
220
|
builder = builder.header(name, value);
|
|
@@ -208,7 +224,7 @@ impl<L: LanguageLifecycleHook> LifecycleExecutor<L> {
|
|
|
208
224
|
let body = modifications.body.unwrap_or(body_bytes);
|
|
209
225
|
return builder
|
|
210
226
|
.body(Body::from(body))
|
|
211
|
-
.map_err(|e| format!("Failed to build modified response: {}"
|
|
227
|
+
.map_err(|e| format!("Failed to build modified response: {e}"));
|
|
212
228
|
}
|
|
213
229
|
|
|
214
230
|
let mut builder = Response::builder().status(parts.status);
|
|
@@ -219,14 +235,14 @@ impl<L: LanguageLifecycleHook> LifecycleExecutor<L> {
|
|
|
219
235
|
}
|
|
220
236
|
builder
|
|
221
237
|
.body(Body::from(body_bytes))
|
|
222
|
-
.map_err(|e| format!("Failed to rebuild response: {}"
|
|
238
|
+
.map_err(|e| format!("Failed to rebuild response: {e}"))
|
|
223
239
|
}
|
|
224
240
|
|
|
225
241
|
/// Build an axum Response from hook result data
|
|
226
|
-
fn build_response_from_hook_result(
|
|
242
|
+
fn build_response_from_hook_result(result: &HookResultData) -> Result<Response<Body>, String> {
|
|
227
243
|
let status_code = result.status_code.unwrap_or(200);
|
|
228
244
|
let status =
|
|
229
|
-
StatusCode::from_u16(status_code).map_err(|e| format!("Invalid status code {}: {}"
|
|
245
|
+
StatusCode::from_u16(status_code).map_err(|e| format!("Invalid status code {status_code}: {e}"))?;
|
|
230
246
|
|
|
231
247
|
let mut builder = Response::builder().status(status);
|
|
232
248
|
|
|
@@ -236,11 +252,7 @@ impl<L: LanguageLifecycleHook> LifecycleExecutor<L> {
|
|
|
236
252
|
}
|
|
237
253
|
}
|
|
238
254
|
|
|
239
|
-
if !builder
|
|
240
|
-
.headers_ref()
|
|
241
|
-
.map(|h| h.contains_key("content-type"))
|
|
242
|
-
.unwrap_or(false)
|
|
243
|
-
{
|
|
255
|
+
if !builder.headers_ref().is_some_and(|h| h.contains_key("content-type")) {
|
|
244
256
|
builder = builder.header("content-type", "application/json");
|
|
245
257
|
}
|
|
246
258
|
|
|
@@ -248,43 +260,33 @@ impl<L: LanguageLifecycleHook> LifecycleExecutor<L> {
|
|
|
248
260
|
|
|
249
261
|
builder
|
|
250
262
|
.body(Body::from(body))
|
|
251
|
-
.map_err(|e| format!("Failed to build response: {}"
|
|
263
|
+
.map_err(|e| format!("Failed to build response: {e}"))
|
|
252
264
|
}
|
|
253
265
|
|
|
254
266
|
/// Apply request modifications to a request
|
|
255
|
-
fn apply_request_modifications(
|
|
256
|
-
&self,
|
|
257
|
-
req: Request<Body>,
|
|
258
|
-
mods: RequestModifications,
|
|
259
|
-
) -> Result<Request<Body>, String> {
|
|
267
|
+
fn apply_request_modifications(req: Request<Body>, mods: RequestModifications) -> Result<Request<Body>, String> {
|
|
260
268
|
let (mut parts, body) = req.into_parts();
|
|
261
269
|
|
|
262
270
|
if let Some(method) = &mods.method {
|
|
263
|
-
parts.method = method
|
|
264
|
-
.parse()
|
|
265
|
-
.map_err(|e| format!("Invalid method '{}': {}", method, e))?;
|
|
271
|
+
parts.method = method.parse().map_err(|e| format!("Invalid method '{method}': {e}"))?;
|
|
266
272
|
}
|
|
267
273
|
|
|
268
274
|
if let Some(path) = &mods.path {
|
|
269
|
-
parts.uri = path.parse().map_err(|e| format!("Invalid path '{}': {}"
|
|
275
|
+
parts.uri = path.parse().map_err(|e| format!("Invalid path '{path}': {e}"))?;
|
|
270
276
|
}
|
|
271
277
|
|
|
272
278
|
if let Some(new_headers) = &mods.headers {
|
|
273
279
|
for (key, value) in new_headers {
|
|
274
280
|
let header_name: http::header::HeaderName =
|
|
275
|
-
key.parse().map_err(|_| format!("Invalid header name: {}"
|
|
281
|
+
key.parse().map_err(|_| format!("Invalid header name: {key}"))?;
|
|
276
282
|
let header_value: http::header::HeaderValue = value
|
|
277
283
|
.parse()
|
|
278
|
-
.map_err(|_| format!("Invalid header value for {}: {}"
|
|
284
|
+
.map_err(|_| format!("Invalid header value for {key}: {value}"))?;
|
|
279
285
|
parts.headers.insert(header_name, header_value);
|
|
280
286
|
}
|
|
281
287
|
}
|
|
282
288
|
|
|
283
|
-
let body =
|
|
284
|
-
Body::from(new_body)
|
|
285
|
-
} else {
|
|
286
|
-
body
|
|
287
|
-
};
|
|
289
|
+
let body = mods.body.map_or(body, Body::from);
|
|
288
290
|
|
|
289
291
|
Ok(Request::from_parts(parts, body))
|
|
290
292
|
}
|
|
@@ -293,13 +295,17 @@ impl<L: LanguageLifecycleHook> LifecycleExecutor<L> {
|
|
|
293
295
|
/// Extract body bytes from an axum Body
|
|
294
296
|
///
|
|
295
297
|
/// This is a helper used by lifecycle executors to read response bodies.
|
|
298
|
+
///
|
|
299
|
+
/// # Errors
|
|
300
|
+
///
|
|
301
|
+
/// Returns an error if body collection fails.
|
|
296
302
|
pub async fn extract_body(body: Body) -> Result<Vec<u8>, String> {
|
|
297
303
|
use http_body_util::BodyExt;
|
|
298
304
|
|
|
299
305
|
let bytes = body
|
|
300
306
|
.collect()
|
|
301
307
|
.await
|
|
302
|
-
.map_err(|e| format!("Failed to read body: {}"
|
|
308
|
+
.map_err(|e| format!("Failed to read body: {e}"))?
|
|
303
309
|
.to_bytes();
|
|
304
310
|
Ok(bytes.to_vec())
|
|
305
311
|
}
|
|
@@ -12,6 +12,7 @@ pub struct ResponseBuilder {
|
|
|
12
12
|
|
|
13
13
|
impl ResponseBuilder {
|
|
14
14
|
/// Create a new response builder with default status 200 OK
|
|
15
|
+
#[must_use]
|
|
15
16
|
pub fn new() -> Self {
|
|
16
17
|
Self {
|
|
17
18
|
status: StatusCode::OK,
|
|
@@ -21,18 +22,21 @@ impl ResponseBuilder {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/// Set the HTTP status code
|
|
24
|
-
|
|
25
|
+
#[must_use]
|
|
26
|
+
pub const fn status(mut self, status: StatusCode) -> Self {
|
|
25
27
|
self.status = status;
|
|
26
28
|
self
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/// Set the response body
|
|
32
|
+
#[must_use]
|
|
30
33
|
pub fn body(mut self, body: serde_json::Value) -> Self {
|
|
31
34
|
self.body = body;
|
|
32
35
|
self
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/// Add a response header
|
|
39
|
+
#[must_use]
|
|
36
40
|
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
|
|
37
41
|
if let Ok(name) = key.into().parse::<header::HeaderName>()
|
|
38
42
|
&& let Ok(val) = value.into().parse::<header::HeaderValue>()
|
|
@@ -43,6 +47,7 @@ impl ResponseBuilder {
|
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
/// Build the response as (status, headers, body)
|
|
50
|
+
#[must_use]
|
|
46
51
|
pub fn build(self) -> (StatusCode, HeaderMap, String) {
|
|
47
52
|
let body = serde_json::to_string(&self.body).unwrap_or_else(|_| "{}".to_string());
|
|
48
53
|
(self.status, self.headers, body)
|
|
@@ -23,13 +23,15 @@ impl TestClientConfig {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/// Set the timeout in milliseconds
|
|
26
|
-
|
|
26
|
+
#[must_use]
|
|
27
|
+
pub const fn with_timeout(mut self, timeout_ms: u64) -> Self {
|
|
27
28
|
self.timeout_ms = timeout_ms;
|
|
28
29
|
self
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/// Set whether to follow redirects
|
|
32
|
-
|
|
33
|
+
#[must_use]
|
|
34
|
+
pub const fn with_follow_redirects(mut self, follow_redirects: bool) -> Self {
|
|
33
35
|
self.follow_redirects = follow_redirects;
|
|
34
36
|
self
|
|
35
37
|
}
|
|
@@ -60,7 +62,13 @@ pub struct TestResponseMetadata {
|
|
|
60
62
|
|
|
61
63
|
impl TestResponseMetadata {
|
|
62
64
|
/// Create a new test response metadata
|
|
63
|
-
|
|
65
|
+
#[must_use]
|
|
66
|
+
pub const fn new(
|
|
67
|
+
status_code: u16,
|
|
68
|
+
headers: HashMap<String, String>,
|
|
69
|
+
body_size: usize,
|
|
70
|
+
response_time_ms: u64,
|
|
71
|
+
) -> Self {
|
|
64
72
|
Self {
|
|
65
73
|
status_code,
|
|
66
74
|
headers,
|
|
@@ -70,6 +78,7 @@ impl TestResponseMetadata {
|
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
/// Get a header value by name (case-insensitive)
|
|
81
|
+
#[must_use]
|
|
73
82
|
pub fn get_header(&self, name: &str) -> Option<&String> {
|
|
74
83
|
let lower_name = name.to_lowercase();
|
|
75
84
|
self.headers
|
|
@@ -79,17 +88,20 @@ impl TestResponseMetadata {
|
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
/// Check if response was successful (2xx status code)
|
|
82
|
-
|
|
91
|
+
#[must_use]
|
|
92
|
+
pub const fn is_success(&self) -> bool {
|
|
83
93
|
self.status_code >= 200 && self.status_code < 300
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
/// Check if response was a client error (4xx status code)
|
|
87
|
-
|
|
97
|
+
#[must_use]
|
|
98
|
+
pub const fn is_client_error(&self) -> bool {
|
|
88
99
|
self.status_code >= 400 && self.status_code < 500
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
/// Check if response was a server error (5xx status code)
|
|
92
|
-
|
|
103
|
+
#[must_use]
|
|
104
|
+
pub const fn is_server_error(&self) -> bool {
|
|
93
105
|
self.status_code >= 500 && self.status_code < 600
|
|
94
106
|
}
|
|
95
107
|
}
|
|
@@ -7,29 +7,37 @@ pub struct HeaderValidator;
|
|
|
7
7
|
|
|
8
8
|
impl HeaderValidator {
|
|
9
9
|
/// Validate that required headers are present
|
|
10
|
+
///
|
|
11
|
+
/// # Errors
|
|
12
|
+
///
|
|
13
|
+
/// Returns an error if required headers are missing.
|
|
10
14
|
pub fn validate_required(headers: &[(String, String)], required: &[&str]) -> Result<(), String> {
|
|
11
15
|
let header_names: std::collections::HashSet<_> = headers.iter().map(|(k, _)| k.to_lowercase()).collect();
|
|
12
16
|
|
|
13
17
|
for req in required {
|
|
14
18
|
if !header_names.contains(&req.to_lowercase()) {
|
|
15
|
-
return Err(format!("Missing required header: {}"
|
|
19
|
+
return Err(format!("Missing required header: {req}"));
|
|
16
20
|
}
|
|
17
21
|
}
|
|
18
22
|
Ok(())
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
/// Validate header format
|
|
26
|
+
///
|
|
27
|
+
/// # Errors
|
|
28
|
+
///
|
|
29
|
+
/// Returns an error if the header format is invalid.
|
|
22
30
|
pub fn validate_format(key: &str, value: &str, format: HeaderFormat) -> Result<(), String> {
|
|
23
31
|
match format {
|
|
24
32
|
HeaderFormat::Bearer => {
|
|
25
33
|
if !value.starts_with("Bearer ") {
|
|
26
|
-
return Err(format!("{}: must start with 'Bearer '"
|
|
34
|
+
return Err(format!("{key}: must start with 'Bearer '"));
|
|
27
35
|
}
|
|
28
36
|
Ok(())
|
|
29
37
|
}
|
|
30
38
|
HeaderFormat::Json => {
|
|
31
39
|
if !value.starts_with("application/json") {
|
|
32
|
-
return Err(format!("{}: must be 'application/json'"
|
|
40
|
+
return Err(format!("{key}: must be 'application/json'"));
|
|
33
41
|
}
|
|
34
42
|
Ok(())
|
|
35
43
|
}
|
|
@@ -38,6 +46,7 @@ impl HeaderValidator {
|
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
/// Header validation formats
|
|
49
|
+
#[derive(Copy, Clone)]
|
|
41
50
|
pub enum HeaderFormat {
|
|
42
51
|
/// Bearer token format
|
|
43
52
|
Bearer,
|
|
@@ -50,6 +59,10 @@ pub struct BodyValidator;
|
|
|
50
59
|
|
|
51
60
|
impl BodyValidator {
|
|
52
61
|
/// Validate that required fields are present in a JSON object
|
|
62
|
+
///
|
|
63
|
+
/// # Errors
|
|
64
|
+
///
|
|
65
|
+
/// Returns an error if required fields are missing.
|
|
53
66
|
pub fn validate_required_fields(body: &Value, required: &[&str]) -> Result<(), String> {
|
|
54
67
|
let obj = body
|
|
55
68
|
.as_object()
|
|
@@ -57,44 +70,48 @@ impl BodyValidator {
|
|
|
57
70
|
|
|
58
71
|
for field in required {
|
|
59
72
|
if !obj.contains_key(*field) {
|
|
60
|
-
return Err(format!("Missing required field: {}"
|
|
73
|
+
return Err(format!("Missing required field: {field}"));
|
|
61
74
|
}
|
|
62
75
|
}
|
|
63
76
|
Ok(())
|
|
64
77
|
}
|
|
65
78
|
|
|
66
79
|
/// Validate field type
|
|
80
|
+
///
|
|
81
|
+
/// # Errors
|
|
82
|
+
///
|
|
83
|
+
/// Returns an error if the field type doesn't match.
|
|
67
84
|
pub fn validate_field_type(body: &Value, field: &str, expected_type: FieldType) -> Result<(), String> {
|
|
68
85
|
let obj = body
|
|
69
86
|
.as_object()
|
|
70
87
|
.ok_or_else(|| "Body must be a JSON object".to_string())?;
|
|
71
88
|
|
|
72
|
-
let value = obj.get(field).ok_or_else(|| format!("Field not found: {}"
|
|
89
|
+
let value = obj.get(field).ok_or_else(|| format!("Field not found: {field}"))?;
|
|
73
90
|
|
|
74
91
|
match expected_type {
|
|
75
92
|
FieldType::String => {
|
|
76
93
|
if !value.is_string() {
|
|
77
|
-
return Err(format!("{}: expected string"
|
|
94
|
+
return Err(format!("{field}: expected string"));
|
|
78
95
|
}
|
|
79
96
|
}
|
|
80
97
|
FieldType::Number => {
|
|
81
98
|
if !value.is_number() {
|
|
82
|
-
return Err(format!("{}: expected number"
|
|
99
|
+
return Err(format!("{field}: expected number"));
|
|
83
100
|
}
|
|
84
101
|
}
|
|
85
102
|
FieldType::Boolean => {
|
|
86
103
|
if !value.is_boolean() {
|
|
87
|
-
return Err(format!("{}: expected boolean"
|
|
104
|
+
return Err(format!("{field}: expected boolean"));
|
|
88
105
|
}
|
|
89
106
|
}
|
|
90
107
|
FieldType::Object => {
|
|
91
108
|
if !value.is_object() {
|
|
92
|
-
return Err(format!("{}: expected object"
|
|
109
|
+
return Err(format!("{field}: expected object"));
|
|
93
110
|
}
|
|
94
111
|
}
|
|
95
112
|
FieldType::Array => {
|
|
96
113
|
if !value.is_array() {
|
|
97
|
-
return Err(format!("{}: expected array"
|
|
114
|
+
return Err(format!("{field}: expected array"));
|
|
98
115
|
}
|
|
99
116
|
}
|
|
100
117
|
}
|
|
@@ -103,6 +120,7 @@ impl BodyValidator {
|
|
|
103
120
|
}
|
|
104
121
|
|
|
105
122
|
/// Field types for validation
|
|
123
|
+
#[derive(Copy, Clone)]
|
|
106
124
|
pub enum FieldType {
|
|
107
125
|
String,
|
|
108
126
|
Number,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "spikard-core"
|
|
3
|
-
version = "0.8.
|
|
3
|
+
version = "0.8.3"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
authors = ["Na'aman Hirschfeld <nhirschfeld@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -12,6 +12,14 @@ categories = ["web-programming::http-server", "web-programming", "development-to
|
|
|
12
12
|
documentation = "https://docs.rs/spikard-core"
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
|
|
15
|
+
[lints.rust]
|
|
16
|
+
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(tarpaulin_include)'] }
|
|
17
|
+
|
|
18
|
+
[lints.clippy]
|
|
19
|
+
all = { level = "deny", priority = 0 }
|
|
20
|
+
pedantic = { level = "deny", priority = 0 }
|
|
21
|
+
nursery = { level = "deny", priority = 0 }
|
|
22
|
+
|
|
15
23
|
[dependencies]
|
|
16
24
|
serde = { version = "1.0", features = ["derive"] }
|
|
17
25
|
serde_json = "1.0"
|
|
@@ -15,6 +15,8 @@ pub struct RawResponse {
|
|
|
15
15
|
|
|
16
16
|
impl RawResponse {
|
|
17
17
|
/// Construct a new response.
|
|
18
|
+
#[must_use]
|
|
19
|
+
#[allow(clippy::missing_const_for_fn)]
|
|
18
20
|
pub fn new(status: u16, headers: HashMap<String, String>, body: Vec<u8>) -> Self {
|
|
19
21
|
Self { status, headers, body }
|
|
20
22
|
}
|
|
@@ -35,19 +37,13 @@ impl RawResponse {
|
|
|
35
37
|
return;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
let accept_encoding = header_value(request_headers, "Accept-Encoding").map(
|
|
39
|
-
let accepts_brotli = accept_encoding
|
|
40
|
-
.as_ref()
|
|
41
|
-
.map(|value| value.contains("br"))
|
|
42
|
-
.unwrap_or(false);
|
|
40
|
+
let accept_encoding = header_value(request_headers, "Accept-Encoding").map(str::to_ascii_lowercase);
|
|
41
|
+
let accepts_brotli = accept_encoding.as_ref().is_some_and(|value| value.contains("br"));
|
|
43
42
|
if compression.brotli && accepts_brotli && self.try_compress_brotli(compression) {
|
|
44
43
|
return;
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
let accepts_gzip = accept_encoding
|
|
48
|
-
.as_ref()
|
|
49
|
-
.map(|value| value.contains("gzip"))
|
|
50
|
-
.unwrap_or(false);
|
|
46
|
+
let accepts_gzip = accept_encoding.as_ref().is_some_and(|value| value.contains("gzip"));
|
|
51
47
|
if compression.gzip && accepts_gzip {
|
|
52
48
|
self.try_compress_gzip(compression);
|
|
53
49
|
}
|
|
@@ -110,6 +106,7 @@ pub struct StaticAsset {
|
|
|
110
106
|
|
|
111
107
|
impl StaticAsset {
|
|
112
108
|
/// Build a response snapshot if the incoming request targets this asset.
|
|
109
|
+
#[must_use]
|
|
113
110
|
pub fn serve(&self, method: &str, normalized_path: &str) -> Option<RawResponse> {
|
|
114
111
|
if !method.eq_ignore_ascii_case("GET") && !method.eq_ignore_ascii_case("HEAD") {
|
|
115
112
|
return None;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
//! Debug logging utilities for spikard-http
|
|
2
2
|
//!
|
|
3
3
|
//! This module provides debug logging that can be enabled via:
|
|
4
|
-
//! - Building in debug mode (cfg(debug_assertions))
|
|
5
|
-
//! - Setting SPIKARD_DEBUG=1 environment variable
|
|
4
|
+
//! - Building in debug mode (`cfg(debug_assertions)`)
|
|
5
|
+
//! - Setting `SPIKARD_DEBUG=1` environment variable
|
|
6
6
|
|
|
7
7
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
8
8
|
|
|
@@ -365,7 +365,7 @@ impl std::fmt::Debug for DependencyContainer {
|
|
|
365
365
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
366
366
|
f.debug_struct("DependencyContainer")
|
|
367
367
|
.field("dependencies", &self.dependencies.keys())
|
|
368
|
-
.
|
|
368
|
+
.finish_non_exhaustive()
|
|
369
369
|
}
|
|
370
370
|
}
|
|
371
371
|
|
|
@@ -33,7 +33,7 @@ pub enum DependencyError {
|
|
|
33
33
|
/// ```
|
|
34
34
|
#[error("Circular dependency detected: {cycle:?}")]
|
|
35
35
|
CircularDependency {
|
|
36
|
-
/// The cycle of dependencies (e.g., ["A", "B", "C", "A"])
|
|
36
|
+
/// The cycle of dependencies (e.g., `["A", "B", "C", "A"]`)
|
|
37
37
|
cycle: Vec<String>,
|
|
38
38
|
},
|
|
39
39
|
|
|
@@ -134,7 +134,7 @@ impl std::fmt::Debug for FactoryDependency {
|
|
|
134
134
|
.field("dependencies", &self.dependencies)
|
|
135
135
|
.field("cacheable", &self.cacheable)
|
|
136
136
|
.field("singleton", &self.singleton)
|
|
137
|
-
.
|
|
137
|
+
.finish_non_exhaustive()
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
@@ -209,6 +209,7 @@ impl FactoryDependencyBuilder {
|
|
|
209
209
|
/// })
|
|
210
210
|
/// .build();
|
|
211
211
|
/// ```
|
|
212
|
+
#[must_use]
|
|
212
213
|
pub fn factory<F>(mut self, factory: F) -> Self
|
|
213
214
|
where
|
|
214
215
|
F: Fn(
|
|
@@ -245,6 +246,7 @@ impl FactoryDependencyBuilder {
|
|
|
245
246
|
/// })
|
|
246
247
|
/// .build();
|
|
247
248
|
/// ```
|
|
249
|
+
#[must_use]
|
|
248
250
|
pub fn depends_on(mut self, dependencies: Vec<String>) -> Self {
|
|
249
251
|
self.dependencies = dependencies;
|
|
250
252
|
self
|
|
@@ -272,7 +274,8 @@ impl FactoryDependencyBuilder {
|
|
|
272
274
|
/// .cacheable(true) // Same ID for all uses in one request
|
|
273
275
|
/// .build();
|
|
274
276
|
/// ```
|
|
275
|
-
|
|
277
|
+
#[must_use]
|
|
278
|
+
pub const fn cacheable(mut self, cacheable: bool) -> Self {
|
|
276
279
|
self.cacheable = cacheable;
|
|
277
280
|
self
|
|
278
281
|
}
|
|
@@ -300,7 +303,8 @@ impl FactoryDependencyBuilder {
|
|
|
300
303
|
/// .singleton(true) // Share across all requests
|
|
301
304
|
/// .build();
|
|
302
305
|
/// ```
|
|
303
|
-
|
|
306
|
+
#[must_use]
|
|
307
|
+
pub const fn singleton(mut self, singleton: bool) -> Self {
|
|
304
308
|
self.singleton = singleton;
|
|
305
309
|
self
|
|
306
310
|
}
|
|
@@ -133,6 +133,7 @@ impl DependencyGraph {
|
|
|
133
133
|
/// // Adding c -> [] would not
|
|
134
134
|
/// assert!(!graph.has_cycle_with("c", &[]));
|
|
135
135
|
/// ```
|
|
136
|
+
#[must_use]
|
|
136
137
|
pub fn has_cycle_with(&self, new_key: &str, new_deps: &[String]) -> bool {
|
|
137
138
|
let mut temp_graph = self.graph.clone();
|
|
138
139
|
temp_graph.insert(new_key.to_string(), new_deps.to_vec());
|