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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: add40ab33a80f54c472f6b888638ec6e3859a05d616a5233d5347c3b07ee414d
|
|
4
|
+
data.tar.gz: cf1ab4efa6bacd7edc6c23ffebd96382804754811b9e9da7e91300c6eee9af3b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4e569ced4b7aba06c0f2c590807517987e8c6dd4a37da90a0c4caf248f99d3413482a418b429e183ac02c6ca1a658c1bb99a00e17104747057ecf21e74b56a13
|
|
7
|
+
data.tar.gz: 0b87cf1a9c439c58e078d0104b6b86e8264d28b194b9044f03d2cfdc49c11a1b33c5997523ead13bd5a0a720a1600a07908d414e876798c1c3cd4613de07d30d
|
data/ext/spikard_rb/Cargo.lock
CHANGED
|
@@ -2515,7 +2515,7 @@ dependencies = [
|
|
|
2515
2515
|
|
|
2516
2516
|
[[package]]
|
|
2517
2517
|
name = "spikard-bindings-shared"
|
|
2518
|
-
version = "0.
|
|
2518
|
+
version = "0.8.2"
|
|
2519
2519
|
dependencies = [
|
|
2520
2520
|
"axum",
|
|
2521
2521
|
"http",
|
|
@@ -2532,7 +2532,7 @@ dependencies = [
|
|
|
2532
2532
|
|
|
2533
2533
|
[[package]]
|
|
2534
2534
|
name = "spikard-core"
|
|
2535
|
-
version = "0.
|
|
2535
|
+
version = "0.8.2"
|
|
2536
2536
|
dependencies = [
|
|
2537
2537
|
"anyhow",
|
|
2538
2538
|
"base64",
|
|
@@ -2556,7 +2556,7 @@ dependencies = [
|
|
|
2556
2556
|
|
|
2557
2557
|
[[package]]
|
|
2558
2558
|
name = "spikard-http"
|
|
2559
|
-
version = "0.
|
|
2559
|
+
version = "0.8.2"
|
|
2560
2560
|
dependencies = [
|
|
2561
2561
|
"anyhow",
|
|
2562
2562
|
"axum",
|
|
@@ -2603,7 +2603,7 @@ dependencies = [
|
|
|
2603
2603
|
|
|
2604
2604
|
[[package]]
|
|
2605
2605
|
name = "spikard-rb"
|
|
2606
|
-
version = "0.
|
|
2606
|
+
version = "0.8.2"
|
|
2607
2607
|
dependencies = [
|
|
2608
2608
|
"async-stream",
|
|
2609
2609
|
"axum",
|
|
@@ -2633,7 +2633,7 @@ dependencies = [
|
|
|
2633
2633
|
|
|
2634
2634
|
[[package]]
|
|
2635
2635
|
name = "spikard-rb-ext"
|
|
2636
|
-
version = "0.
|
|
2636
|
+
version = "0.8.2"
|
|
2637
2637
|
dependencies = [
|
|
2638
2638
|
"magnus",
|
|
2639
2639
|
"spikard-rb",
|
|
@@ -2641,7 +2641,7 @@ dependencies = [
|
|
|
2641
2641
|
|
|
2642
2642
|
[[package]]
|
|
2643
2643
|
name = "spikard-rb-macros"
|
|
2644
|
-
version = "0.
|
|
2644
|
+
version = "0.8.2"
|
|
2645
2645
|
dependencies = [
|
|
2646
2646
|
"proc-macro2",
|
|
2647
2647
|
"quote",
|
data/ext/spikard_rb/Cargo.toml
CHANGED
data/lib/spikard/version.rb
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "spikard-bindings-shared"
|
|
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"
|
|
7
7
|
repository = "https://github.com/Goldziher/spikard"
|
|
8
8
|
homepage = "https://github.com/Goldziher/spikard"
|
|
9
9
|
|
|
10
|
+
[lints.rust]
|
|
11
|
+
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(tarpaulin_include)'] }
|
|
12
|
+
|
|
13
|
+
[lints.clippy]
|
|
14
|
+
all = { level = "deny", priority = 0 }
|
|
15
|
+
pedantic = { level = "deny", priority = 0 }
|
|
16
|
+
nursery = { level = "deny", priority = 0 }
|
|
17
|
+
|
|
10
18
|
[dependencies]
|
|
11
19
|
serde = { version = "1.0", features = ["derive"] }
|
|
12
20
|
serde_json = "1.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//! Configuration extraction trait and implementation for language bindings
|
|
2
2
|
//!
|
|
3
|
-
//! This module provides a trait-based abstraction for extracting ServerConfig and related
|
|
3
|
+
//! This module provides a trait-based abstraction for extracting `ServerConfig` and related
|
|
4
4
|
//! configuration structs from language-specific objects (Python dicts, JavaScript objects, etc.)
|
|
5
5
|
//! without duplicating extraction logic across bindings.
|
|
6
6
|
//!
|
|
@@ -17,7 +17,7 @@ use std::collections::HashMap;
|
|
|
17
17
|
/// Trait for reading configuration from language-specific objects
|
|
18
18
|
///
|
|
19
19
|
/// Bindings implement this trait to provide unified access to configuration values
|
|
20
|
-
/// regardless of the language-specific representation (PyDict
|
|
20
|
+
/// regardless of the language-specific representation (`PyDict`, JavaScript Object, etc.).
|
|
21
21
|
pub trait ConfigSource {
|
|
22
22
|
/// Get a boolean value from the source
|
|
23
23
|
fn get_bool(&self, key: &str) -> Option<bool>;
|
|
@@ -34,7 +34,7 @@ pub trait ConfigSource {
|
|
|
34
34
|
/// Get a vector of strings from the source
|
|
35
35
|
fn get_vec_string(&self, key: &str) -> Option<Vec<String>>;
|
|
36
36
|
|
|
37
|
-
/// Get a nested ConfigSource for nested objects
|
|
37
|
+
/// Get a nested `ConfigSource` for nested objects
|
|
38
38
|
fn get_nested(&self, key: &str) -> Option<Box<dyn ConfigSource + '_>>;
|
|
39
39
|
|
|
40
40
|
/// Check if a key exists in the source
|
|
@@ -61,11 +61,15 @@ pub trait ConfigSource {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
/// Configuration extractor that works with any ConfigSource
|
|
64
|
+
/// Configuration extractor that works with any `ConfigSource`
|
|
65
65
|
pub struct ConfigExtractor;
|
|
66
66
|
|
|
67
67
|
impl ConfigExtractor {
|
|
68
|
-
/// Extract a complete ServerConfig from a ConfigSource
|
|
68
|
+
/// Extract a complete `ServerConfig` from a `ConfigSource`
|
|
69
|
+
///
|
|
70
|
+
/// # Errors
|
|
71
|
+
///
|
|
72
|
+
/// Returns an error if required configuration fields are invalid or missing.
|
|
69
73
|
pub fn extract_server_config(source: &dyn ConfigSource) -> Result<ServerConfig, String> {
|
|
70
74
|
let mut config = ServerConfig::default();
|
|
71
75
|
|
|
@@ -73,10 +77,14 @@ impl ConfigExtractor {
|
|
|
73
77
|
config.host = host;
|
|
74
78
|
}
|
|
75
79
|
|
|
76
|
-
if let Some(port) = source
|
|
77
|
-
.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
if let Some(port) = source.get_u16("port").or_else(|| {
|
|
81
|
+
source.get_u32("port").map(|p| {
|
|
82
|
+
#[allow(clippy::cast_possible_truncation)]
|
|
83
|
+
{
|
|
84
|
+
p as u16
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
}) {
|
|
80
88
|
config.port = port;
|
|
81
89
|
}
|
|
82
90
|
|
|
@@ -144,7 +152,11 @@ impl ConfigExtractor {
|
|
|
144
152
|
Ok(config)
|
|
145
153
|
}
|
|
146
154
|
|
|
147
|
-
/// Extract CompressionConfig from a ConfigSource
|
|
155
|
+
/// Extract `CompressionConfig` from a `ConfigSource`
|
|
156
|
+
///
|
|
157
|
+
/// # Errors
|
|
158
|
+
///
|
|
159
|
+
/// Returns an error if required configuration fields are invalid.
|
|
148
160
|
pub fn extract_compression_config(source: &dyn ConfigSource) -> Result<CompressionConfig, String> {
|
|
149
161
|
let gzip = source.get_bool("gzip").unwrap_or(true);
|
|
150
162
|
let brotli = source.get_bool("brotli").unwrap_or(true);
|
|
@@ -162,7 +174,11 @@ impl ConfigExtractor {
|
|
|
162
174
|
})
|
|
163
175
|
}
|
|
164
176
|
|
|
165
|
-
/// Extract RateLimitConfig from a ConfigSource
|
|
177
|
+
/// Extract `RateLimitConfig` from a `ConfigSource`
|
|
178
|
+
///
|
|
179
|
+
/// # Errors
|
|
180
|
+
///
|
|
181
|
+
/// Returns an error if required fields `per_second` or `burst` are missing.
|
|
166
182
|
pub fn extract_rate_limit_config(source: &dyn ConfigSource) -> Result<RateLimitConfig, String> {
|
|
167
183
|
let per_second = source.get_u64("per_second").ok_or("Rate limit requires 'per_second'")?;
|
|
168
184
|
|
|
@@ -177,7 +193,11 @@ impl ConfigExtractor {
|
|
|
177
193
|
})
|
|
178
194
|
}
|
|
179
195
|
|
|
180
|
-
/// Extract JwtConfig from a ConfigSource
|
|
196
|
+
/// Extract `JwtConfig` from a `ConfigSource`
|
|
197
|
+
///
|
|
198
|
+
/// # Errors
|
|
199
|
+
///
|
|
200
|
+
/// Returns an error if the required `secret` field is missing.
|
|
181
201
|
pub fn extract_jwt_config(source: &dyn ConfigSource) -> Result<JwtConfig, String> {
|
|
182
202
|
let secret = source.get_string("secret").ok_or("JWT auth requires 'secret'")?;
|
|
183
203
|
|
|
@@ -198,7 +218,11 @@ impl ConfigExtractor {
|
|
|
198
218
|
})
|
|
199
219
|
}
|
|
200
220
|
|
|
201
|
-
/// Extract ApiKeyConfig from a ConfigSource
|
|
221
|
+
/// Extract `ApiKeyConfig` from a `ConfigSource`
|
|
222
|
+
///
|
|
223
|
+
/// # Errors
|
|
224
|
+
///
|
|
225
|
+
/// Returns an error if the required `keys` field is missing.
|
|
202
226
|
pub fn extract_api_key_config(source: &dyn ConfigSource) -> Result<ApiKeyConfig, String> {
|
|
203
227
|
let keys = source
|
|
204
228
|
.get_vec_string("keys")
|
|
@@ -211,7 +235,11 @@ impl ConfigExtractor {
|
|
|
211
235
|
Ok(ApiKeyConfig { keys, header_name })
|
|
212
236
|
}
|
|
213
237
|
|
|
214
|
-
/// Extract static files configuration list from a ConfigSource
|
|
238
|
+
/// Extract static files configuration list from a `ConfigSource`
|
|
239
|
+
///
|
|
240
|
+
/// # Errors
|
|
241
|
+
///
|
|
242
|
+
/// Returns an error if array elements are invalid or missing required fields.
|
|
215
243
|
pub fn extract_static_files_config(source: &dyn ConfigSource) -> Result<Vec<StaticFilesConfig>, String> {
|
|
216
244
|
let length = source.get_array_length("static_files").unwrap_or(0);
|
|
217
245
|
if length == 0 {
|
|
@@ -247,7 +275,11 @@ impl ConfigExtractor {
|
|
|
247
275
|
Ok(configs)
|
|
248
276
|
}
|
|
249
277
|
|
|
250
|
-
/// Extract OpenApiConfig from a ConfigSource
|
|
278
|
+
/// Extract `OpenApiConfig` from a `ConfigSource`
|
|
279
|
+
///
|
|
280
|
+
/// # Errors
|
|
281
|
+
///
|
|
282
|
+
/// Returns an error if required configuration fields are invalid.
|
|
251
283
|
pub fn extract_openapi_config(source: &dyn ConfigSource) -> Result<OpenApiConfig, String> {
|
|
252
284
|
let enabled = source.get_bool("enabled").unwrap_or(false);
|
|
253
285
|
let title = source.get_string("title").unwrap_or_else(|| "API".to_string());
|
|
@@ -279,7 +311,7 @@ impl ConfigExtractor {
|
|
|
279
311
|
|
|
280
312
|
let servers = Self::extract_servers_config(source)?;
|
|
281
313
|
|
|
282
|
-
let security_schemes = Self::extract_security_schemes_config(source)
|
|
314
|
+
let security_schemes = Self::extract_security_schemes_config(source);
|
|
283
315
|
|
|
284
316
|
Ok(OpenApiConfig {
|
|
285
317
|
enabled,
|
|
@@ -296,7 +328,11 @@ impl ConfigExtractor {
|
|
|
296
328
|
})
|
|
297
329
|
}
|
|
298
330
|
|
|
299
|
-
/// Extract servers list from OpenAPI config
|
|
331
|
+
/// Extract servers list from `OpenAPI` config
|
|
332
|
+
///
|
|
333
|
+
/// # Errors
|
|
334
|
+
///
|
|
335
|
+
/// Returns an error if array elements are invalid or missing.
|
|
300
336
|
fn extract_servers_config(source: &dyn ConfigSource) -> Result<Vec<ServerInfo>, String> {
|
|
301
337
|
let length = source.get_array_length("servers").unwrap_or(0);
|
|
302
338
|
if length == 0 {
|
|
@@ -319,15 +355,17 @@ impl ConfigExtractor {
|
|
|
319
355
|
Ok(servers)
|
|
320
356
|
}
|
|
321
357
|
|
|
322
|
-
/// Extract security schemes from OpenAPI config
|
|
323
|
-
fn extract_security_schemes_config(
|
|
324
|
-
_source: &dyn ConfigSource,
|
|
325
|
-
) -> Result<HashMap<String, SecuritySchemeInfo>, String> {
|
|
358
|
+
/// Extract security schemes from `OpenAPI` config
|
|
359
|
+
fn extract_security_schemes_config(_source: &dyn ConfigSource) -> HashMap<String, SecuritySchemeInfo> {
|
|
326
360
|
// TODO: Implement when bindings support iterating HashMap-like structures
|
|
327
|
-
|
|
361
|
+
HashMap::new()
|
|
328
362
|
}
|
|
329
363
|
|
|
330
|
-
/// Extract JsonRpcConfig from a ConfigSource
|
|
364
|
+
/// Extract `JsonRpcConfig` from a `ConfigSource`
|
|
365
|
+
///
|
|
366
|
+
/// # Errors
|
|
367
|
+
///
|
|
368
|
+
/// Returns an error if required configuration fields are invalid.
|
|
331
369
|
pub fn extract_jsonrpc_config(source: &dyn ConfigSource) -> Result<JsonRpcConfig, String> {
|
|
332
370
|
let enabled = source.get_bool("enabled").unwrap_or(true);
|
|
333
371
|
let endpoint_path = source.get_string("endpoint_path").unwrap_or_else(|| "/rpc".to_string());
|
|
@@ -8,6 +8,10 @@ pub trait FromLanguage: Sized {
|
|
|
8
8
|
type Error: std::fmt::Display;
|
|
9
9
|
|
|
10
10
|
/// Convert from a language-specific value
|
|
11
|
+
///
|
|
12
|
+
/// # Errors
|
|
13
|
+
///
|
|
14
|
+
/// Returns an error if the value cannot be converted to the expected type.
|
|
11
15
|
fn from_any(value: &(dyn Any + Send + Sync)) -> Result<Self, Self::Error>;
|
|
12
16
|
}
|
|
13
17
|
|
|
@@ -17,6 +21,10 @@ pub trait ToLanguage {
|
|
|
17
21
|
type Error: std::fmt::Display;
|
|
18
22
|
|
|
19
23
|
/// Convert to a language-specific value
|
|
24
|
+
///
|
|
25
|
+
/// # Errors
|
|
26
|
+
///
|
|
27
|
+
/// Returns an error if the conversion fails.
|
|
20
28
|
fn to_any(&self) -> Result<Box<dyn Any + Send + Sync>, Self::Error>;
|
|
21
29
|
}
|
|
22
30
|
|
|
@@ -26,9 +34,17 @@ pub trait JsonConvertible: Sized {
|
|
|
26
34
|
type Error: std::fmt::Display;
|
|
27
35
|
|
|
28
36
|
/// Convert from a JSON value
|
|
37
|
+
///
|
|
38
|
+
/// # Errors
|
|
39
|
+
///
|
|
40
|
+
/// Returns an error if the JSON value is not valid for the target type.
|
|
29
41
|
fn from_json(value: serde_json::Value) -> Result<Self, Self::Error>;
|
|
30
42
|
|
|
31
43
|
/// Convert to a JSON value
|
|
44
|
+
///
|
|
45
|
+
/// # Errors
|
|
46
|
+
///
|
|
47
|
+
/// Returns an error if the conversion fails.
|
|
32
48
|
fn to_json(&self) -> Result<serde_json::Value, Self::Error>;
|
|
33
49
|
}
|
|
34
50
|
|
|
@@ -33,7 +33,7 @@ pub trait ValueDependencyAdapter: Send + Sync {
|
|
|
33
33
|
/// Adapter trait for factory dependencies across language bindings
|
|
34
34
|
///
|
|
35
35
|
/// Language bindings should implement this trait to wrap their
|
|
36
|
-
/// language-specific callable storage (e.g., Py<PyAny>, ThreadsafeFunction
|
|
36
|
+
/// language-specific callable storage (e.g., Py<PyAny>, `ThreadsafeFunction`, etc.)
|
|
37
37
|
pub trait FactoryDependencyAdapter: Send + Sync {
|
|
38
38
|
/// Get the dependency key
|
|
39
39
|
fn key(&self) -> &str;
|
|
@@ -15,7 +15,7 @@ pub struct ErrorResponseBuilder;
|
|
|
15
15
|
impl ErrorResponseBuilder {
|
|
16
16
|
/// Create a structured error response with status code and error details
|
|
17
17
|
///
|
|
18
|
-
/// Returns a tuple of (StatusCode
|
|
18
|
+
/// Returns a tuple of (`StatusCode`, JSON body as String)
|
|
19
19
|
///
|
|
20
20
|
/// # Arguments
|
|
21
21
|
/// * `status` - HTTP status code
|
|
@@ -42,7 +42,7 @@ impl ErrorResponseBuilder {
|
|
|
42
42
|
|
|
43
43
|
/// Create an error response with additional details
|
|
44
44
|
///
|
|
45
|
-
/// Returns a tuple of (StatusCode
|
|
45
|
+
/// Returns a tuple of (`StatusCode`, JSON body as String)
|
|
46
46
|
///
|
|
47
47
|
/// # Arguments
|
|
48
48
|
/// * `status` - HTTP status code
|
|
@@ -79,17 +79,18 @@ impl ErrorResponseBuilder {
|
|
|
79
79
|
(status, body)
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
/// Create an error response from a StructuredError
|
|
82
|
+
/// Create an error response from a `StructuredError`
|
|
83
83
|
///
|
|
84
|
-
/// Returns a tuple of (StatusCode
|
|
84
|
+
/// Returns a tuple of (`StatusCode`, JSON body as String)
|
|
85
85
|
///
|
|
86
86
|
/// # Arguments
|
|
87
87
|
/// * `error` - The structured error
|
|
88
88
|
///
|
|
89
89
|
/// # Note
|
|
90
|
-
/// Uses INTERNAL_SERVER_ERROR as the default status code. Override with
|
|
90
|
+
/// Uses `INTERNAL_SERVER_ERROR` as the default status code. Override with
|
|
91
91
|
/// `structured_error()` or `with_details()` for specific status codes.
|
|
92
|
-
|
|
92
|
+
#[must_use]
|
|
93
|
+
pub fn from_structured_error(error: &StructuredError) -> (StatusCode, String) {
|
|
93
94
|
let status = StatusCode::INTERNAL_SERVER_ERROR;
|
|
94
95
|
let body = serde_json::to_string(&error)
|
|
95
96
|
.unwrap_or_else(|_| r#"{"error":"serialization_failed","code":"internal_error","details":{}}"#.to_string());
|
|
@@ -98,8 +99,8 @@ impl ErrorResponseBuilder {
|
|
|
98
99
|
|
|
99
100
|
/// Create a validation error response
|
|
100
101
|
///
|
|
101
|
-
/// Converts ValidationError to RFC 9457 Problem Details format.
|
|
102
|
-
/// Returns (StatusCode::UNPROCESSABLE_ENTITY
|
|
102
|
+
/// Converts `ValidationError` to RFC 9457 Problem Details format.
|
|
103
|
+
/// Returns (`StatusCode::UNPROCESSABLE_ENTITY`, JSON body)
|
|
103
104
|
///
|
|
104
105
|
/// # Arguments
|
|
105
106
|
/// * `validation_error` - The validation error containing one or more details
|
|
@@ -124,6 +125,7 @@ impl ErrorResponseBuilder {
|
|
|
124
125
|
///
|
|
125
126
|
/// let (status, body) = ErrorResponseBuilder::validation_error(&validation_error);
|
|
126
127
|
/// ```
|
|
128
|
+
#[must_use]
|
|
127
129
|
pub fn validation_error(validation_error: &ValidationError) -> (StatusCode, String) {
|
|
128
130
|
let problem = ProblemDetails::from_validation_error(validation_error);
|
|
129
131
|
let status = problem.status_code();
|
|
@@ -136,7 +138,7 @@ impl ErrorResponseBuilder {
|
|
|
136
138
|
|
|
137
139
|
/// Create an RFC 9457 Problem Details response
|
|
138
140
|
///
|
|
139
|
-
/// Returns a tuple of (StatusCode
|
|
141
|
+
/// Returns a tuple of (`StatusCode`, JSON body as String)
|
|
140
142
|
///
|
|
141
143
|
/// # Arguments
|
|
142
144
|
/// * `problem` - The Problem Details object
|
|
@@ -150,6 +152,7 @@ impl ErrorResponseBuilder {
|
|
|
150
152
|
/// let problem = ProblemDetails::not_found("User with id 123 not found");
|
|
151
153
|
/// let (status, body) = ErrorResponseBuilder::problem_details_response(&problem);
|
|
152
154
|
/// ```
|
|
155
|
+
#[must_use]
|
|
153
156
|
pub fn problem_details_response(problem: &ProblemDetails) -> (StatusCode, String) {
|
|
154
157
|
let status = problem.status_code();
|
|
155
158
|
let body = serde_json::to_string(problem).unwrap_or_else(|_| {
|
|
@@ -161,70 +164,70 @@ impl ErrorResponseBuilder {
|
|
|
161
164
|
|
|
162
165
|
/// Create a generic bad request error
|
|
163
166
|
///
|
|
164
|
-
/// Returns (StatusCode::BAD_REQUEST
|
|
167
|
+
/// Returns (`StatusCode::BAD_REQUEST`, JSON body)
|
|
165
168
|
pub fn bad_request(message: impl Into<String>) -> (StatusCode, String) {
|
|
166
169
|
Self::structured_error(StatusCode::BAD_REQUEST, "bad_request", message)
|
|
167
170
|
}
|
|
168
171
|
|
|
169
172
|
/// Create a generic internal server error
|
|
170
173
|
///
|
|
171
|
-
/// Returns (StatusCode::INTERNAL_SERVER_ERROR
|
|
174
|
+
/// Returns (`StatusCode::INTERNAL_SERVER_ERROR`, JSON body)
|
|
172
175
|
pub fn internal_error(message: impl Into<String>) -> (StatusCode, String) {
|
|
173
176
|
Self::structured_error(StatusCode::INTERNAL_SERVER_ERROR, "internal_error", message)
|
|
174
177
|
}
|
|
175
178
|
|
|
176
179
|
/// Create an unauthorized error
|
|
177
180
|
///
|
|
178
|
-
/// Returns (StatusCode::UNAUTHORIZED
|
|
181
|
+
/// Returns (`StatusCode::UNAUTHORIZED`, JSON body)
|
|
179
182
|
pub fn unauthorized(message: impl Into<String>) -> (StatusCode, String) {
|
|
180
183
|
Self::structured_error(StatusCode::UNAUTHORIZED, "unauthorized", message)
|
|
181
184
|
}
|
|
182
185
|
|
|
183
186
|
/// Create a forbidden error
|
|
184
187
|
///
|
|
185
|
-
/// Returns (StatusCode::FORBIDDEN
|
|
188
|
+
/// Returns (`StatusCode::FORBIDDEN`, JSON body)
|
|
186
189
|
pub fn forbidden(message: impl Into<String>) -> (StatusCode, String) {
|
|
187
190
|
Self::structured_error(StatusCode::FORBIDDEN, "forbidden", message)
|
|
188
191
|
}
|
|
189
192
|
|
|
190
193
|
/// Create a not found error
|
|
191
194
|
///
|
|
192
|
-
/// Returns (StatusCode::NOT_FOUND
|
|
195
|
+
/// Returns (`StatusCode::NOT_FOUND`, JSON body)
|
|
193
196
|
pub fn not_found(message: impl Into<String>) -> (StatusCode, String) {
|
|
194
197
|
Self::structured_error(StatusCode::NOT_FOUND, "not_found", message)
|
|
195
198
|
}
|
|
196
199
|
|
|
197
200
|
/// Create a method not allowed error
|
|
198
201
|
///
|
|
199
|
-
/// Returns (StatusCode::METHOD_NOT_ALLOWED
|
|
202
|
+
/// Returns (`StatusCode::METHOD_NOT_ALLOWED`, JSON body)
|
|
200
203
|
pub fn method_not_allowed(message: impl Into<String>) -> (StatusCode, String) {
|
|
201
204
|
Self::structured_error(StatusCode::METHOD_NOT_ALLOWED, "method_not_allowed", message)
|
|
202
205
|
}
|
|
203
206
|
|
|
204
207
|
/// Create an unprocessable entity error (validation failed)
|
|
205
208
|
///
|
|
206
|
-
/// Returns (StatusCode::UNPROCESSABLE_ENTITY
|
|
209
|
+
/// Returns (`StatusCode::UNPROCESSABLE_ENTITY`, JSON body)
|
|
207
210
|
pub fn unprocessable_entity(message: impl Into<String>) -> (StatusCode, String) {
|
|
208
211
|
Self::structured_error(StatusCode::UNPROCESSABLE_ENTITY, "unprocessable_entity", message)
|
|
209
212
|
}
|
|
210
213
|
|
|
211
214
|
/// Create a conflict error
|
|
212
215
|
///
|
|
213
|
-
/// Returns (StatusCode::CONFLICT
|
|
216
|
+
/// Returns (`StatusCode::CONFLICT`, JSON body)
|
|
214
217
|
pub fn conflict(message: impl Into<String>) -> (StatusCode, String) {
|
|
215
218
|
Self::structured_error(StatusCode::CONFLICT, "conflict", message)
|
|
216
219
|
}
|
|
217
220
|
|
|
218
221
|
/// Create a service unavailable error
|
|
219
222
|
///
|
|
220
|
-
/// Returns (StatusCode::SERVICE_UNAVAILABLE
|
|
223
|
+
/// Returns (`StatusCode::SERVICE_UNAVAILABLE`, JSON body)
|
|
221
224
|
pub fn service_unavailable(message: impl Into<String>) -> (StatusCode, String) {
|
|
222
225
|
Self::structured_error(StatusCode::SERVICE_UNAVAILABLE, "service_unavailable", message)
|
|
223
226
|
}
|
|
224
227
|
|
|
225
228
|
/// Create a request timeout error
|
|
226
229
|
///
|
|
227
|
-
/// Returns (StatusCode::REQUEST_TIMEOUT
|
|
230
|
+
/// Returns (`StatusCode::REQUEST_TIMEOUT`, JSON body)
|
|
228
231
|
pub fn request_timeout(message: impl Into<String>) -> (StatusCode, String) {
|
|
229
232
|
Self::structured_error(StatusCode::REQUEST_TIMEOUT, "request_timeout", message)
|
|
230
233
|
}
|
|
@@ -6,20 +6,20 @@
|
|
|
6
6
|
use std::collections::HashMap;
|
|
7
7
|
use tonic::metadata::{MetadataKey, MetadataMap, MetadataValue};
|
|
8
8
|
|
|
9
|
-
/// Extract metadata from gRPC MetadataMap to a simple HashMap
|
|
9
|
+
/// Extract metadata from gRPC `MetadataMap` to a simple `HashMap`.
|
|
10
10
|
///
|
|
11
|
-
/// This function converts gRPC metadata to a language-agnostic HashMap format
|
|
11
|
+
/// This function converts gRPC metadata to a language-agnostic `HashMap` format
|
|
12
12
|
/// that can be easily passed to language bindings. Only ASCII metadata is
|
|
13
13
|
/// included; binary metadata is skipped with optional logging.
|
|
14
14
|
///
|
|
15
15
|
/// # Arguments
|
|
16
16
|
///
|
|
17
|
-
/// * `metadata` - The gRPC MetadataMap to extract from
|
|
17
|
+
/// * `metadata` - The gRPC `MetadataMap` to extract from
|
|
18
18
|
/// * `log_binary_skip` - Whether to log when binary metadata is skipped
|
|
19
19
|
///
|
|
20
20
|
/// # Returns
|
|
21
21
|
///
|
|
22
|
-
/// A HashMap containing all ASCII metadata key-value pairs
|
|
22
|
+
/// A `HashMap` containing all ASCII metadata key-value pairs
|
|
23
23
|
///
|
|
24
24
|
/// # Examples
|
|
25
25
|
///
|
|
@@ -55,19 +55,19 @@ pub fn extract_metadata_to_hashmap(metadata: &MetadataMap, log_binary_skip: bool
|
|
|
55
55
|
map
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
/// Convert a HashMap to gRPC MetadataMap
|
|
58
|
+
/// Convert a `HashMap` to gRPC `MetadataMap`.
|
|
59
59
|
///
|
|
60
|
-
/// This function converts a language-agnostic HashMap into a gRPC MetadataMap
|
|
60
|
+
/// This function converts a language-agnostic `HashMap` into a gRPC `MetadataMap`
|
|
61
61
|
/// that can be used in responses. All keys and values are validated and errors
|
|
62
62
|
/// are returned if any are invalid.
|
|
63
63
|
///
|
|
64
64
|
/// # Arguments
|
|
65
65
|
///
|
|
66
|
-
/// * `map` - The HashMap to convert
|
|
66
|
+
/// * `map` - The `HashMap` to convert
|
|
67
67
|
///
|
|
68
68
|
/// # Returns
|
|
69
69
|
///
|
|
70
|
-
/// A Result containing the MetadataMap or an error message
|
|
70
|
+
/// A Result containing the `MetadataMap` or an error message
|
|
71
71
|
///
|
|
72
72
|
/// # Errors
|
|
73
73
|
///
|
|
@@ -87,15 +87,17 @@ pub fn extract_metadata_to_hashmap(metadata: &MetadataMap, log_binary_skip: bool
|
|
|
87
87
|
/// let metadata = hashmap_to_metadata(&map).unwrap();
|
|
88
88
|
/// assert!(metadata.contains_key("content-type"));
|
|
89
89
|
/// ```
|
|
90
|
-
pub fn hashmap_to_metadata
|
|
90
|
+
pub fn hashmap_to_metadata<S: std::hash::BuildHasher>(
|
|
91
|
+
map: &std::collections::HashMap<String, String, S>,
|
|
92
|
+
) -> Result<MetadataMap, String> {
|
|
91
93
|
let mut metadata = MetadataMap::new();
|
|
92
94
|
|
|
93
95
|
for (key, value) in map {
|
|
94
|
-
let metadata_key =
|
|
95
|
-
.map_err(|err| format!("Invalid metadata key '{}': {}"
|
|
96
|
+
let metadata_key =
|
|
97
|
+
MetadataKey::from_bytes(key.as_bytes()).map_err(|err| format!("Invalid metadata key '{key}': {err}"))?;
|
|
96
98
|
|
|
97
99
|
let metadata_value =
|
|
98
|
-
MetadataValue::try_from(value).map_err(|err| format!("Invalid metadata value for '{}': {}"
|
|
100
|
+
MetadataValue::try_from(value).map_err(|err| format!("Invalid metadata value for '{key}': {err}"))?;
|
|
99
101
|
|
|
100
102
|
metadata.insert(metadata_key, metadata_value);
|
|
101
103
|
}
|
|
@@ -33,7 +33,7 @@ pub enum HandlerError {
|
|
|
33
33
|
|
|
34
34
|
impl From<ValidationError> for HandlerError {
|
|
35
35
|
fn from(err: ValidationError) -> Self {
|
|
36
|
-
|
|
36
|
+
Self::Validation(format!("{err:?}"))
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -54,6 +54,10 @@ pub trait LanguageHandler: Send + Sync {
|
|
|
54
54
|
type Output: Send;
|
|
55
55
|
|
|
56
56
|
/// Prepare request data for passing to the language handler
|
|
57
|
+
///
|
|
58
|
+
/// # Errors
|
|
59
|
+
///
|
|
60
|
+
/// Returns an error if request preparation fails.
|
|
57
61
|
fn prepare_request(&self, request_data: &RequestData) -> Result<Self::Input, HandlerError>;
|
|
58
62
|
|
|
59
63
|
/// Invoke the language-specific handler with the prepared input
|
|
@@ -63,6 +67,10 @@ pub trait LanguageHandler: Send + Sync {
|
|
|
63
67
|
) -> Pin<Box<dyn Future<Output = Result<Self::Output, HandlerError>> + Send + '_>>;
|
|
64
68
|
|
|
65
69
|
/// Interpret the handler's output and convert it to an HTTP response
|
|
70
|
+
///
|
|
71
|
+
/// # Errors
|
|
72
|
+
///
|
|
73
|
+
/// Returns an error if response interpretation fails.
|
|
66
74
|
fn interpret_response(&self, output: Self::Output) -> Result<Response<Body>, HandlerError>;
|
|
67
75
|
}
|
|
68
76
|
|
|
@@ -81,7 +89,7 @@ pub struct HandlerExecutor<L: LanguageHandler> {
|
|
|
81
89
|
|
|
82
90
|
impl<L: LanguageHandler> HandlerExecutor<L> {
|
|
83
91
|
/// Create a new handler executor
|
|
84
|
-
pub fn new(language_handler: Arc<L>, request_validator: Option<Arc<SchemaValidator>>) -> Self {
|
|
92
|
+
pub const fn new(language_handler: Arc<L>, request_validator: Option<Arc<SchemaValidator>>) -> Self {
|
|
85
93
|
Self {
|
|
86
94
|
language_handler,
|
|
87
95
|
request_validator,
|
|
@@ -89,7 +97,7 @@ impl<L: LanguageHandler> HandlerExecutor<L> {
|
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
/// Create a handler executor with only a language handler
|
|
92
|
-
pub fn with_handler(language_handler: Arc<L>) -> Self {
|
|
100
|
+
pub const fn with_handler(language_handler: Arc<L>) -> Self {
|
|
93
101
|
Self {
|
|
94
102
|
language_handler,
|
|
95
103
|
request_validator: None,
|
|
@@ -97,6 +105,7 @@ impl<L: LanguageHandler> HandlerExecutor<L> {
|
|
|
97
105
|
}
|
|
98
106
|
|
|
99
107
|
/// Add request validation to this executor
|
|
108
|
+
#[must_use]
|
|
100
109
|
pub fn with_request_validator(mut self, validator: Arc<SchemaValidator>) -> Self {
|
|
101
110
|
self.request_validator = Some(validator);
|
|
102
111
|
self
|
|
@@ -119,18 +128,18 @@ impl<L: LanguageHandler + 'static> Handler for HandlerExecutor<L> {
|
|
|
119
128
|
let input = self
|
|
120
129
|
.language_handler
|
|
121
130
|
.prepare_request(&request_data)
|
|
122
|
-
.map_err(|e| ErrorResponseBuilder::internal_error(format!("Failed to prepare request: {}"
|
|
131
|
+
.map_err(|e| ErrorResponseBuilder::internal_error(format!("Failed to prepare request: {e}")))?;
|
|
123
132
|
|
|
124
133
|
let output = self
|
|
125
134
|
.language_handler
|
|
126
135
|
.invoke_handler(input)
|
|
127
136
|
.await
|
|
128
|
-
.map_err(|e| ErrorResponseBuilder::internal_error(format!("Handler execution failed: {}"
|
|
137
|
+
.map_err(|e| ErrorResponseBuilder::internal_error(format!("Handler execution failed: {e}")))?;
|
|
129
138
|
|
|
130
139
|
let response = self
|
|
131
140
|
.language_handler
|
|
132
141
|
.interpret_response(output)
|
|
133
|
-
.map_err(|e| ErrorResponseBuilder::internal_error(format!("Failed to interpret response: {}"
|
|
142
|
+
.map_err(|e| ErrorResponseBuilder::internal_error(format!("Failed to interpret response: {e}")))?;
|
|
134
143
|
|
|
135
144
|
Ok(response)
|
|
136
145
|
})
|