granc_core 0.5.0

Cranc gRPC CLI core library
Documentation
# Granc Core

[![Crates.io](https://img.shields.io/crates/v/granc_core.svg)](https://crates.io/crates/granc_core)
[![Documentation](https://docs.rs/granc_core/badge.svg)](https://docs.rs/granc_core)
[![License](https://img.shields.io/crates/l/granc_core.svg)](https://github.com/JasterV/granc/blob/main/LICENSE)

**`granc-core`** is the foundational library powering the [Granc CLI](https://crates.io/crates/granc). 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.

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. Using Server Reflection (Default)

By default, when you connect, the client is ready to use the server's reflection service to resolve methods and types dynamically.

```rust
use granc_core::client::{GrancClient, DynamicRequest, DynamicResponse};
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Connect (starts in Reflection mode)
    let mut client = GrancClient::connect("http://localhost:50051").await?;

    let request = DynamicRequest {
        service: "helloworld.Greeter".to_string(),
        method: "SayHello".to_string(),
        body: json!({ "name": "World" }),
        headers: vec![],
    };

    // Execute (Schema is fetched automatically via reflection)
    let response = client.dynamic(request).await?;

    match response {
        DynamicResponse::Unary(Ok(value)) => println!("Response: {}", value),
        DynamicResponse::Unary(Err(status)) => eprintln!("gRPC Error: {:?}", status),
        _ => {}
    }

    Ok(())
}

```

### 2. Using a Local Descriptor File

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.

```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
    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);

    Ok(())
}

```

### 3. Schema Introspection

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)

```rust
// List available services (requires network call)
let services = client.list_services().await?;

// Get a specific descriptor (requires network call)
// Returns Result<Descriptor, Error>
let descriptor = client.get_descriptor_by_symbol("helloworld.Greeter").await?;

match descriptor {
    Descriptor::ServiceDescriptor(svc) => println!("Service: {}", svc.name()),
    Descriptor::MessageDescriptor(msg) => println!("Message: {}", msg.name()),
    Descriptor::EnumDescriptor(enm) => println!("Enum: {}", enm.name()),
}
```

#### 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

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:

* `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?;

```

### 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.

## ⚖️ License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.