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
|
@@ -88,6 +88,9 @@ impl ResolvedDependencies {
|
|
|
88
88
|
/// let config = Arc::new("production".to_string());
|
|
89
89
|
/// resolved.insert("config".to_string(), config);
|
|
90
90
|
/// ```
|
|
91
|
+
///
|
|
92
|
+
/// # Panics
|
|
93
|
+
/// Panics if the lock is poisoned.
|
|
91
94
|
pub fn insert(&mut self, key: String, value: Arc<dyn Any + Send + Sync>) {
|
|
92
95
|
self.dependencies.lock().unwrap().insert(key, value);
|
|
93
96
|
}
|
|
@@ -126,6 +129,10 @@ impl ResolvedDependencies {
|
|
|
126
129
|
/// let missing: Option<Arc<i32>> = resolved.get("missing");
|
|
127
130
|
/// assert!(missing.is_none());
|
|
128
131
|
/// ```
|
|
132
|
+
///
|
|
133
|
+
/// # Panics
|
|
134
|
+
/// Panics if the lock is poisoned.
|
|
135
|
+
#[must_use]
|
|
129
136
|
pub fn get<T: Send + Sync + 'static>(&self, key: &str) -> Option<Arc<T>> {
|
|
130
137
|
self.dependencies
|
|
131
138
|
.lock()
|
|
@@ -155,6 +162,10 @@ impl ResolvedDependencies {
|
|
|
155
162
|
/// let any_ref = resolved.get_arc("data");
|
|
156
163
|
/// assert!(any_ref.is_some());
|
|
157
164
|
/// ```
|
|
165
|
+
///
|
|
166
|
+
/// # Panics
|
|
167
|
+
/// Panics if the lock is poisoned.
|
|
168
|
+
#[must_use]
|
|
158
169
|
pub fn get_arc(&self, key: &str) -> Option<Arc<dyn Any + Send + Sync>> {
|
|
159
170
|
self.dependencies.lock().unwrap().get(key).cloned()
|
|
160
171
|
}
|
|
@@ -177,6 +188,9 @@ impl ResolvedDependencies {
|
|
|
177
188
|
/// assert!(resolved.contains("exists"));
|
|
178
189
|
/// assert!(!resolved.contains("missing"));
|
|
179
190
|
/// ```
|
|
191
|
+
///
|
|
192
|
+
/// # Panics
|
|
193
|
+
/// Panics if the lock is poisoned.
|
|
180
194
|
#[must_use]
|
|
181
195
|
pub fn contains(&self, key: &str) -> bool {
|
|
182
196
|
self.dependencies.lock().unwrap().contains_key(key)
|
|
@@ -202,6 +216,9 @@ impl ResolvedDependencies {
|
|
|
202
216
|
/// assert!(keys.contains(&"config".to_string()));
|
|
203
217
|
/// assert!(keys.contains(&"db".to_string()));
|
|
204
218
|
/// ```
|
|
219
|
+
///
|
|
220
|
+
/// # Panics
|
|
221
|
+
/// Panics if the lock is poisoned.
|
|
205
222
|
#[must_use]
|
|
206
223
|
pub fn keys(&self) -> Vec<String> {
|
|
207
224
|
self.dependencies.lock().unwrap().keys().cloned().collect()
|
|
@@ -233,6 +250,9 @@ impl ResolvedDependencies {
|
|
|
233
250
|
/// resolved.cleanup().await;
|
|
234
251
|
/// # });
|
|
235
252
|
/// ```
|
|
253
|
+
///
|
|
254
|
+
/// # Panics
|
|
255
|
+
/// Panics if the lock is poisoned.
|
|
236
256
|
pub fn add_cleanup_task(&self, task: CleanupTask) {
|
|
237
257
|
self.cleanup_tasks.lock().unwrap().push(task);
|
|
238
258
|
}
|
|
@@ -275,6 +295,9 @@ impl ResolvedDependencies {
|
|
|
275
295
|
/// assert_eq!(*order.lock().unwrap(), vec![2, 1]);
|
|
276
296
|
/// # });
|
|
277
297
|
/// ```
|
|
298
|
+
///
|
|
299
|
+
/// # Panics
|
|
300
|
+
/// Panics if the lock is poisoned.
|
|
278
301
|
pub async fn cleanup(self) {
|
|
279
302
|
let tasks = {
|
|
280
303
|
let mut cleanup_tasks = self.cleanup_tasks.lock().unwrap();
|
|
@@ -134,6 +134,7 @@ impl<T: Clone + Send + Sync + 'static> std::fmt::Debug for ValueDependency<T> {
|
|
|
134
134
|
f.debug_struct("ValueDependency")
|
|
135
135
|
.field("key", &self.key)
|
|
136
136
|
.field("value_type", &std::any::type_name::<T>())
|
|
137
|
+
.field("value", &"<T>")
|
|
137
138
|
.finish()
|
|
138
139
|
}
|
|
139
140
|
}
|
|
@@ -31,6 +31,9 @@ impl StructuredError {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/// Catch panics and convert to a structured error so they don't cross FFI boundaries.
|
|
34
|
+
///
|
|
35
|
+
/// # Errors
|
|
36
|
+
/// Returns a structured error if a panic occurs during function execution.
|
|
34
37
|
pub fn shield_panic<T, F>(f: F) -> Result<T, StructuredError>
|
|
35
38
|
where
|
|
36
39
|
F: FnOnce() -> T + UnwindSafe,
|
|
@@ -15,16 +15,17 @@ pub enum Method {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
impl Method {
|
|
18
|
-
|
|
18
|
+
#[must_use]
|
|
19
|
+
pub const fn as_str(&self) -> &'static str {
|
|
19
20
|
match self {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
Self::Get => "GET",
|
|
22
|
+
Self::Post => "POST",
|
|
23
|
+
Self::Put => "PUT",
|
|
24
|
+
Self::Patch => "PATCH",
|
|
25
|
+
Self::Delete => "DELETE",
|
|
26
|
+
Self::Head => "HEAD",
|
|
27
|
+
Self::Options => "OPTIONS",
|
|
28
|
+
Self::Trace => "TRACE",
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
}
|
|
@@ -40,15 +41,15 @@ impl std::str::FromStr for Method {
|
|
|
40
41
|
|
|
41
42
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
42
43
|
match s.to_uppercase().as_str() {
|
|
43
|
-
"GET" => Ok(
|
|
44
|
-
"POST" => Ok(
|
|
45
|
-
"PUT" => Ok(
|
|
46
|
-
"PATCH" => Ok(
|
|
47
|
-
"DELETE" => Ok(
|
|
48
|
-
"HEAD" => Ok(
|
|
49
|
-
"OPTIONS" => Ok(
|
|
50
|
-
"TRACE" => Ok(
|
|
51
|
-
_ => Err(format!("Unknown HTTP method: {}"
|
|
44
|
+
"GET" => Ok(Self::Get),
|
|
45
|
+
"POST" => Ok(Self::Post),
|
|
46
|
+
"PUT" => Ok(Self::Put),
|
|
47
|
+
"PATCH" => Ok(Self::Patch),
|
|
48
|
+
"DELETE" => Ok(Self::Delete),
|
|
49
|
+
"HEAD" => Ok(Self::Head),
|
|
50
|
+
"OPTIONS" => Ok(Self::Options),
|
|
51
|
+
"TRACE" => Ok(Self::Trace),
|
|
52
|
+
_ => Err(format!("Unknown HTTP method: {s}")),
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
}
|
|
@@ -29,10 +29,10 @@ pub trait NativeLifecycleHook<Req, Resp>: Send + Sync {
|
|
|
29
29
|
fn name(&self) -> &str;
|
|
30
30
|
|
|
31
31
|
/// Execute hook with a request
|
|
32
|
-
fn execute_request<'a>(&
|
|
32
|
+
fn execute_request<'a>(&self, req: Req) -> RequestHookFutureSend<'a, Req, Resp>;
|
|
33
33
|
|
|
34
34
|
/// Execute hook with a response
|
|
35
|
-
fn execute_response<'a>(&
|
|
35
|
+
fn execute_response<'a>(&self, resp: Resp) -> ResponseHookFutureSend<'a, Resp>;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/// Trait for lifecycle hooks on local (wasm) targets (no Send requirements).
|
|
@@ -41,10 +41,10 @@ pub trait LocalLifecycleHook<Req, Resp> {
|
|
|
41
41
|
fn name(&self) -> &str;
|
|
42
42
|
|
|
43
43
|
/// Execute hook with a request
|
|
44
|
-
fn execute_request<'a>(&
|
|
44
|
+
fn execute_request<'a>(&self, req: Req) -> RequestHookFutureLocal<'a, Req, Resp>;
|
|
45
45
|
|
|
46
46
|
/// Execute hook with a response
|
|
47
|
-
fn execute_response<'a>(&
|
|
47
|
+
fn execute_response<'a>(&self, resp: Resp) -> ResponseHookFutureLocal<'a, Resp>;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
#[cfg(target_arch = "wasm32")]
|
|
@@ -97,17 +97,19 @@ impl<Req, Resp> std::fmt::Debug for LifecycleHooks<Req, Resp> {
|
|
|
97
97
|
|
|
98
98
|
impl<Req, Resp> LifecycleHooks<Req, Resp> {
|
|
99
99
|
/// Create a new empty hooks container
|
|
100
|
+
#[must_use]
|
|
100
101
|
pub fn new() -> Self {
|
|
101
102
|
Self::default()
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
/// Builder constructor for ergonomic hook registration
|
|
106
|
+
#[must_use]
|
|
105
107
|
pub fn builder() -> LifecycleHooksBuilder<Req, Resp> {
|
|
106
108
|
LifecycleHooksBuilder::new()
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
/// Check if any hooks are registered
|
|
110
|
-
#[
|
|
112
|
+
#[must_use]
|
|
111
113
|
pub fn is_empty(&self) -> bool {
|
|
112
114
|
self.on_request.is_empty()
|
|
113
115
|
&& self.pre_validation.is_empty()
|
|
@@ -136,6 +138,8 @@ impl<Req, Resp> LifecycleHooks<Req, Resp> {
|
|
|
136
138
|
self.on_error.push(hook);
|
|
137
139
|
}
|
|
138
140
|
|
|
141
|
+
/// # Errors
|
|
142
|
+
/// Returns an error string if a hook execution fails.
|
|
139
143
|
pub async fn execute_on_request(&self, mut req: Req) -> Result<HookResult<Req, Resp>, String> {
|
|
140
144
|
if self.on_request.is_empty() {
|
|
141
145
|
return Ok(HookResult::Continue(req));
|
|
@@ -151,6 +155,8 @@ impl<Req, Resp> LifecycleHooks<Req, Resp> {
|
|
|
151
155
|
Ok(HookResult::Continue(req))
|
|
152
156
|
}
|
|
153
157
|
|
|
158
|
+
/// # Errors
|
|
159
|
+
/// Returns an error string if a hook execution fails.
|
|
154
160
|
pub async fn execute_pre_validation(&self, mut req: Req) -> Result<HookResult<Req, Resp>, String> {
|
|
155
161
|
if self.pre_validation.is_empty() {
|
|
156
162
|
return Ok(HookResult::Continue(req));
|
|
@@ -166,6 +172,8 @@ impl<Req, Resp> LifecycleHooks<Req, Resp> {
|
|
|
166
172
|
Ok(HookResult::Continue(req))
|
|
167
173
|
}
|
|
168
174
|
|
|
175
|
+
/// # Errors
|
|
176
|
+
/// Returns an error string if a hook execution fails.
|
|
169
177
|
pub async fn execute_pre_handler(&self, mut req: Req) -> Result<HookResult<Req, Resp>, String> {
|
|
170
178
|
if self.pre_handler.is_empty() {
|
|
171
179
|
return Ok(HookResult::Continue(req));
|
|
@@ -181,6 +189,8 @@ impl<Req, Resp> LifecycleHooks<Req, Resp> {
|
|
|
181
189
|
Ok(HookResult::Continue(req))
|
|
182
190
|
}
|
|
183
191
|
|
|
192
|
+
/// # Errors
|
|
193
|
+
/// Returns an error string if a hook execution fails.
|
|
184
194
|
pub async fn execute_on_response(&self, mut resp: Resp) -> Result<Resp, String> {
|
|
185
195
|
if self.on_response.is_empty() {
|
|
186
196
|
return Ok(resp);
|
|
@@ -188,14 +198,15 @@ impl<Req, Resp> LifecycleHooks<Req, Resp> {
|
|
|
188
198
|
|
|
189
199
|
for hook in &self.on_response {
|
|
190
200
|
match hook.execute_response(resp).await? {
|
|
191
|
-
HookResult::Continue(r) => resp = r,
|
|
192
|
-
HookResult::ShortCircuit(r) => resp = r,
|
|
201
|
+
HookResult::Continue(r) | HookResult::ShortCircuit(r) => resp = r,
|
|
193
202
|
}
|
|
194
203
|
}
|
|
195
204
|
|
|
196
205
|
Ok(resp)
|
|
197
206
|
}
|
|
198
207
|
|
|
208
|
+
/// # Errors
|
|
209
|
+
/// Returns an error string if a hook execution fails.
|
|
199
210
|
pub async fn execute_on_error(&self, mut resp: Resp) -> Result<Resp, String> {
|
|
200
211
|
if self.on_error.is_empty() {
|
|
201
212
|
return Ok(resp);
|
|
@@ -203,8 +214,7 @@ impl<Req, Resp> LifecycleHooks<Req, Resp> {
|
|
|
203
214
|
|
|
204
215
|
for hook in &self.on_error {
|
|
205
216
|
match hook.execute_response(resp).await? {
|
|
206
|
-
HookResult::Continue(r) => resp = r,
|
|
207
|
-
HookResult::ShortCircuit(r) => resp = r,
|
|
217
|
+
HookResult::Continue(r) | HookResult::ShortCircuit(r) => resp = r,
|
|
208
218
|
}
|
|
209
219
|
}
|
|
210
220
|
|
|
@@ -237,11 +247,11 @@ where
|
|
|
237
247
|
&self.name
|
|
238
248
|
}
|
|
239
249
|
|
|
240
|
-
fn execute_request<'a>(&
|
|
250
|
+
fn execute_request<'a>(&self, req: Req) -> RequestHookFutureSend<'a, Req, Resp> {
|
|
241
251
|
Box::pin((self.func)(req))
|
|
242
252
|
}
|
|
243
253
|
|
|
244
|
-
fn execute_response<'a>(&
|
|
254
|
+
fn execute_response<'a>(&self, _resp: Resp) -> ResponseHookFutureSend<'a, Resp> {
|
|
245
255
|
Box::pin(async move { Err("Request hook called with response - this is a bug".to_string()) })
|
|
246
256
|
}
|
|
247
257
|
}
|
|
@@ -258,11 +268,11 @@ where
|
|
|
258
268
|
&self.name
|
|
259
269
|
}
|
|
260
270
|
|
|
261
|
-
fn execute_request<'a>(&
|
|
271
|
+
fn execute_request<'a>(&self, req: Req) -> RequestHookFutureLocal<'a, Req, Resp> {
|
|
262
272
|
Box::pin((self.func)(req))
|
|
263
273
|
}
|
|
264
274
|
|
|
265
|
-
fn execute_response<'a>(&
|
|
275
|
+
fn execute_response<'a>(&self, _resp: Resp) -> ResponseHookFutureLocal<'a, Resp> {
|
|
266
276
|
Box::pin(async move { Err("Request hook called with response - this is a bug".to_string()) })
|
|
267
277
|
}
|
|
268
278
|
}
|
|
@@ -279,11 +289,11 @@ where
|
|
|
279
289
|
&self.name
|
|
280
290
|
}
|
|
281
291
|
|
|
282
|
-
fn execute_request<'a>(&
|
|
292
|
+
fn execute_request<'a>(&self, _req: Req) -> RequestHookFutureSend<'a, Req, Resp> {
|
|
283
293
|
Box::pin(async move { Err("Response hook called with request - this is a bug".to_string()) })
|
|
284
294
|
}
|
|
285
295
|
|
|
286
|
-
fn execute_response<'a>(&
|
|
296
|
+
fn execute_response<'a>(&self, resp: Resp) -> ResponseHookFutureSend<'a, Resp> {
|
|
287
297
|
Box::pin((self.func)(resp))
|
|
288
298
|
}
|
|
289
299
|
}
|
|
@@ -300,52 +310,66 @@ where
|
|
|
300
310
|
&self.name
|
|
301
311
|
}
|
|
302
312
|
|
|
303
|
-
fn execute_request<'a>(&
|
|
313
|
+
fn execute_request<'a>(&self, _req: Req) -> RequestHookFutureLocal<'a, Req, Resp> {
|
|
304
314
|
Box::pin(async move { Err("Response hook called with request - this is a bug".to_string()) })
|
|
305
315
|
}
|
|
306
316
|
|
|
307
|
-
fn execute_response<'a>(&
|
|
317
|
+
fn execute_response<'a>(&self, resp: Resp) -> ResponseHookFutureLocal<'a, Resp> {
|
|
308
318
|
Box::pin((self.func)(resp))
|
|
309
319
|
}
|
|
310
320
|
}
|
|
311
321
|
|
|
312
|
-
/// Builder
|
|
322
|
+
/// Builder pattern for `LifecycleHooks`
|
|
313
323
|
pub struct LifecycleHooksBuilder<Req, Resp> {
|
|
314
324
|
hooks: LifecycleHooks<Req, Resp>,
|
|
315
325
|
}
|
|
316
326
|
|
|
317
327
|
impl<Req, Resp> LifecycleHooksBuilder<Req, Resp> {
|
|
328
|
+
/// Create a new builder
|
|
329
|
+
#[must_use]
|
|
318
330
|
pub fn new() -> Self {
|
|
319
331
|
Self {
|
|
320
332
|
hooks: LifecycleHooks::default(),
|
|
321
333
|
}
|
|
322
334
|
}
|
|
323
335
|
|
|
336
|
+
/// Add an `on_request` hook
|
|
337
|
+
#[must_use]
|
|
324
338
|
pub fn on_request(mut self, hook: Arc<CoreHook<Req, Resp>>) -> Self {
|
|
325
339
|
self.hooks.add_on_request(hook);
|
|
326
340
|
self
|
|
327
341
|
}
|
|
328
342
|
|
|
343
|
+
/// Add a `pre_validation` hook
|
|
344
|
+
#[must_use]
|
|
329
345
|
pub fn pre_validation(mut self, hook: Arc<CoreHook<Req, Resp>>) -> Self {
|
|
330
346
|
self.hooks.add_pre_validation(hook);
|
|
331
347
|
self
|
|
332
348
|
}
|
|
333
349
|
|
|
350
|
+
/// Add a `pre_handler` hook
|
|
351
|
+
#[must_use]
|
|
334
352
|
pub fn pre_handler(mut self, hook: Arc<CoreHook<Req, Resp>>) -> Self {
|
|
335
353
|
self.hooks.add_pre_handler(hook);
|
|
336
354
|
self
|
|
337
355
|
}
|
|
338
356
|
|
|
357
|
+
/// Add an `on_response` hook
|
|
358
|
+
#[must_use]
|
|
339
359
|
pub fn on_response(mut self, hook: Arc<CoreHook<Req, Resp>>) -> Self {
|
|
340
360
|
self.hooks.add_on_response(hook);
|
|
341
361
|
self
|
|
342
362
|
}
|
|
343
363
|
|
|
364
|
+
/// Add an `on_error` hook
|
|
365
|
+
#[must_use]
|
|
344
366
|
pub fn on_error(mut self, hook: Arc<CoreHook<Req, Resp>>) -> Self {
|
|
345
367
|
self.hooks.add_on_error(hook);
|
|
346
368
|
self
|
|
347
369
|
}
|
|
348
370
|
|
|
371
|
+
/// Build the `LifecycleHooks` instance
|
|
372
|
+
#[must_use]
|
|
349
373
|
pub fn build(self) -> LifecycleHooks<Req, Resp> {
|
|
350
374
|
self.hooks
|
|
351
375
|
}
|
|
@@ -69,6 +69,9 @@ impl ParameterValidator {
|
|
|
69
69
|
///
|
|
70
70
|
/// The schema should describe all parameters with their types and constraints.
|
|
71
71
|
/// Each property MUST have a "source" field indicating where the parameter comes from.
|
|
72
|
+
///
|
|
73
|
+
/// # Errors
|
|
74
|
+
/// Returns an error if the schema is invalid or malformed.
|
|
72
75
|
pub fn new(schema: Value) -> Result<Self, String> {
|
|
73
76
|
let parameter_defs = Self::extract_parameter_defs(&schema)?;
|
|
74
77
|
let validation_schema = Self::create_validation_schema(&schema);
|
|
@@ -88,6 +91,7 @@ impl ParameterValidator {
|
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
/// Whether this validator needs access to request headers.
|
|
94
|
+
#[must_use]
|
|
91
95
|
pub fn requires_headers(&self) -> bool {
|
|
92
96
|
self.inner
|
|
93
97
|
.parameter_defs
|
|
@@ -96,6 +100,7 @@ impl ParameterValidator {
|
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
/// Whether this validator needs access to request cookies.
|
|
103
|
+
#[must_use]
|
|
99
104
|
pub fn requires_cookies(&self) -> bool {
|
|
100
105
|
self.inner
|
|
101
106
|
.parameter_defs
|
|
@@ -104,6 +109,7 @@ impl ParameterValidator {
|
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
/// Whether the validator has any parameter definitions.
|
|
112
|
+
#[must_use]
|
|
107
113
|
pub fn has_params(&self) -> bool {
|
|
108
114
|
!self.inner.parameter_defs.is_empty()
|
|
109
115
|
}
|
|
@@ -125,12 +131,22 @@ impl ParameterValidator {
|
|
|
125
131
|
|
|
126
132
|
for (key, child) in obj {
|
|
127
133
|
match key.as_str() {
|
|
128
|
-
// Structural keywords we support in the coercion pass.
|
|
129
|
-
"type"
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
| "
|
|
134
|
+
// Structural keywords we support in the coercion pass, and metadata keywords.
|
|
135
|
+
"type"
|
|
136
|
+
| "format"
|
|
137
|
+
| "properties"
|
|
138
|
+
| "required"
|
|
139
|
+
| "items"
|
|
140
|
+
| "additionalProperties"
|
|
141
|
+
| "title"
|
|
142
|
+
| "description"
|
|
143
|
+
| "default"
|
|
144
|
+
| "examples"
|
|
145
|
+
| "deprecated"
|
|
146
|
+
| "readOnly"
|
|
147
|
+
| "writeOnly"
|
|
148
|
+
| "$schema"
|
|
149
|
+
| "$id" => {}
|
|
134
150
|
|
|
135
151
|
// Anything else may impose constraints we don't enforce manually.
|
|
136
152
|
_ => return true,
|
|
@@ -166,15 +182,14 @@ impl ParameterValidator {
|
|
|
166
182
|
for (name, prop) in properties {
|
|
167
183
|
let source_str = prop.get("source").and_then(|s| s.as_str()).ok_or_else(|| {
|
|
168
184
|
anyhow::anyhow!("Invalid parameter schema")
|
|
169
|
-
.context(format!("Parameter '{}' missing required 'source' field"
|
|
185
|
+
.context(format!("Parameter '{name}' missing required 'source' field"))
|
|
170
186
|
.to_string()
|
|
171
187
|
})?;
|
|
172
188
|
|
|
173
189
|
let source = ParameterSource::from_str(source_str).ok_or_else(|| {
|
|
174
190
|
anyhow::anyhow!("Invalid parameter schema")
|
|
175
191
|
.context(format!(
|
|
176
|
-
"Invalid source '{}' for parameter '{}' (expected: query, path, header, or cookie)"
|
|
177
|
-
source_str, name
|
|
192
|
+
"Invalid source '{source_str}' for parameter '{name}' (expected: query, path, header, or cookie)"
|
|
178
193
|
))
|
|
179
194
|
.to_string()
|
|
180
195
|
})?;
|
|
@@ -182,7 +197,10 @@ impl ParameterValidator {
|
|
|
182
197
|
let expected_type = prop.get("type").and_then(|t| t.as_str()).map(String::from);
|
|
183
198
|
let format = prop.get("format").and_then(|f| f.as_str()).map(String::from);
|
|
184
199
|
|
|
185
|
-
let is_optional = prop
|
|
200
|
+
let is_optional = prop
|
|
201
|
+
.get("optional")
|
|
202
|
+
.and_then(serde_json::Value::as_bool)
|
|
203
|
+
.unwrap_or(false);
|
|
186
204
|
let required = required_list.contains(&name.as_str()) && !is_optional;
|
|
187
205
|
|
|
188
206
|
let (lookup_key, error_key) = if source == ParameterSource::Header {
|
|
@@ -207,6 +225,7 @@ impl ParameterValidator {
|
|
|
207
225
|
}
|
|
208
226
|
|
|
209
227
|
/// Get the underlying JSON Schema
|
|
228
|
+
#[must_use]
|
|
210
229
|
pub fn schema(&self) -> &Value {
|
|
211
230
|
&self.inner.schema
|
|
212
231
|
}
|
|
@@ -217,6 +236,10 @@ impl ParameterValidator {
|
|
|
217
236
|
/// It performs type coercion (e.g., "123" → 123) based on the schema.
|
|
218
237
|
///
|
|
219
238
|
/// Returns the validated JSON object that can be directly converted to Python kwargs.
|
|
239
|
+
///
|
|
240
|
+
/// # Errors
|
|
241
|
+
/// Returns a validation error if parameter validation fails.
|
|
242
|
+
#[allow(clippy::too_many_lines)]
|
|
220
243
|
pub fn validate_and_extract(
|
|
221
244
|
&self,
|
|
222
245
|
query_params: &Value,
|
|
@@ -279,11 +302,11 @@ impl ParameterValidator {
|
|
|
279
302
|
"Input should be a valid boolean, unable to interpret input".to_string()
|
|
280
303
|
}
|
|
281
304
|
Some("string") => match item_format {
|
|
282
|
-
Some("uuid") => format!("Input should be a valid UUID, {}"
|
|
283
|
-
Some("date") => format!("Input should be a valid date, {}"
|
|
284
|
-
Some("date-time") => format!("Input should be a valid datetime, {}"
|
|
285
|
-
Some("time") => format!("Input should be a valid time, {}"
|
|
286
|
-
Some("duration") => format!("Input should be a valid duration, {}"
|
|
305
|
+
Some("uuid") => format!("Input should be a valid UUID, {e}"),
|
|
306
|
+
Some("date") => format!("Input should be a valid date, {e}"),
|
|
307
|
+
Some("date-time") => format!("Input should be a valid datetime, {e}"),
|
|
308
|
+
Some("time") => format!("Input should be a valid time, {e}"),
|
|
309
|
+
Some("duration") => format!("Input should be a valid duration, {e}"),
|
|
287
310
|
_ => e,
|
|
288
311
|
},
|
|
289
312
|
_ => e,
|
|
@@ -303,6 +326,7 @@ impl ParameterValidator {
|
|
|
303
326
|
};
|
|
304
327
|
let (item_type, item_format) = self.array_item_type_and_format(¶m_def.name);
|
|
305
328
|
|
|
329
|
+
#[allow(clippy::option_if_let_else)]
|
|
306
330
|
let coerced_items = match array_value.as_array() {
|
|
307
331
|
Some(items) => {
|
|
308
332
|
let mut out = Vec::with_capacity(items.len());
|
|
@@ -332,11 +356,11 @@ impl ParameterValidator {
|
|
|
332
356
|
Some("number") => "Input should be a valid number, unable to parse string as a number".to_string(),
|
|
333
357
|
Some("boolean") => "Input should be a valid boolean, unable to interpret input".to_string(),
|
|
334
358
|
Some("string") => match item_format {
|
|
335
|
-
Some("uuid") => format!("Input should be a valid UUID, {}"
|
|
336
|
-
Some("date") => format!("Input should be a valid date, {}"
|
|
337
|
-
Some("date-time") => format!("Input should be a valid datetime, {}"
|
|
338
|
-
Some("time") => format!("Input should be a valid time, {}"
|
|
339
|
-
Some("duration") => format!("Input should be a valid duration, {}"
|
|
359
|
+
Some("uuid") => format!("Input should be a valid UUID, {e}"),
|
|
360
|
+
Some("date") => format!("Input should be a valid date, {e}"),
|
|
361
|
+
Some("date-time") => format!("Input should be a valid datetime, {e}"),
|
|
362
|
+
Some("time") => format!("Input should be a valid time, {e}"),
|
|
363
|
+
Some("duration") => format!("Input should be a valid duration, {e}"),
|
|
340
364
|
_ => e.clone(),
|
|
341
365
|
},
|
|
342
366
|
_ => e.clone(),
|
|
@@ -410,19 +434,19 @@ impl ParameterValidator {
|
|
|
410
434
|
"Input should be a valid boolean, unable to interpret input".to_string(),
|
|
411
435
|
),
|
|
412
436
|
(Some("string"), Some("uuid")) => {
|
|
413
|
-
("uuid_parsing", format!("Input should be a valid UUID, {}"
|
|
437
|
+
("uuid_parsing", format!("Input should be a valid UUID, {e}"))
|
|
414
438
|
}
|
|
415
439
|
(Some("string"), Some("date")) => {
|
|
416
|
-
("date_parsing", format!("Input should be a valid date, {}"
|
|
440
|
+
("date_parsing", format!("Input should be a valid date, {e}"))
|
|
417
441
|
}
|
|
418
442
|
(Some("string"), Some("date-time")) => {
|
|
419
|
-
("datetime_parsing", format!("Input should be a valid datetime, {}"
|
|
443
|
+
("datetime_parsing", format!("Input should be a valid datetime, {e}"))
|
|
420
444
|
}
|
|
421
445
|
(Some("string"), Some("time")) => {
|
|
422
|
-
("time_parsing", format!("Input should be a valid time, {}"
|
|
446
|
+
("time_parsing", format!("Input should be a valid time, {e}"))
|
|
423
447
|
}
|
|
424
448
|
(Some("string"), Some("duration")) => {
|
|
425
|
-
("duration_parsing", format!("Input should be a valid duration, {}"
|
|
449
|
+
("duration_parsing", format!("Input should be a valid duration, {e}"))
|
|
426
450
|
}
|
|
427
451
|
_ => ("type_error", e),
|
|
428
452
|
};
|
|
@@ -445,7 +469,7 @@ impl ParameterValidator {
|
|
|
445
469
|
let params_json = Value::Object(params_map);
|
|
446
470
|
if let Some(schema_validator) = &self.inner.schema_validator {
|
|
447
471
|
match schema_validator.validate(¶ms_json) {
|
|
448
|
-
Ok(
|
|
472
|
+
Ok(()) => Ok(params_json),
|
|
449
473
|
Err(mut validation_err) => {
|
|
450
474
|
for error in &mut validation_err.errors {
|
|
451
475
|
if error.loc.len() >= 2 && error.loc[0] == "body" {
|
|
@@ -459,7 +483,7 @@ impl ParameterValidator {
|
|
|
459
483
|
};
|
|
460
484
|
error.loc[0] = source_str.to_string();
|
|
461
485
|
if param_def.source == ParameterSource::Header {
|
|
462
|
-
error.loc[1]
|
|
486
|
+
error.loc[1].clone_from(¶m_def.error_key);
|
|
463
487
|
}
|
|
464
488
|
if let Some(raw_value) =
|
|
465
489
|
self.raw_value_for_error(param_def, raw_query_params, path_params, headers, cookies)
|
|
@@ -477,6 +501,7 @@ impl ParameterValidator {
|
|
|
477
501
|
}
|
|
478
502
|
}
|
|
479
503
|
|
|
504
|
+
#[allow(clippy::unused_self)]
|
|
480
505
|
fn raw_value_for_error<'a>(
|
|
481
506
|
&self,
|
|
482
507
|
param_def: &ParameterDef,
|
|
@@ -485,6 +510,7 @@ impl ParameterValidator {
|
|
|
485
510
|
headers: &'a HashMap<String, String>,
|
|
486
511
|
cookies: &'a HashMap<String, String>,
|
|
487
512
|
) -> Option<&'a str> {
|
|
513
|
+
#[allow(clippy::too_many_arguments)]
|
|
488
514
|
match param_def.source {
|
|
489
515
|
ParameterSource::Query => raw_query_params
|
|
490
516
|
.get(¶m_def.lookup_key)
|
|
@@ -548,11 +574,11 @@ impl ParameterValidator {
|
|
|
548
574
|
Some("integer") => value
|
|
549
575
|
.parse::<i64>()
|
|
550
576
|
.map(|i| json!(i))
|
|
551
|
-
.map_err(|e| format!("Invalid integer: {}"
|
|
577
|
+
.map_err(|e| format!("Invalid integer: {e}")),
|
|
552
578
|
Some("number") => value
|
|
553
579
|
.parse::<f64>()
|
|
554
580
|
.map(|f| json!(f))
|
|
555
|
-
.map_err(|e| format!("Invalid number: {}"
|
|
581
|
+
.map_err(|e| format!("Invalid number: {e}")),
|
|
556
582
|
Some("boolean") => {
|
|
557
583
|
if value.is_empty() {
|
|
558
584
|
return Ok(json!(false));
|
|
@@ -563,7 +589,7 @@ impl ParameterValidator {
|
|
|
563
589
|
} else if value_lower == "false" || value == "0" {
|
|
564
590
|
Ok(json!(false))
|
|
565
591
|
} else {
|
|
566
|
-
Err(format!("Invalid boolean: {}"
|
|
592
|
+
Err(format!("Invalid boolean: {value}"))
|
|
567
593
|
}
|
|
568
594
|
}
|
|
569
595
|
_ => Ok(json!(value)),
|
|
@@ -574,7 +600,7 @@ impl ParameterValidator {
|
|
|
574
600
|
fn validate_date_format(value: &str) -> Result<(), String> {
|
|
575
601
|
jiff::civil::Date::strptime("%Y-%m-%d", value)
|
|
576
602
|
.map(|_| ())
|
|
577
|
-
.map_err(|e| format!("Invalid date format: {}"
|
|
603
|
+
.map_err(|e| format!("Invalid date format: {e}"))
|
|
578
604
|
}
|
|
579
605
|
|
|
580
606
|
/// Validate ISO 8601 datetime format
|
|
@@ -582,7 +608,7 @@ impl ParameterValidator {
|
|
|
582
608
|
use std::str::FromStr;
|
|
583
609
|
jiff::Timestamp::from_str(value)
|
|
584
610
|
.map(|_| ())
|
|
585
|
-
.map_err(|e| format!("Invalid datetime format: {}"
|
|
611
|
+
.map_err(|e| format!("Invalid datetime format: {e}"))
|
|
586
612
|
}
|
|
587
613
|
|
|
588
614
|
/// Validate ISO 8601 time format: HH:MM:SS or HH:MM:SS.ffffff
|
|
@@ -608,7 +634,7 @@ impl ParameterValidator {
|
|
|
608
634
|
};
|
|
609
635
|
|
|
610
636
|
let base_time = time_part.split('.').next().unwrap_or(time_part);
|
|
611
|
-
jiff::civil::Time::strptime("%H:%M:%S", base_time).map_err(|e| format!("Invalid time format: {}"
|
|
637
|
+
jiff::civil::Time::strptime("%H:%M:%S", base_time).map_err(|e| format!("Invalid time format: {e}"))?;
|
|
612
638
|
|
|
613
639
|
if let Some((_, frac)) = time_part.split_once('.')
|
|
614
640
|
&& (frac.is_empty() || frac.len() > 9 || !frac.chars().all(|c| c.is_ascii_digit()))
|
|
@@ -648,7 +674,7 @@ impl ParameterValidator {
|
|
|
648
674
|
use std::str::FromStr;
|
|
649
675
|
jiff::Span::from_str(value)
|
|
650
676
|
.map(|_| ())
|
|
651
|
-
.map_err(|e| format!("Invalid duration format: {}"
|
|
677
|
+
.map_err(|e| format!("Invalid duration format: {e}"))
|
|
652
678
|
}
|
|
653
679
|
|
|
654
680
|
/// Validate UUID format
|
|
@@ -671,7 +697,7 @@ impl ParameterValidator {
|
|
|
671
697
|
for (name, prop) in properties.iter_mut() {
|
|
672
698
|
if let Some(obj) = prop.as_object_mut() {
|
|
673
699
|
obj.remove("source");
|
|
674
|
-
if obj.get("optional").and_then(
|
|
700
|
+
if obj.get("optional").and_then(serde_json::Value::as_bool) == Some(true) {
|
|
675
701
|
optional_fields.push(name.clone());
|
|
676
702
|
}
|
|
677
703
|
obj.remove("optional");
|