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. |
||
|---|---|---|
| .. | ||
| src | ||
| tests | ||
| Cargo.toml | ||
| README.md | ||
Granc Core
granc-core is the foundational library powering the Granc CLI. It provides a dynamic gRPC client capability that allows you to interact with any gRPC server without needing compile-time Protobuf code generation.
Instead of strictly typed Rust structs, this library bridges standard serde_json::Value payloads directly to Protobuf binary wire format at runtime.
🚀 High-Level Usage
The primary entry point is the [GrancClient]. It acts as an orchestrator that connects to a gRPC server and provides methods for both executing requests and inspecting the server's schema.
1. Making a Dynamic Call
The dynamic method handles the full request lifecycle:
- Resolves the schema (either from a local file or via Server Reflection).
- Determines the method type (Unary, Server Streaming, etc.).
- Executes the request using JSON.
use granc_core::client::{GrancClient, DynamicRequest, DynamicResponse};
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect to the server
let mut client = GrancClient::connect("http://localhost:50051").await?;
// Prepare the request
// If you don't provide a file_descriptor_set, the client will attempt
// to fetch the schema from the server's reflection service automatically.
let request = DynamicRequest {
service: "helloworld.Greeter".to_string(),
method: "SayHello".to_string(),
body: json!({ "name": "World" }),
headers: vec![],
file_descriptor_set: None, // Uses Server Reflection
};
let response = client.dynamic(request).await?;
match response {
DynamicResponse::Unary(Ok(value)) => {
println!("Response: {}", value);
}
DynamicResponse::Unary(Err(status)) => {
eprintln!("gRPC Error: {:?}", status);
}
DynamicResponse::Streaming(Ok(stream)) => {
for msg in stream {
println!("Stream Msg: {:?}", msg);
}
}
_ => eprintln!("Unexpected response type"),
}
Ok(())
}
2. Schema Introspection
GrancClient exposes several methods to inspect the server's available services and types using reflection.
// List all services exposed by the server
let services = client.list_services().await?;
println!("Available Services: {:?}", services);
// Get the descriptor for a specific type
let descriptor = client.get_descriptor_by_symbol("helloworld.Greeter").await?;
match descriptor {
Descriptor::MessageDescriptor(descriptor)) => println!("{}", descriptor.name())
Descriptor::ServiceDescriptor(descriptor)) => println!("{}", descriptor.name())
Descriptor::EnumDescriptor(descriptor)) => println!("{}", descriptor.name())
}
🛠️ Internal Components
We expose the internal building blocks of granc for developers who need more granular control or want to build their own tools on top of our dynamic transport layer.
1. GrpcClient (Generic Transport)
Standard tonic clients are strongly typed (e.g., client.say_hello(HelloRequest)).
GrpcClient is a generic wrapper around tonic::client::Grpc that works strictly with serde_json::Value and prost_reflect::MethodDescriptor.
It handles the raw HTTP/2 path construction and metadata mapping, providing specific methods for all four gRPC access patterns:
unaryserver_streamingclient_streamingbidirectional_streaming
use granc_core::grpc::client::GrpcClient;
// You need a method_descriptor from prost_reflect::DescriptorPool
// let method_descriptor = ...;
let mut grpc = GrpcClient::new(channel);
let result = grpc.unary(method_descriptor, json_value, headers).await?;
2. JsonCodec
The magic behind the dynamic serialization. This implementation of tonic::codec::Codec validates and transcodes JSON to Protobuf bytes (and vice versa) on the fly.
- Encoder: Validates
serde_json::Valueagainst the inputMessageDescriptorand serializes it. - Decoder: Deserializes bytes into a
DynamicMessageand converts it back toserde_json::Value.
3. ReflectionClient
A client for grpc.reflection.v1. It enables runtime schema discovery.
The ReflectionClient is smart enough to handle dependencies. When you ask for a symbol (e.g., my.package.Service),
it recursively fetches the file defining that symbol and all its transitive imports, building a complete prost_types::FileDescriptorSet ready for use. It also supports listing available services.
use granc_core::reflection::client::ReflectionClient;
let mut reflection = ReflectionClient::new(channel);
// List services
let services = reflection.list_services().await?;
// Fetch full schema for a symbol
let fd_set = reflection.file_descriptor_set_by_symbol("my.package.Service").await?;
You can then build a prost_reflect::DescriptorPool with the returned prost_types::FileDescriptorSet to be able to inspect in detail the descriptor.
⚖️ License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.