update documentation

This commit is contained in:
JasterV 2026-01-28 13:37:31 +01:00
commit 9002c2ef3f
2 changed files with 44 additions and 76 deletions

View file

@ -80,7 +80,7 @@ granc call <ENDPOINT> --url <URL> --body <JSON> [OPTIONS]
| `<ENDPOINT>` | | Fully qualified method name (e.g., `my.package.Service/Method`). | **Yes** |
| `--url` | `-u` | Server address (e.g., `http://[::1]:50051`). | **Yes** |
| `--body` | `-b` | The request body in JSON format. Object `{}` for unary, Array `[]` for streaming. | **Yes** |
| `--header` | `-h` | Custom header `key:value`. Can be used multiple times. | No |
| `--header` | `-H` | Custom header `key:value`. Can be used multiple times. | No |
| `--file-descriptor-set` | `-f` | Path to a local `.bin` descriptor file to use instead of reflection. | No |
**Example using Server Reflection:**
@ -216,7 +216,7 @@ The core logic of Granc is decoupled into a separate library crate, **`granc-cor
If you want to build your own tools using the dynamic gRPC engine (e.g., for custom integration testing, proxies, or automation tools), you can depend on `granc-core` directly.
* **Documentation & Usage**: See the **[`granc-core` README](https://www.google.com/search?q=https://github.com/JasterV/granc/tree/main/granc-core%23readme)** for examples on how to use the `GrancClient` programmatically.
* **Documentation & Usage**: See the **[`granc-core` README](./granc-core/README.md)** for examples on how to use the `GrancClient` programmatically.
* **Crate**: [`granc-core`](https://crates.io/crates/granc_core)
## ⚠️ Common Errors

View file

@ -10,13 +10,15 @@ Instead of strictly typed Rust structs, this library bridges standard `serde_jso
## 🚀 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.
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:
To ensure safety and correctness, `GrancClient` uses a **Typestate Pattern**. It starts in a state that relies on Server Reflection, but can transition to a state that uses a local `FileDescriptorSet`.
1. **[`Online`]**: Connected to a server, uses Server Reflection (Async introspection).
2. **[`OnlineWithoutReflection`]**: Connected to a server, uses a local `FileDescriptorSet` (Sync introspection).
3. **[`Offline`]**: Disconnected, uses a local `FileDescriptorSet` (Sync introspection).
### 1. Using Server Reflection (Default)
### 1. Online (Server Reflection)
By default, when you connect, the client is ready to use the server's reflection service to resolve methods and types dynamically.
This is the default state when you connect. The client queries the server's reflection endpoint to dynamically discover services and message formats.
```rust
use granc_core::client::{GrancClient, DynamicRequest, DynamicResponse};
@ -24,91 +26,81 @@ use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect (starts in Reflection mode)
// 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": "World" }),
body: json!({ "name": "Ferris" }),
headers: vec![],
};
// Execute (Schema is fetched automatically via reflection)
// Schema is fetched automatically from the server
let response = client.dynamic(request).await?;
match response {
DynamicResponse::Unary(Ok(value)) => println!("Response: {}", value),
DynamicResponse::Unary(Err(status)) => eprintln!("gRPC Error: {:?}", status),
_ => {}
}
println!("{:?}", response);
Ok(())
}
```
### 2. Using a Local Descriptor File
### 2. OnlineWithoutReflection (Local Schema)
If you have a `.bin` file generated by `protoc`, you can load it into the client. This transforms the client's state, disabling reflection and forcing it to look up schemas in the provided file.
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.
```rust
use granc_core::client::GrancClient;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect (starts in Reflection mode)
let client = GrancClient::connect("http://localhost:50051").await?;
// Read the descriptor file
let descriptor_bytes = std::fs::read("descriptor.bin")?;
// Transition to File Descriptor mode
// Transition state: Online -> OnlineWithoutReflection
let mut client = client.with_file_descriptor(descriptor_bytes)?;
// Now use this client for requests. It will NOT query the server for schema.
// Introspection is now SYNCHRONOUS (in-memory)
let services = client.list_services();
println!("Services in file: {:?}", services);
println!("Local services: {:?}", services);
// Dynamic calls use the local schema to encode/decode
// client.dynamic(req).await?;
Ok(())
}
```
### 3. Schema Introspection
### 3. Offline (Introspection Only)
Both client states expose methods to inspect the available schema, but their APIs differ slightly because reflection requires network calls (async) while file lookups are in-memory (sync).
#### Using Server Reflection (Async)
This state is useful for building tools that need to inspect `.bin` descriptor files without establishing a network connection.
```rust
// List available services (requires network call)
let services = client.list_services().await?;
use granc_core::client::GrancClient;
// Get a specific descriptor (requires network call)
// Returns Result<Descriptor, Error>
let descriptor = client.get_descriptor_by_symbol("helloworld.Greeter").await?;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let descriptor_bytes = std::fs::read("descriptor.bin")?;
match descriptor {
Descriptor::ServiceDescriptor(svc) => println!("Service: {}", svc.name()),
Descriptor::MessageDescriptor(msg) => println!("Message: {}", msg.name()),
Descriptor::EnumDescriptor(enm) => println!("Enum: {}", enm.name()),
// 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(())
}
```
#### Using Local File (Sync)
```rust
// List available services (immediate, can't fail)
let services = client_fd.list_services();
// Get a specific descriptor (immediate)
// Returns Option<Descriptor>
if let Some(descriptor) = client_fd.get_descriptor_by_symbol("helloworld.Greeter") {
println!("Found symbol: {:?}", descriptor);
} else {
println!("Symbol not found in file");
}
```
## 🛠️ Internal Components
@ -117,39 +109,15 @@ We expose the internal building blocks of `granc` for developers who need more g
### 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:
* `unary`
* `server_streaming`
* `client_streaming`
* `bidirectional_streaming`
```rust
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?;
```
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.
* **Encoder**: Validates `serde_json::Value` against the input `MessageDescriptor` and serializes it.
* **Decoder**: Deserializes bytes into a `DynamicMessage` and converts it back to `serde_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.
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