This PR implements a new subcommand `doc` that generates markdown documentation for a given gRPC service! **Description** For the most part, the inner logic of this subcommand is the same as the `describe`, the only thing that changes is the way that the found descriptor is transformed to a final output. In this case, a `Packages` type has been implemented to transform a `ServiceDescriptor` into a map of `Package`s. Each package groups all the file descriptors with the same package name (or namespace). A `Package` contains all the necessary information for a file of documentation to be generated (All its contained services, messages and enum descriptors and its name). The output of this command is a folder with all the generated documentation, which contains a file per protobuf package. **Introduced the `granc-test-support` crate** * Renamed the `echo_service` crate as `granc-test-support`, providing both the definition of a protobuf service for integration testing and a function to compile protobuffer at runtime into a file descriptor (Potentially this could be used to let users pass a folder to a proto project in addition to the server reflection and the local file descriptor options. For example, the `call` command could compile a file descriptor on the fly from a folder containing a protobuffer project before making the call to the gRPC server. **Descriptor API Enhancements:** * Added `name`, `full_name`, and `package_name` methods to the `Descriptor` enum to simplify access to descriptor metadata. (`granc-core/src/client/types.rs`) **Dependency Management Improvements:** * Added grouping for gRPC-related dependencies in `dependabot.yml` for improved automated dependency updates. (`.github/dependabot.yml`) |
||
|---|---|---|
| .. | ||
| 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 uses a Typestate Pattern to ensure safety and correctness regarding how the Protobuf schema is resolved. There are three distinct states:
- [
Online]: Connected to a server, uses Server Reflection (Async introspection). - [
OnlineWithoutReflection]: Connected to a server, uses a localFileDescriptorSet(Sync introspection). - [
Offline]: Disconnected, uses a localFileDescriptorSet(Sync introspection).
1. Online (Server Reflection)
This is the default state when you connect. The client queries the server's reflection endpoint to dynamically discover services and message formats.
use granc_core::client::{GrancClient, DynamicRequest, DynamicResponse};
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Connect (Starts in 'Online' state)
let mut client = GrancClient::connect("http://localhost:50051").await?;
// 2. Introspection (Async via Reflection)
let services = client.list_services().await?;
println!("Server services: {:?}", services);
// 3. Dynamic Call
let request = DynamicRequest {
service: "helloworld.Greeter".to_string(),
method: "SayHello".to_string(),
body: json!({ "name": "Ferris" }),
headers: vec![],
};
// Schema is fetched automatically from the server
let response = client.dynamic(request).await?;
println!("{:?}", response);
Ok(())
}
2. OnlineWithoutReflection (Local Schema)
Use this state if you are connecting to a server that does not support reflection, or if you want to enforce a specific schema version from a local file.
use granc_core::client::GrancClient;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = GrancClient::connect("http://localhost:50051").await?;
let descriptor_bytes = std::fs::read("descriptor.bin")?;
// Transition state: Online -> OnlineWithoutReflection
let mut client = client.with_file_descriptor(descriptor_bytes)?;
// Introspection is now SYNCHRONOUS (in-memory)
let services = client.list_services();
println!("Local services: {:?}", services);
// Dynamic calls use the local schema to encode/decode
// client.dynamic(req).await?;
Ok(())
}
3. Offline (Introspection Only)
This state is useful for building tools that need to inspect .bin descriptor files without establishing a network connection.
use granc_core::client::GrancClient;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let descriptor_bytes = std::fs::read("descriptor.bin")?;
// Create directly in 'Offline' state
let client = GrancClient::offline(descriptor_bytes)?;
// Sync introspection methods
let services = client.list_services();
if let Some(descriptor) = client.get_descriptor_by_symbol("helloworld.Greeter") {
println!("Found service: {:?}", descriptor);
}
// Note: client.dynamic() is NOT available in this state.
Ok(())
}
🛠️ 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. 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.
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.
3. ReflectionClient
A robust client for grpc.reflection.v1. It automatically handles transitive dependency resolution, recursively fetching all imported files to build a complete, self-contained FileDescriptorSet.
⚖️ License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.