mirror of
https://codeberg.org/JasterV/granc.git
synced 2026-04-26 18:40:05 +00:00
This pull request introduces major improvements to the `granc` gRPC CLI, focusing on enhanced introspection and discovery features, a more user-friendly command-line interface, and improved error and output formatting. The changes include new commands for listing and describing services, methods, and messages, a restructured CLI argument parser, and a new formatter for colored, readable output. Additionally, the core client is extended to support these new features, and error handling is refactored for clarity. **New CLI features and UX improvements:** * Added new `list` and `describe` commands to the CLI, allowing users to discover available services and inspect service/message definitions directly from the server using reflection. The CLI argument structure is now subcommand-based for better usability. [[1]](diffhunk://#diff-dfa67e7f5e147119fe8d665da6b31b3605f5e196734ec7407aab2bcc9e2f656cL8-R84) [[2]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L71-R139) * Updated the README with documentation for the new commands and improved usage instructions. [[1]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R21) [[2]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L71-R139) **Core client and reflection enhancements:** * Implemented new methods in the core client for listing services and fetching symbol descriptors via reflection, including robust error types for each operation. [[1]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adL25-R27) [[2]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adL38-R78) [[3]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adR149-R211) [[4]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adL125-R229) [[5]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adL154-R252) [[6]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adL164-R262) [[7]](diffhunk://#diff-13deee04dd97de938cc46f0ef4faca083f3b471800e94cf45937122b83f01d57R19) [[8]](diffhunk://#diff-13deee04dd97de938cc46f0ef4faca083f3b471800e94cf45937122b83f01d57R124-R161) **Output formatting and error handling:** * Added a new `formatter` module for producing colored, human-friendly output for all major CLI operations, including pretty-printing of service lists, descriptors, and errors. * Improved error handling throughout the client and CLI, with more specific error types and user-facing messages. [[1]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adL38-R78) [[2]](diffhunk://#diff-0de6f761cf394791a15b0707e1a41f54559b5626f7aedb06ef339bc1a7ca6287R1-R248) **Dependency and project structure updates:** * Updated dependencies and added the `colored` crate for output styling.
213 lines
6.9 KiB
Rust
213 lines
6.9 KiB
Rust
use echo_service::EchoServiceServer;
|
|
use echo_service::FILE_DESCRIPTOR_SET;
|
|
use echo_service_impl::EchoServiceImpl;
|
|
use granc_core::client::{DynamicRequest, DynamicResponse, GrancClient};
|
|
use tonic_reflection::server::v1::ServerReflectionServer;
|
|
|
|
mod echo_service_impl;
|
|
|
|
fn reflection_service()
|
|
-> ServerReflectionServer<impl tonic_reflection::server::v1::ServerReflection> {
|
|
tonic_reflection::server::Builder::configure()
|
|
.register_encoded_file_descriptor_set(FILE_DESCRIPTOR_SET)
|
|
.build_v1()
|
|
.expect("Failed to setup Reflection Service")
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_unary() {
|
|
let payload = serde_json::json!({ "message": "hello" });
|
|
|
|
let request = DynamicRequest {
|
|
file_descriptor_set: Some(FILE_DESCRIPTOR_SET.to_vec()),
|
|
body: payload.clone(),
|
|
headers: vec![],
|
|
service: "echo.EchoService".to_string(),
|
|
method: "UnaryEcho".to_string(),
|
|
};
|
|
|
|
let mut client = GrancClient::new(EchoServiceServer::new(EchoServiceImpl));
|
|
|
|
let res = client.dynamic(request).await.unwrap();
|
|
|
|
match res {
|
|
DynamicResponse::Unary(Ok(value)) => assert_eq!(value, payload),
|
|
DynamicResponse::Unary(Err(_)) => {
|
|
panic!("Received error status for valid unary request")
|
|
}
|
|
_ => panic!("Received stream response for unary request"),
|
|
};
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_server_streaming() {
|
|
let payload = serde_json::json!({ "message": "stream" });
|
|
|
|
let request = DynamicRequest {
|
|
file_descriptor_set: Some(FILE_DESCRIPTOR_SET.to_vec()),
|
|
body: payload.clone(),
|
|
headers: vec![],
|
|
service: "echo.EchoService".to_string(),
|
|
method: "ServerStreamingEcho".to_string(),
|
|
};
|
|
|
|
let mut client = GrancClient::new(EchoServiceServer::new(EchoServiceImpl));
|
|
|
|
let res = client.dynamic(request).await.unwrap();
|
|
|
|
match res {
|
|
DynamicResponse::Streaming(Ok(elems)) => {
|
|
let results: Vec<_> = elems.into_iter().map(|r| r.unwrap()).collect();
|
|
|
|
assert_eq!(results.len(), 3);
|
|
assert_eq!(results[0]["message"], "stream - seq 0");
|
|
assert_eq!(results[1]["message"], "stream - seq 1");
|
|
assert_eq!(results[2]["message"], "stream - seq 2");
|
|
}
|
|
DynamicResponse::Streaming(Err(_)) => {
|
|
panic!("Received error status for valid server streaming request")
|
|
}
|
|
_ => panic!("Received unary response for server streaming request"),
|
|
};
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_client_streaming() {
|
|
let payload = serde_json::json!([
|
|
{ "message": "A" },
|
|
{ "message": "B" },
|
|
{ "message": "C" }
|
|
]);
|
|
|
|
let request = DynamicRequest {
|
|
file_descriptor_set: Some(FILE_DESCRIPTOR_SET.to_vec()),
|
|
body: payload.clone(),
|
|
headers: vec![],
|
|
service: "echo.EchoService".to_string(),
|
|
method: "ClientStreamingEcho".to_string(),
|
|
};
|
|
|
|
let mut client = GrancClient::new(EchoServiceServer::new(EchoServiceImpl));
|
|
|
|
let res = client.dynamic(request).await.unwrap();
|
|
|
|
match res {
|
|
DynamicResponse::Unary(Ok(value)) => {
|
|
assert_eq!(value, serde_json::json!({"message": "ABC"}))
|
|
}
|
|
DynamicResponse::Unary(Err(_)) => {
|
|
panic!("Received error status for valid client stream request")
|
|
}
|
|
_ => panic!("Received stream response for client stream request"),
|
|
};
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bidirectional_streaming() {
|
|
let payload = serde_json::json!([
|
|
{ "message": "Ping" },
|
|
{ "message": "Pong" }
|
|
]);
|
|
|
|
let request = DynamicRequest {
|
|
file_descriptor_set: Some(FILE_DESCRIPTOR_SET.to_vec()),
|
|
body: payload.clone(),
|
|
headers: vec![],
|
|
service: "echo.EchoService".to_string(),
|
|
method: "BidirectionalEcho".to_string(),
|
|
};
|
|
|
|
let mut client = GrancClient::new(EchoServiceServer::new(EchoServiceImpl));
|
|
|
|
let res = client.dynamic(request).await.unwrap();
|
|
|
|
match res {
|
|
DynamicResponse::Streaming(Ok(elems)) => {
|
|
let results: Vec<_> = elems.into_iter().map(|r| r.unwrap()).collect();
|
|
|
|
assert_eq!(results.len(), 2);
|
|
assert_eq!(results[0]["message"], "echo: Ping");
|
|
assert_eq!(results[1]["message"], "echo: Pong");
|
|
}
|
|
DynamicResponse::Streaming(Err(_)) => {
|
|
panic!("Received error status for valid bidirectional streaming request")
|
|
}
|
|
_ => panic!("Received unary response for bidirectional streaming request"),
|
|
};
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_list_services_success() {
|
|
let mut client = GrancClient::new(reflection_service());
|
|
|
|
let services = client
|
|
.list_services()
|
|
.await
|
|
.expect("Failed to list services");
|
|
|
|
// We expect "echo.EchoService" because we registered it.
|
|
// The list usually also includes the reflection service itself ("grpc.reflection.v1.ServerReflection").
|
|
assert!(
|
|
services.contains(&"echo.EchoService".to_string()),
|
|
"Services list did not contain 'echo.EchoService'. Found: {:?}",
|
|
services
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_service_descriptor_success() {
|
|
let mut client = GrancClient::new(reflection_service());
|
|
|
|
let descriptor = client
|
|
.get_descriptor_by_symbol("echo.EchoService")
|
|
.await
|
|
.expect("Failed to get service descriptor");
|
|
|
|
let descriptor = descriptor.service_descriptor().unwrap();
|
|
|
|
assert_eq!(descriptor.name(), "EchoService");
|
|
assert_eq!(descriptor.full_name(), "echo.EchoService");
|
|
|
|
// Verify methods are present
|
|
let method_names: Vec<String> = descriptor.methods().map(|m| m.name().to_string()).collect();
|
|
assert!(method_names.contains(&"UnaryEcho".to_string()));
|
|
assert!(method_names.contains(&"ServerStreamingEcho".to_string()));
|
|
assert!(method_names.contains(&"ClientStreamingEcho".to_string()));
|
|
assert!(method_names.contains(&"BidirectionalEcho".to_string()));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_message_descriptor_success() {
|
|
let mut client = GrancClient::new(reflection_service());
|
|
|
|
let desc = client
|
|
.get_descriptor_by_symbol("echo.EchoRequest")
|
|
.await
|
|
.expect("Failed to get message descriptor");
|
|
|
|
let desc = desc.message_descriptor().unwrap();
|
|
|
|
assert_eq!(desc.name(), "EchoRequest");
|
|
assert_eq!(desc.full_name(), "echo.EchoRequest");
|
|
|
|
// Verify fields
|
|
let fields: Vec<String> = desc.fields().map(|f| f.name().to_string()).collect();
|
|
assert!(fields.contains(&"message".to_string()));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_descriptor_not_found() {
|
|
let mut client = GrancClient::new(reflection_service());
|
|
|
|
// "echo.GhostService" does not exist in the registered descriptors.
|
|
// The reflection client should fail to find the symbol, resulting in a ResolutionError.
|
|
let err = client
|
|
.get_descriptor_by_symbol("echo.GhostService")
|
|
.await
|
|
.unwrap_err();
|
|
|
|
assert!(matches!(
|
|
err,
|
|
granc_core::client::GetDescriptorError::NotFound(_)
|
|
));
|
|
}
|