mirror of
https://codeberg.org/JasterV/granc.git
synced 2026-04-26 18:40:05 +00:00
update documentation
This commit is contained in:
parent
c2553016a7
commit
9002c2ef3f
2 changed files with 45 additions and 77 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
let services = client.list_services();
|
||||
println!("Services in file: {:?}", services);
|
||||
// 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. 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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue