spikard 0.8.1 → 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/grpc.rb +5 -5
- 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 +16 -14
- 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/metadata.rs +3 -14
- 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-http/src/grpc/mod.rs +1 -1
- data/vendor/crates/spikard-http/src/grpc/service.rs +11 -11
- data/vendor/crates/spikard-http/src/grpc/streaming.rs +5 -1
- data/vendor/crates/spikard-http/src/server/grpc_routing.rs +59 -20
- data/vendor/crates/spikard-http/src/server/routing_factory.rs +179 -201
- data/vendor/crates/spikard-http/tests/common/grpc_helpers.rs +49 -60
- data/vendor/crates/spikard-http/tests/common/handlers.rs +5 -5
- data/vendor/crates/spikard-http/tests/common/mod.rs +7 -8
- data/vendor/crates/spikard-http/tests/common/test_builders.rs +14 -19
- data/vendor/crates/spikard-http/tests/grpc_error_handling_test.rs +68 -69
- data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +1 -3
- data/vendor/crates/spikard-http/tests/grpc_metadata_test.rs +98 -84
- data/vendor/crates/spikard-http/tests/grpc_server_integration.rs +76 -57
- data/vendor/crates/spikard-rb/Cargo.toml +9 -1
- data/vendor/crates/spikard-rb/build.rs +1 -0
- data/vendor/crates/spikard-rb/src/grpc/handler.rs +30 -25
- data/vendor/crates/spikard-rb/src/lib.rs +59 -2
- 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
|
@@ -51,31 +51,35 @@ pub enum ErrorCondition {
|
|
|
51
51
|
|
|
52
52
|
impl ErrorCondition {
|
|
53
53
|
/// Determine the error condition from schema path and error message
|
|
54
|
+
#[must_use]
|
|
55
|
+
#[allow(clippy::ignored_unit_patterns)]
|
|
54
56
|
pub fn from_schema_error(schema_path_str: &str, error_msg: &str) -> Self {
|
|
55
57
|
match () {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
() if schema_path_str.contains("minLength") => Self::StringTooShort { min_length: None },
|
|
59
|
+
() if schema_path_str.contains("maxLength") => Self::StringTooLong { max_length: None },
|
|
60
|
+
() if schema_path_str.contains("exclusiveMinimum")
|
|
59
61
|
|| (error_msg.contains("less than or equal to") && error_msg.contains("minimum")) =>
|
|
60
62
|
{
|
|
61
63
|
Self::GreaterThan { value: None }
|
|
62
64
|
}
|
|
63
|
-
|
|
65
|
+
() if schema_path_str.contains("minimum") || error_msg.contains("less than the minimum") => {
|
|
64
66
|
Self::GreaterThanEqual { value: None }
|
|
65
67
|
}
|
|
66
|
-
|
|
68
|
+
() if schema_path_str.contains("exclusiveMaximum")
|
|
67
69
|
|| (error_msg.contains("greater than or equal to") && error_msg.contains("maximum")) =>
|
|
68
70
|
{
|
|
69
71
|
Self::LessThan { value: None }
|
|
70
72
|
}
|
|
71
|
-
|
|
73
|
+
() if schema_path_str.contains("maximum") || error_msg.contains("greater than the maximum") => {
|
|
72
74
|
Self::LessThanEqual { value: None }
|
|
73
75
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
() if schema_path_str.contains("enum") || error_msg.contains("is not one of") => {
|
|
77
|
+
Self::Enum { values: None }
|
|
78
|
+
}
|
|
79
|
+
() if schema_path_str.contains("pattern") || error_msg.contains("does not match") => {
|
|
76
80
|
Self::StringPatternMismatch { pattern: None }
|
|
77
81
|
}
|
|
78
|
-
|
|
82
|
+
() if schema_path_str.contains("format") => {
|
|
79
83
|
if error_msg.contains("email") {
|
|
80
84
|
Self::EmailFormat
|
|
81
85
|
} else if error_msg.contains("uuid") {
|
|
@@ -104,7 +108,8 @@ impl ErrorCondition {
|
|
|
104
108
|
}
|
|
105
109
|
|
|
106
110
|
/// Get the error type code for this condition
|
|
107
|
-
|
|
111
|
+
#[must_use]
|
|
112
|
+
pub const fn error_type(&self) -> &'static str {
|
|
108
113
|
match self {
|
|
109
114
|
Self::StringTooShort { .. } => "string_too_short",
|
|
110
115
|
Self::StringTooLong { .. } => "string_too_long",
|
|
@@ -113,23 +118,22 @@ impl ErrorCondition {
|
|
|
113
118
|
Self::LessThan { .. } => "less_than",
|
|
114
119
|
Self::LessThanEqual { .. } => "less_than_equal",
|
|
115
120
|
Self::Enum { .. } => "enum",
|
|
116
|
-
Self::StringPatternMismatch { .. } => "string_pattern_mismatch",
|
|
117
|
-
Self::EmailFormat => "string_pattern_mismatch",
|
|
121
|
+
Self::StringPatternMismatch { .. } | Self::EmailFormat => "string_pattern_mismatch",
|
|
118
122
|
Self::UuidFormat => "uuid_parsing",
|
|
119
123
|
Self::DatetimeFormat => "datetime_parsing",
|
|
120
124
|
Self::DateFormat => "date_parsing",
|
|
121
125
|
Self::FormatError => "format_error",
|
|
122
126
|
Self::TypeMismatch { .. } => "type_error",
|
|
123
127
|
Self::Missing => "missing",
|
|
124
|
-
Self::AdditionalProperties { .. } => "validation_error",
|
|
128
|
+
Self::AdditionalProperties { .. } | Self::ValidationError => "validation_error",
|
|
125
129
|
Self::TooFewItems { .. } => "too_short",
|
|
126
130
|
Self::TooManyItems => "too_long",
|
|
127
|
-
Self::ValidationError => "validation_error",
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
/// Get default message for this error condition
|
|
132
|
-
|
|
135
|
+
#[must_use]
|
|
136
|
+
pub const fn default_message(&self) -> &'static str {
|
|
133
137
|
match self {
|
|
134
138
|
Self::StringTooShort { .. } => "String is too short",
|
|
135
139
|
Self::StringTooLong { .. } => "String is too long",
|
|
@@ -159,6 +163,16 @@ pub struct ErrorMapper;
|
|
|
159
163
|
|
|
160
164
|
impl ErrorMapper {
|
|
161
165
|
/// Map an error condition to its type, message, and context
|
|
166
|
+
///
|
|
167
|
+
/// # Panics
|
|
168
|
+
/// Panics if accessing `.last()` on an empty vector for enum values extraction.
|
|
169
|
+
#[must_use]
|
|
170
|
+
#[allow(
|
|
171
|
+
clippy::too_many_lines,
|
|
172
|
+
clippy::option_if_let_else,
|
|
173
|
+
clippy::redundant_closure_for_method_calls,
|
|
174
|
+
clippy::uninlined_format_args
|
|
175
|
+
)]
|
|
162
176
|
pub fn map_error(
|
|
163
177
|
condition: &ErrorCondition,
|
|
164
178
|
schema: &Value,
|
|
@@ -18,6 +18,9 @@ pub struct SchemaValidator {
|
|
|
18
18
|
|
|
19
19
|
impl SchemaValidator {
|
|
20
20
|
/// Create a new validator from a JSON Schema
|
|
21
|
+
///
|
|
22
|
+
/// # Errors
|
|
23
|
+
/// Returns an error if the schema is invalid or compilation fails.
|
|
21
24
|
pub fn new(schema: Value) -> Result<Self, String> {
|
|
22
25
|
let compiled = jsonschema::options()
|
|
23
26
|
.with_draft(jsonschema::Draft::Draft202012)
|
|
@@ -26,7 +29,7 @@ impl SchemaValidator {
|
|
|
26
29
|
.build(&schema)
|
|
27
30
|
.map_err(|e| {
|
|
28
31
|
anyhow::anyhow!("Invalid JSON Schema")
|
|
29
|
-
.context(format!("Schema compilation failed: {}"
|
|
32
|
+
.context(format!("Schema compilation failed: {e}"))
|
|
30
33
|
.to_string()
|
|
31
34
|
})?;
|
|
32
35
|
|
|
@@ -37,16 +40,17 @@ impl SchemaValidator {
|
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
/// Get the underlying JSON Schema
|
|
40
|
-
|
|
43
|
+
#[must_use]
|
|
44
|
+
pub const fn schema(&self) -> &Value {
|
|
41
45
|
&self.schema
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
/// Pre-process data to convert file objects to strings for format:
|
|
48
|
+
/// Pre-process data to convert file objects to strings for format: `binary` validation
|
|
45
49
|
///
|
|
46
50
|
/// Files uploaded via multipart are converted to objects like:
|
|
47
|
-
/// {"filename": "...", "size": N, "content": "...", "content_type": "..."}
|
|
51
|
+
/// `{"filename": "...", "size": N, "content": "...", "content_type": "..."}`
|
|
48
52
|
///
|
|
49
|
-
/// But schemas define them as: {"type": "string", "format": "binary"}
|
|
53
|
+
/// But schemas define them as: `{"type": "string", "format": "binary"}`
|
|
50
54
|
///
|
|
51
55
|
/// This method recursively processes the data and converts file objects to their content strings
|
|
52
56
|
/// so that validation passes, while preserving the original structure for handlers to use.
|
|
@@ -54,7 +58,7 @@ impl SchemaValidator {
|
|
|
54
58
|
self.preprocess_value_with_schema(data, &self.schema)
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
#[allow(clippy::only_used_in_recursion)]
|
|
61
|
+
#[allow(clippy::only_used_in_recursion, clippy::self_only_used_in_recursion)]
|
|
58
62
|
fn preprocess_value_with_schema(&self, data: &Value, schema: &Value) -> Value {
|
|
59
63
|
if let Some(schema_obj) = schema.as_object() {
|
|
60
64
|
let is_string_type = schema_obj.get("type").and_then(|t| t.as_str()) == Some("string");
|
|
@@ -110,6 +114,13 @@ impl SchemaValidator {
|
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
/// Validate JSON data against the schema
|
|
117
|
+
///
|
|
118
|
+
/// # Errors
|
|
119
|
+
/// Returns a `ValidationError` if the data does not conform to the schema.
|
|
120
|
+
///
|
|
121
|
+
/// # Too Many Lines
|
|
122
|
+
/// This function is complex due to error mapping logic.
|
|
123
|
+
#[allow(clippy::option_if_let_else, clippy::uninlined_format_args, clippy::too_many_lines)]
|
|
113
124
|
pub fn validate(&self, data: &Value) -> Result<(), ValidationError> {
|
|
114
125
|
let processed_data = self.preprocess_binary_fields(data);
|
|
115
126
|
|
|
@@ -131,41 +142,39 @@ impl SchemaValidator {
|
|
|
131
142
|
if let Some(end) = error_msg[start + 1..].find('"') {
|
|
132
143
|
error_msg[start + 1..start + 1 + end].to_string()
|
|
133
144
|
} else {
|
|
134
|
-
|
|
145
|
+
String::new()
|
|
135
146
|
}
|
|
136
147
|
} else {
|
|
137
|
-
|
|
148
|
+
String::new()
|
|
138
149
|
};
|
|
139
150
|
|
|
140
|
-
if
|
|
151
|
+
if instance_path.starts_with('/') && instance_path.len() > 1 {
|
|
141
152
|
let base_path = &instance_path[1..];
|
|
142
|
-
if
|
|
143
|
-
format!("{}/{}", base_path, field_name)
|
|
144
|
-
} else {
|
|
153
|
+
if field_name.is_empty() {
|
|
145
154
|
base_path.to_string()
|
|
155
|
+
} else {
|
|
156
|
+
format!("{base_path}/{field_name}")
|
|
146
157
|
}
|
|
147
|
-
} else if
|
|
148
|
-
field_name
|
|
149
|
-
} else {
|
|
158
|
+
} else if field_name.is_empty() {
|
|
150
159
|
"body".to_string()
|
|
160
|
+
} else {
|
|
161
|
+
field_name
|
|
151
162
|
}
|
|
152
163
|
} else if schema_path_str.contains("/additionalProperties") {
|
|
153
164
|
if let Some(start) = error_msg.find('(') {
|
|
154
165
|
if let Some(quote_start) = error_msg[start..].find('\'') {
|
|
155
166
|
let abs_start = start + quote_start + 1;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
&& instance_path.len() > 1
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
instance_path[1..].to_string()
|
|
168
|
-
}
|
|
167
|
+
error_msg[abs_start..].find('\'').map_or_else(
|
|
168
|
+
|| instance_path[1..].to_string(),
|
|
169
|
+
|quote_end| {
|
|
170
|
+
let property_name = error_msg[abs_start..abs_start + quote_end].to_string();
|
|
171
|
+
if instance_path.starts_with('/') && instance_path.len() > 1 {
|
|
172
|
+
format!("{}/{property_name}", &instance_path[1..])
|
|
173
|
+
} else {
|
|
174
|
+
property_name
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
)
|
|
169
178
|
} else {
|
|
170
179
|
instance_path[1..].to_string()
|
|
171
180
|
}
|
|
@@ -184,7 +193,7 @@ impl SchemaValidator {
|
|
|
184
193
|
|
|
185
194
|
let loc_parts: Vec<String> = if param_name.contains('/') {
|
|
186
195
|
let mut parts = vec!["body".to_string()];
|
|
187
|
-
parts.extend(param_name.split('/').map(
|
|
196
|
+
parts.extend(param_name.split('/').map(ToString::to_string));
|
|
188
197
|
parts
|
|
189
198
|
} else if param_name == "body" {
|
|
190
199
|
vec!["body".to_string()]
|
|
@@ -201,7 +210,7 @@ impl SchemaValidator {
|
|
|
201
210
|
let schema_prop_path = if param_name.contains('/') {
|
|
202
211
|
format!("/properties/{}", param_name.replace('/', "/properties/"))
|
|
203
212
|
} else {
|
|
204
|
-
format!("/properties/{}"
|
|
213
|
+
format!("/properties/{param_name}")
|
|
205
214
|
};
|
|
206
215
|
|
|
207
216
|
let mut error_condition = ErrorCondition::from_schema_error(schema_path_str, &error_msg);
|
|
@@ -210,13 +219,14 @@ impl SchemaValidator {
|
|
|
210
219
|
ErrorCondition::TypeMismatch { .. } => {
|
|
211
220
|
let expected_type = self
|
|
212
221
|
.schema
|
|
213
|
-
.pointer(&format!("{}/type"
|
|
222
|
+
.pointer(&format!("{schema_prop_path}/type"))
|
|
214
223
|
.and_then(|v| v.as_str())
|
|
215
224
|
.unwrap_or("unknown")
|
|
216
225
|
.to_string();
|
|
217
226
|
ErrorCondition::TypeMismatch { expected_type }
|
|
218
227
|
}
|
|
219
228
|
ErrorCondition::AdditionalProperties { .. } => {
|
|
229
|
+
#[allow(clippy::redundant_clone)]
|
|
220
230
|
let unexpected_field = if param_name.contains('/') {
|
|
221
231
|
param_name.split('/').next_back().unwrap_or(¶m_name).to_string()
|
|
222
232
|
} else {
|
|
@@ -268,12 +278,15 @@ impl SchemaValidator {
|
|
|
268
278
|
}
|
|
269
279
|
|
|
270
280
|
/// Validate and parse JSON bytes
|
|
281
|
+
///
|
|
282
|
+
/// # Errors
|
|
283
|
+
/// Returns a validation error if the JSON is invalid or fails validation against the schema.
|
|
271
284
|
pub fn validate_json(&self, json_bytes: &[u8]) -> Result<Value, ValidationError> {
|
|
272
285
|
let value: Value = serde_json::from_slice(json_bytes).map_err(|e| ValidationError {
|
|
273
286
|
errors: vec![ValidationErrorDetail {
|
|
274
287
|
error_type: "json_parse_error".to_string(),
|
|
275
288
|
loc: vec!["body".to_string()],
|
|
276
|
-
msg: format!("Invalid JSON: {}"
|
|
289
|
+
msg: format!("Invalid JSON: {e}"),
|
|
277
290
|
input: Value::Null,
|
|
278
291
|
ctx: None,
|
|
279
292
|
}],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "spikard-http"
|
|
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"
|
|
@@ -55,6 +55,13 @@ prost = "0.14"
|
|
|
55
55
|
prost-types = "0.14"
|
|
56
56
|
h2 = "0.4"
|
|
57
57
|
|
|
58
|
+
[lints.rust]
|
|
59
|
+
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(tarpaulin_include)'] }
|
|
60
|
+
|
|
61
|
+
[lints.clippy]
|
|
62
|
+
all = { level = "deny", priority = 0 }
|
|
63
|
+
pedantic = { level = "deny", priority = 0 }
|
|
64
|
+
nursery = { level = "deny", priority = 0 }
|
|
58
65
|
|
|
59
66
|
[features]
|
|
60
67
|
default = []
|
|
@@ -48,7 +48,7 @@ pub mod streaming;
|
|
|
48
48
|
|
|
49
49
|
// Re-export main types
|
|
50
50
|
pub use handler::{GrpcHandler, GrpcHandlerResult, GrpcRequestData, GrpcResponseData};
|
|
51
|
-
pub use service::{
|
|
51
|
+
pub use service::{GenericGrpcService, copy_metadata, is_grpc_request, parse_grpc_path};
|
|
52
52
|
pub use streaming::{MessageStream, StreamingRequest, StreamingResponse};
|
|
53
53
|
|
|
54
54
|
use serde::{Deserialize, Serialize};
|
|
@@ -200,7 +200,9 @@ mod tests {
|
|
|
200
200
|
let service = GenericGrpcService::new(handler);
|
|
201
201
|
|
|
202
202
|
let request = Request::new(Bytes::from("test payload"));
|
|
203
|
-
let result = service
|
|
203
|
+
let result = service
|
|
204
|
+
.handle_unary("test.TestService".to_string(), "TestMethod".to_string(), request)
|
|
205
|
+
.await;
|
|
204
206
|
|
|
205
207
|
assert!(result.is_ok());
|
|
206
208
|
let response = result.unwrap();
|
|
@@ -217,7 +219,9 @@ mod tests {
|
|
|
217
219
|
.metadata_mut()
|
|
218
220
|
.insert("custom-header", "custom-value".parse().unwrap());
|
|
219
221
|
|
|
220
|
-
let result = service
|
|
222
|
+
let result = service
|
|
223
|
+
.handle_unary("test.TestService".to_string(), "TestMethod".to_string(), request)
|
|
224
|
+
.await;
|
|
221
225
|
|
|
222
226
|
assert!(result.is_ok());
|
|
223
227
|
}
|
|
@@ -266,10 +270,7 @@ mod tests {
|
|
|
266
270
|
#[test]
|
|
267
271
|
fn test_is_grpc_request_valid() {
|
|
268
272
|
let mut headers = axum::http::HeaderMap::new();
|
|
269
|
-
headers.insert(
|
|
270
|
-
axum::http::header::CONTENT_TYPE,
|
|
271
|
-
"application/grpc".parse().unwrap(),
|
|
272
|
-
);
|
|
273
|
+
headers.insert(axum::http::header::CONTENT_TYPE, "application/grpc".parse().unwrap());
|
|
273
274
|
assert!(is_grpc_request(&headers));
|
|
274
275
|
}
|
|
275
276
|
|
|
@@ -286,10 +287,7 @@ mod tests {
|
|
|
286
287
|
#[test]
|
|
287
288
|
fn test_is_grpc_request_not_grpc() {
|
|
288
289
|
let mut headers = axum::http::HeaderMap::new();
|
|
289
|
-
headers.insert(
|
|
290
|
-
axum::http::header::CONTENT_TYPE,
|
|
291
|
-
"application/json".parse().unwrap(),
|
|
292
|
-
);
|
|
290
|
+
headers.insert(axum::http::header::CONTENT_TYPE, "application/json".parse().unwrap());
|
|
293
291
|
assert!(!is_grpc_request(&headers));
|
|
294
292
|
}
|
|
295
293
|
|
|
@@ -382,7 +380,9 @@ mod tests {
|
|
|
382
380
|
let service = GenericGrpcService::new(handler);
|
|
383
381
|
|
|
384
382
|
let request = Request::new(Bytes::new());
|
|
385
|
-
let result = service
|
|
383
|
+
let result = service
|
|
384
|
+
.handle_unary("test.ErrorService".to_string(), "ErrorMethod".to_string(), request)
|
|
385
|
+
.await;
|
|
386
386
|
|
|
387
387
|
assert!(result.is_err());
|
|
388
388
|
let status = result.unwrap_err();
|
|
@@ -193,7 +193,11 @@ mod tests {
|
|
|
193
193
|
|
|
194
194
|
#[tokio::test]
|
|
195
195
|
async fn test_from_tonic_stream() {
|
|
196
|
-
let messages = vec![
|
|
196
|
+
let messages = vec![
|
|
197
|
+
Ok(Bytes::from("a")),
|
|
198
|
+
Ok(Bytes::from("b")),
|
|
199
|
+
Err(Status::cancelled("done")),
|
|
200
|
+
];
|
|
197
201
|
|
|
198
202
|
let tonic_stream = futures_util::stream::iter(messages);
|
|
199
203
|
let mut stream = from_tonic_stream(tonic_stream);
|
|
@@ -69,10 +69,7 @@ pub async fn route_grpc_request(
|
|
|
69
69
|
let handler = match registry.get(&service_name) {
|
|
70
70
|
Some(h) => h,
|
|
71
71
|
None => {
|
|
72
|
-
return Err((
|
|
73
|
-
StatusCode::NOT_FOUND,
|
|
74
|
-
format!("Service not found: {}", service_name),
|
|
75
|
-
));
|
|
72
|
+
return Err((StatusCode::NOT_FOUND, format!("Service not found: {}", service_name)));
|
|
76
73
|
}
|
|
77
74
|
};
|
|
78
75
|
|
|
@@ -94,7 +91,10 @@ pub async fn route_grpc_request(
|
|
|
94
91
|
// Try to parse as ASCII metadata
|
|
95
92
|
if let Ok(metadata_value) = value_str.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>() {
|
|
96
93
|
// Use key.as_str() directly instead of creating String
|
|
97
|
-
if let Ok(metadata_key) = key
|
|
94
|
+
if let Ok(metadata_key) = key
|
|
95
|
+
.as_str()
|
|
96
|
+
.parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>()
|
|
97
|
+
{
|
|
98
98
|
tonic_request.metadata_mut().insert(metadata_key, metadata_value);
|
|
99
99
|
}
|
|
100
100
|
}
|
|
@@ -132,9 +132,12 @@ pub async fn route_grpc_request(
|
|
|
132
132
|
response = response.header("grpc-status", "0");
|
|
133
133
|
|
|
134
134
|
// Convert bytes::Bytes to Body
|
|
135
|
-
let response = response
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
let response = response.body(Body::from(payload)).map_err(|e| {
|
|
136
|
+
(
|
|
137
|
+
StatusCode::INTERNAL_SERVER_ERROR,
|
|
138
|
+
format!("Failed to build response: {}", e),
|
|
139
|
+
)
|
|
140
|
+
})?;
|
|
138
141
|
|
|
139
142
|
Ok(response)
|
|
140
143
|
}
|
|
@@ -268,21 +271,57 @@ mod tests {
|
|
|
268
271
|
fn test_grpc_status_to_http_mappings() {
|
|
269
272
|
// Test all gRPC status codes map correctly
|
|
270
273
|
assert_eq!(grpc_status_to_http(tonic::Code::Ok), StatusCode::OK);
|
|
271
|
-
assert_eq!(
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
274
|
+
assert_eq!(
|
|
275
|
+
grpc_status_to_http(tonic::Code::Cancelled),
|
|
276
|
+
StatusCode::from_u16(499).unwrap()
|
|
277
|
+
);
|
|
278
|
+
assert_eq!(
|
|
279
|
+
grpc_status_to_http(tonic::Code::Unknown),
|
|
280
|
+
StatusCode::INTERNAL_SERVER_ERROR
|
|
281
|
+
);
|
|
282
|
+
assert_eq!(
|
|
283
|
+
grpc_status_to_http(tonic::Code::InvalidArgument),
|
|
284
|
+
StatusCode::BAD_REQUEST
|
|
285
|
+
);
|
|
286
|
+
assert_eq!(
|
|
287
|
+
grpc_status_to_http(tonic::Code::DeadlineExceeded),
|
|
288
|
+
StatusCode::GATEWAY_TIMEOUT
|
|
289
|
+
);
|
|
275
290
|
assert_eq!(grpc_status_to_http(tonic::Code::NotFound), StatusCode::NOT_FOUND);
|
|
276
291
|
assert_eq!(grpc_status_to_http(tonic::Code::AlreadyExists), StatusCode::CONFLICT);
|
|
277
|
-
assert_eq!(
|
|
278
|
-
|
|
279
|
-
|
|
292
|
+
assert_eq!(
|
|
293
|
+
grpc_status_to_http(tonic::Code::PermissionDenied),
|
|
294
|
+
StatusCode::FORBIDDEN
|
|
295
|
+
);
|
|
296
|
+
assert_eq!(
|
|
297
|
+
grpc_status_to_http(tonic::Code::ResourceExhausted),
|
|
298
|
+
StatusCode::TOO_MANY_REQUESTS
|
|
299
|
+
);
|
|
300
|
+
assert_eq!(
|
|
301
|
+
grpc_status_to_http(tonic::Code::FailedPrecondition),
|
|
302
|
+
StatusCode::BAD_REQUEST
|
|
303
|
+
);
|
|
280
304
|
assert_eq!(grpc_status_to_http(tonic::Code::Aborted), StatusCode::CONFLICT);
|
|
281
305
|
assert_eq!(grpc_status_to_http(tonic::Code::OutOfRange), StatusCode::BAD_REQUEST);
|
|
282
|
-
assert_eq!(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
assert_eq!(
|
|
306
|
+
assert_eq!(
|
|
307
|
+
grpc_status_to_http(tonic::Code::Unimplemented),
|
|
308
|
+
StatusCode::NOT_IMPLEMENTED
|
|
309
|
+
);
|
|
310
|
+
assert_eq!(
|
|
311
|
+
grpc_status_to_http(tonic::Code::Internal),
|
|
312
|
+
StatusCode::INTERNAL_SERVER_ERROR
|
|
313
|
+
);
|
|
314
|
+
assert_eq!(
|
|
315
|
+
grpc_status_to_http(tonic::Code::Unavailable),
|
|
316
|
+
StatusCode::SERVICE_UNAVAILABLE
|
|
317
|
+
);
|
|
318
|
+
assert_eq!(
|
|
319
|
+
grpc_status_to_http(tonic::Code::DataLoss),
|
|
320
|
+
StatusCode::INTERNAL_SERVER_ERROR
|
|
321
|
+
);
|
|
322
|
+
assert_eq!(
|
|
323
|
+
grpc_status_to_http(tonic::Code::Unauthenticated),
|
|
324
|
+
StatusCode::UNAUTHORIZED
|
|
325
|
+
);
|
|
287
326
|
}
|
|
288
327
|
}
|