# 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 uses a **Typestate Pattern** to ensure safety and correctness regarding how the Protobuf schema is resolved. There are three distinct states: 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. 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. ```rust use granc_core::client::{GrancClient, DynamicRequest, DynamicResponse}; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box> { // 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. ```rust use granc_core::client::GrancClient; #[tokio::main] async fn main() -> Result<(), Box> { 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. ```rust use granc_core::client::GrancClient; fn main() -> Result<(), Box> { 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.