granc/granc-core/README.md
Víctor Martínez 7bc2e4c0a9
refactor: separate core logic into a library crate granc-core (#16)
This pull request introduces a significant internal refactor of the `granc` project, decoupling core dynamic gRPC client logic into a new reusable library crate (`granc-core`). It also improves project organization, updates documentation, and enhances workspace configuration. The main CLI functionality is now built atop this new core, making future maintenance and extensibility easier.

**Project structure and workspace improvements:**

- Created a new crate, `granc-core`, to encapsulate all core dynamic gRPC client logic, including schema resolution, dynamic request dispatch, and reflection support. This enables potential reuse outside the CLI and clarifies project boundaries. (`granc-core/Cargo.toml`, `granc-core/src/client.rs`, [[1]](diffhunk://#diff-dd6f7ed591a1bd2577444d0079c1f56851ef74e3b9df75a86ef4af76681435f6R1-R126) [[2]](diffhunk://#diff-ddab7585cf4c860c9922ed56471bccf5804da60f0ccb174158fd31b9b82457abR1-R46) [[3]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adR1-R186)
- Updated workspace configuration in `Cargo.toml` to include `granc-core`, centralize dependency versions, and set workspace-wide package metadata for consistency. (`Cargo.toml`, [Cargo.tomlL2-R26](diffhunk://#diff-2e9d962a08321605940b5a657135052fbcef87b5e360662bb527c96d9a615542L2-R26))
- Adjusted `echo-service` and other crates to use workspace-wide settings for edition and authors. (`echo-service/Cargo.toml`, [echo-service/Cargo.tomlL3-R3](diffhunk://#diff-e74eb8a3bebf341a9bee1cdcd5cd3a50e15998db5a9df9eaf9e7aec341287b1eL3-R3))

**Documentation :**

- Added a detailed `README.md` for both the main project and the new `granc-core` library, providing clear installation, usage, and architecture guidance for users and contributors. (`README.md`, `granc-core/README.md`, [[1]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R1-R172) [[2]](diffhunk://#diff-dd6f7ed591a1bd2577444d0079c1f56851ef74e3b9df75a86ef4af76681435f6R1-R126)

**Build and tooling updates:**

- Updated `Makefile.toml` to use workspace-wide test runs and renamed tasks/binaries for consistency with the new crate layout. (`Makefile.toml`, [[1]](diffhunk://#diff-9375fd04332c86472d7be397ef09428cb86babd8826880a5835bd1d1c1bdbc08L18-R18) [[2]](diffhunk://#diff-9375fd04332c86472d7be397ef09428cb86babd8826880a5835bd1d1c1bdbc08L45-R50)

---

**Key changes:**

**1. Core library extraction and refactor**
- Moved dynamic gRPC client logic (including `GrancClient`, request/response types, and reflection handling) into a new `granc-core` crate, decoupling it from the CLI and preparing for independent publishing. [[1]](diffhunk://#diff-ddab7585cf4c860c9922ed56471bccf5804da60f0ccb174158fd31b9b82457abR1-R46) [[2]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adR1-R186)

**2. Workspace and dependency management**
- Updated the root `Cargo.toml` to add `granc-core` as a workspace member, centralize dependency versions, and set workspace-wide metadata fields (authors, edition, license, etc.).
- Adjusted `echo-service` and new crates to inherit workspace settings for consistency.

**3. Documentation**
- Updated and added comprehensive `README.md` files for both the main project and the new core library, with installation, usage, and architecture sections. [[1]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R1-R172) [[2]](diffhunk://#diff-dd6f7ed591a1bd2577444d0079c1f56851ef74e3b9df75a86ef4af76681435f6R1-R126)
- Introduced a `CHANGELOG.md` to document project history and recent changes.

**4. Build and CI tooling**
- Updated test and generation commands in `Makefile.toml` to reflect the new workspace structure and binary names. [[1]](diffhunk://#diff-9375fd04332c86472d7be397ef09428cb86babd8826880a5835bd1d1c1bdbc08L18-R18) [[2]](diffhunk://#diff-9375fd04332c86472d7be397ef09428cb86babd8826880a5835bd1d1c1bdbc08L45-R50)

**5. Housekeeping**
- Removed outdated or redundant files as part of the refactor. [[1]](diffhunk://#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4edL1) [[2]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L1)

This refactor lays the groundwork for improved maintainability, easier future development, and potential wider adoption of the dynamic gRPC client logic outside the CLI.
2026-01-22 15:07:10 +01:00

126 lines
4.6 KiB
Markdown

# 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.
## 📦 Installation
Add this to your `Cargo.toml`:
```toml
[dependencies]
granc_core = "0.2.3"
tokio = { version = "1", features = ["full"] }
serde_json = "1"
```
## 🚀 High-Level Usage
The primary entry point is the [`GrancClient`]. It acts as an orchestrator that:
1. Connects to a gRPC server.
2. Resolves the schema (either from a local file or via Server Reflection).
3. Determines the method type (Unary, Server Streaming, etc.).
4. Execute the request using JSON.
### Example: Making a Dynamic Call
```rust
use granc_core::client::{GrancClient, DynamicRequest, DynamicResponse};
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect to the server
let mut client = GrancClient::connect("http://localhost:50051").await?;
// Prepare the request
// If you don't provide a file_descriptor_set, the client will attempt
// to fetch the schema from the server's reflection service automatically.
let request = DynamicRequest {
service: "helloworld.Greeter".to_string(),
method: "SayHello".to_string(),
body: json!({ "name": "World" }),
headers: vec![],
file_descriptor_set: None, // Uses Server Reflection
};
let response = client.dynamic(request).await?;
match response {
DynamicResponse::Unary(Ok(value)) => {
println!("Response: {}", value);
}
DynamicResponse::Unary(Err(status)) => {
eprintln!("gRPC Error: {:?}", status);
}
DynamicResponse::Streaming(Ok(stream)) => {
for msg in stream {
println!("Stream Msg: {:?}", msg);
}
}
_ => eprintln!("Unexpected response type"),
}
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 (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.
```rust
use granc_core::reflection::client::ReflectionClient;
let mut reflection = ReflectionClient::new(channel);
let fd_set = reflection.file_descriptor_set_by_symbol("my.package.Service").await?;
```
You can then build a `prost_reflect::DescriptorPool` with the returned `prost_types::FileDescriptorSet` to be able to inspect in detail the descriptor.
## ⚖️ License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.