spikard 0.8.1 → 0.8.2
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.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 +1 -1
- data/vendor/crates/spikard-bindings-shared/src/grpc_metadata.rs +3 -3
- data/vendor/crates/spikard-core/Cargo.toml +1 -1
- data/vendor/crates/spikard-core/src/metadata.rs +3 -14
- data/vendor/crates/spikard-http/Cargo.toml +1 -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 +1 -1
- data/vendor/crates/spikard-rb/src/grpc/handler.rs +30 -25
- data/vendor/crates/spikard-rb/src/lib.rs +1 -2
- data/vendor/crates/spikard-rb-macros/Cargo.toml +1 -1
- 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: 812ea62c2af7b3d443a8485aa055c197cdb1b65b04a3cc916ffd7bd6b0dfc81a
|
|
4
|
+
data.tar.gz: 825b7329c8ce47f3d74ebcc97fdbfa5ebbd5cbc16294b2721362bc1bd62fc7c6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8c27c069e2a10c5dc6b43f721f5baf2c56f5361f3fde3362b7ee3242b0deda62d9e4f198853cbba3e3f44c5f7aa0028495623961299fa99cc9bc9518d6f726a6
|
|
7
|
+
data.tar.gz: 6179700d82a1aff3aa93b87ac1152be28722eda2559a8becd8946d3e462cd32acc73328154c4906b79fab39e0188f0e7d84fefaa0e2aa4cc18358bb0fec6a84e
|
data/ext/spikard_rb/Cargo.toml
CHANGED
data/lib/spikard/grpc.rb
CHANGED
|
@@ -42,10 +42,12 @@ module Spikard
|
|
|
42
42
|
# @return [String] Binary string containing serialized protobuf message
|
|
43
43
|
# @!attribute [r] metadata
|
|
44
44
|
# @return [Hash<String, String>] gRPC metadata (headers)
|
|
45
|
+
# rubocop:disable Lint/EmptyClass -- Implementation in Rust via FFI
|
|
45
46
|
class Request
|
|
46
|
-
# These methods are implemented in Rust via Magnus FFI
|
|
47
|
-
# See: crates/spikard-rb/src/grpc/handler.rs
|
|
47
|
+
# These methods are implemented in Rust via Magnus FFI.
|
|
48
|
+
# See: crates/spikard-rb/src/grpc/handler.rs for implementation details.
|
|
48
49
|
end
|
|
50
|
+
# rubocop:enable Lint/EmptyClass
|
|
49
51
|
|
|
50
52
|
# gRPC response object
|
|
51
53
|
#
|
|
@@ -148,9 +150,7 @@ module Spikard
|
|
|
148
150
|
def register_handler(service_name, handler)
|
|
149
151
|
raise ArgumentError, 'Service name cannot be empty' if service_name.nil? || service_name.empty?
|
|
150
152
|
|
|
151
|
-
unless handler.respond_to?(:handle_request)
|
|
152
|
-
raise ArgumentError, "Handler must respond to :handle_request"
|
|
153
|
-
end
|
|
153
|
+
raise ArgumentError, 'Handler must respond to :handle_request' unless handler.respond_to?(:handle_request)
|
|
154
154
|
|
|
155
155
|
@handlers[service_name] = handler
|
|
156
156
|
end
|
data/lib/spikard/version.rb
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
//! language bindings (Python, Node.js, Ruby, PHP) to avoid code duplication.
|
|
5
5
|
|
|
6
6
|
use std::collections::HashMap;
|
|
7
|
-
use tonic::metadata::{
|
|
7
|
+
use tonic::metadata::{MetadataKey, MetadataMap, MetadataValue};
|
|
8
8
|
|
|
9
9
|
/// Extract metadata from gRPC MetadataMap to a simple HashMap.
|
|
10
10
|
///
|
|
@@ -94,8 +94,8 @@ pub fn hashmap_to_metadata(map: &HashMap<String, String>) -> Result<MetadataMap,
|
|
|
94
94
|
let metadata_key = MetadataKey::from_bytes(key.as_bytes())
|
|
95
95
|
.map_err(|err| format!("Invalid metadata key '{}': {}", key, err))?;
|
|
96
96
|
|
|
97
|
-
let metadata_value =
|
|
98
|
-
.map_err(|err| format!("Invalid metadata value for '{}': {}", key, err))?;
|
|
97
|
+
let metadata_value =
|
|
98
|
+
MetadataValue::try_from(value).map_err(|err| format!("Invalid metadata value for '{}': {}", key, err))?;
|
|
99
99
|
|
|
100
100
|
metadata.insert(metadata_key, metadata_value);
|
|
101
101
|
}
|
|
@@ -181,11 +181,7 @@ pub fn parse_parameter_schema(schema: &Value) -> Result<Vec<ParameterMetadata>,
|
|
|
181
181
|
let required: Vec<String> = schema
|
|
182
182
|
.get("required")
|
|
183
183
|
.and_then(|r| r.as_array())
|
|
184
|
-
.map(|arr|
|
|
185
|
-
arr.iter()
|
|
186
|
-
.filter_map(|v| v.as_str().map(String::from))
|
|
187
|
-
.collect()
|
|
188
|
-
})
|
|
184
|
+
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
|
|
189
185
|
.unwrap_or_default();
|
|
190
186
|
|
|
191
187
|
for (param_name, param_schema) in props {
|
|
@@ -197,10 +193,7 @@ pub fn parse_parameter_schema(schema: &Value) -> Result<Vec<ParameterMetadata>,
|
|
|
197
193
|
.and_then(|s| s.parse().ok())
|
|
198
194
|
.unwrap_or(ParameterSource::Query);
|
|
199
195
|
|
|
200
|
-
let schema_type = param_schema
|
|
201
|
-
.get("type")
|
|
202
|
-
.and_then(|t| t.as_str())
|
|
203
|
-
.map(String::from);
|
|
196
|
+
let schema_type = param_schema.get("type").and_then(|t| t.as_str()).map(String::from);
|
|
204
197
|
|
|
205
198
|
params.push(ParameterMetadata {
|
|
206
199
|
name: param_name.clone(),
|
|
@@ -242,11 +235,7 @@ pub fn validate_metadata(metadata: &ExtractedRouteMetadata) -> Result<(), Vec<St
|
|
|
242
235
|
}
|
|
243
236
|
}
|
|
244
237
|
|
|
245
|
-
if errors.is_empty() {
|
|
246
|
-
Ok(())
|
|
247
|
-
} else {
|
|
248
|
-
Err(errors)
|
|
249
|
-
}
|
|
238
|
+
if errors.is_empty() { Ok(()) } else { Err(errors) }
|
|
250
239
|
}
|
|
251
240
|
|
|
252
241
|
/// Merge path parameters with parameter schema
|
|
@@ -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
|
}
|