mirror of
https://codeberg.org/JasterV/granc.git
synced 2026-04-26 18:40:05 +00:00
[feature] + [refactor] => Allow ALL commands to be executed against a local file descriptor (#28)
This PR makes the `--file-descriptor-set` CLI option to be global for all the commands. By consequence, the `GrancClient` has been refactored to use a typestate pattern to ensure that there are two separate and decoupled implementations of the behaviour of the client when a file descriptor is loaded and when server reflection is enabled, since both cases have by nature separate error cases and return values invariants. It also significantly improves the documentation for both the main `README.md` and the `granc-core/README.md`, clarifying usage patterns, command-line options, and the internal architecture of the `GrancClient` API. **API and architecture changes:** * Refactored `granc-core/src/client.rs` to implement the typestate pattern for `GrancClient`, splitting logic into `with_server_reflection` and `with_file_descriptor` modules. Updated documentation comments to explain state transitions and usage. * Simplified the `DynamicRequest` struct by removing the `file_descriptor_set` field, as schema resolution is now determined by the client's state rather than per-request. **Documentation improvements:** * Expanded and reorganized the main `README.md` to clearly explain new and existing features, including local introspection, command-line options, and usage examples for both server reflection and local file descriptor sets. The documentation now covers how to use introspection commands with and without server reflection, and provides concrete example commands and expected output. [[1]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R16-R33) [[2]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L60-R73) [[3]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L84-R159) * Updated the `granc-core/README.md` to document the typestate pattern of `GrancClient`, provide clear async/sync usage examples for both reflection and file descriptor modes, and clarify schema introspection methods.
This commit is contained in:
parent
d74d8a6bf2
commit
d9001fc87e
20 changed files with 1150 additions and 789 deletions
42
Cargo.lock
generated
42
Cargo.lock
generated
|
|
@ -336,34 +336,13 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "granc"
|
name = "granc"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"colored",
|
"colored",
|
||||||
"granc_core 0.4.0",
|
"granc_core",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tonic",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "granc_core"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "470843569f07903a7e34f3b37c8d155e3a13e8450da1aea4dd6126370ce8259d"
|
|
||||||
dependencies = [
|
|
||||||
"futures-util",
|
|
||||||
"http",
|
|
||||||
"http-body",
|
|
||||||
"prost",
|
|
||||||
"prost-reflect",
|
|
||||||
"prost-types",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
|
||||||
"tokio-stream",
|
|
||||||
"tonic",
|
|
||||||
"tonic-prost",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -382,7 +361,6 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tonic",
|
"tonic",
|
||||||
"tonic-prost",
|
|
||||||
"tonic-reflection",
|
"tonic-reflection",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -703,9 +681,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.105"
|
version = "1.0.106"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
@ -798,9 +776,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.43"
|
version = "1.0.44"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
|
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
@ -919,9 +897,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.6.1"
|
version = "0.6.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
|
checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
|
|
@ -1322,6 +1300,6 @@ checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.16"
|
version = "1.0.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65"
|
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ homepage = "https://github.com/JasterV/granc"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = "https://github.com/JasterV/granc"
|
repository = "https://github.com/JasterV/granc"
|
||||||
rust-version = "1.89"
|
rust-version = "1.89"
|
||||||
version = "0.3.1"
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
|
|
@ -21,6 +20,4 @@ prost = "0.14"
|
||||||
prost-reflect = "0.16.3"
|
prost-reflect = "0.16.3"
|
||||||
prost-types = "0.14"
|
prost-types = "0.14"
|
||||||
tonic = "0.14"
|
tonic = "0.14"
|
||||||
tonic-prost = "0.14"
|
|
||||||
tonic-prost-build = "0.14"
|
|
||||||
tonic-reflection = "0.14"
|
tonic-reflection = "0.14"
|
||||||
|
|
|
||||||
109
README.md
109
README.md
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
> ⚠️ **Status: Experimental**
|
> ⚠️ **Status: Experimental**
|
||||||
>
|
>
|
||||||
> This project is currently in a **highly experimental phase**. It is a working prototype intended for testing and development purposes. APIs, command-line arguments, and internal logic are subject to breaking changes. Please use with caution.
|
> This project is a working prototype intended for testing and development purposes. APIs, command-line arguments, and internal logic are subject to breaking changes. Please use with caution.
|
||||||
|
|
||||||
**Granc** (gRPC + Cranc, Crab in Catalan) is a lightweight, dynamic gRPC CLI tool written in Rust.
|
**Granc** (gRPC + Cranc, Crab in Catalan) is a lightweight, dynamic gRPC CLI tool written in Rust.
|
||||||
|
|
||||||
|
|
@ -13,14 +13,16 @@ It allows you to make gRPC calls to any server using simple JSON payloads, witho
|
||||||
|
|
||||||
It is heavily inspired by tools like `grpcurl` but built to leverage the safety and performance of the Rust ecosystem (Tonic + Prost).
|
It is heavily inspired by tools like `grpcurl` but built to leverage the safety and performance of the Rust ecosystem (Tonic + Prost).
|
||||||
|
|
||||||
|
|
||||||
## 🚀 Features
|
## 🚀 Features
|
||||||
|
|
||||||
* **Dynamic Encoding/Decoding**: Transcodes JSON to Protobuf (and vice versa) on the fly using `prost-reflect`.
|
* **Dynamic Encoding/Decoding**: Transcodes JSON to Protobuf (and vice versa) on the fly using `prost-reflect`.
|
||||||
* **Smart Dispatch**: Automatically detects if a call is Unary, Server Streaming, Client Streaming, or Bidirectional based on the descriptor.
|
|
||||||
* **Server Reflection**: Can fetch schemas directly from the server, removing the need to pass a local file descriptor set file (`.bin` or `.pb`).
|
|
||||||
* **Metadata Support**: Easily attach custom headers (authorization, tracing) to your requests.
|
* **Metadata Support**: Easily attach custom headers (authorization, tracing) to your requests.
|
||||||
* **Fast Fail Validation**: Validates your JSON *before* hitting the network.
|
* **Fast Fail Validation**: Validates your JSON *before* hitting the network.
|
||||||
* **Introspection Tools**: Commands to list services and describe services/messages.
|
* **Smart Dispatch**: Automatically detects if a call is Unary, Server Streaming, Client Streaming, or Bidirectional based on the descriptor.
|
||||||
|
* **Server Reflection**: Can fetch schemas directly from the server, removing the need to pass a local file descriptor set file (`.bin` or `.pb`).
|
||||||
|
* **Introspection Tools**: Commands to list services and describe services, messages, and enums.
|
||||||
|
* **Local Introspection**: In addition to making network requests, Granc can also be used as a local introspection tool for file descriptor binary files. You can load a local `.bin` file to inspect services, messages, and enums without needing to fetch the schema from a server.
|
||||||
* **Zero Compilation Dependencies**: Does not require generating Rust code for your protos. Just point to a descriptor file.
|
* **Zero Compilation Dependencies**: Does not require generating Rust code for your protos. Just point to a descriptor file.
|
||||||
* **Tonic 0.14**: Built on the latest stable Rust gRPC stack.
|
* **Tonic 0.14**: Built on the latest stable Rust gRPC stack.
|
||||||
|
|
||||||
|
|
@ -57,7 +59,7 @@ protoc \
|
||||||
**Syntax:**
|
**Syntax:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
granc <URL> <COMMAND> [ARGS]
|
granc <URL> [OPTIONS] <COMMAND> [ARGS]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Global Arguments
|
### Global Arguments
|
||||||
|
|
@ -65,6 +67,7 @@ granc <URL> <COMMAND> [ARGS]
|
||||||
| Argument | Description | Required |
|
| Argument | Description | Required |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `<URL>` | Server address (e.g., `http://[::1]:50051`). Must be the first argument. | **Yes** |
|
| `<URL>` | Server address (e.g., `http://[::1]:50051`). Must be the first argument. | **Yes** |
|
||||||
|
| `--file-descriptor-set` | Path to the binary FileDescriptorSet (`.bin`). If omitted, Granc attempts to use Server Reflection. | No |
|
||||||
|
|
||||||
### Commands
|
### Commands
|
||||||
|
|
||||||
|
|
@ -73,7 +76,7 @@ granc <URL> <COMMAND> [ARGS]
|
||||||
Performs a gRPC call using a JSON body.
|
Performs a gRPC call using a JSON body.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
granc http://localhost:50051 call <ENDPOINT> --body <JSON> [OPTIONS]
|
granc http://localhost:50051 [OPTIONS] call <ENDPOINT> --body <JSON> [ARGS]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Argument/Flag | Description | Required |
|
| Argument/Flag | Description | Required |
|
||||||
|
|
@ -81,49 +84,86 @@ granc http://localhost:50051 call <ENDPOINT> --body <JSON> [OPTIONS]
|
||||||
| `<ENDPOINT>` | Fully qualified method name (e.g., `my.package.Service/Method`). | **Yes** |
|
| `<ENDPOINT>` | Fully qualified method name (e.g., `my.package.Service/Method`). | **Yes** |
|
||||||
| `--body` | The request body in JSON format. Object `{}` for unary, Array `[]` for streaming. | **Yes** |
|
| `--body` | 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` | Path to the binary FileDescriptorSet (`.bin`) if not using reflection. | No |
|
|
||||||
|
|
||||||
##### JSON Body Format
|
**Example using Server Reflection:**
|
||||||
|
|
||||||
* **Unary / Server Streaming**: Provide a single JSON object `{ ... }`.
|
|
||||||
* **Client / Bidirectional Streaming**: Provide a JSON array of objects `[ { ... }, { ... } ]`.
|
|
||||||
|
|
||||||
##### Automatic Server Reflection
|
|
||||||
|
|
||||||
If you omit the `--file-descriptor-set` flag, Granc will automatically attempt to connect to the server's reflection service to download the necessary schemas.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
granc http://localhost:50051 call --body '{"name": "Ferris"}' helloworld.Greeter/SayHello
|
granc http://localhost:50051 call helloworld.Greeter/SayHello --body '{"name": "Ferris"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
This requires the server to have the [`grpc.reflection.v1`](https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto) service enabled.
|
```json
|
||||||
|
{
|
||||||
|
"message": "Hello Ferris"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
#### 2. `list` (Service Discovery) (Server reflection required)
|
**Example using a Local Descriptor File:**
|
||||||
|
|
||||||
Lists all services exposed by the server.
|
```bash
|
||||||
|
granc http://localhost:50051 --file-descriptor-set ./descriptors.bin call helloworld.Greeter/SayHello --body '{"name": "Ferris"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. `list` (Service Discovery)
|
||||||
|
|
||||||
|
Lists all services exposed by the server (via reflection) or contained in the provided descriptor file.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
granc http://localhost:50051 list
|
granc http://localhost:50051 list
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3. `describe` (Introspection) (Server reflection required)
|
```
|
||||||
|
Available Services:
|
||||||
Inspects services, messages or enums and prints their Protobuf definition.
|
- grpc.reflection.v1.ServerReflection
|
||||||
|
- helloworld.Greeter
|
||||||
**Describe Service:**
|
|
||||||
|
|
||||||
Describe in detail all methods of a service.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
granc http://localhost:50051 describe my.package.Greeter
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Describe Message:**
|
**Listing services from a file:**
|
||||||
|
|
||||||
Shows the fields of a specific message type.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
granc http://localhost:50051 describe my.package.HelloRequest
|
granc http://localhost:50051 --file-descriptor-set ./descriptors.bin list
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. `describe` (Introspection)
|
||||||
|
|
||||||
|
Inspects a specific symbol (Service, Message, or Enum) and prints its Protobuf definition in a colored, human-readable format.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
granc http://localhost:50051 describe helloworld.Greeter
|
||||||
|
```
|
||||||
|
|
||||||
|
```proto
|
||||||
|
service Greeter {
|
||||||
|
rpc SayHello(helloworld.HelloRequest) returns (helloworld.HelloReply);
|
||||||
|
rpc StreamHello(stream helloworld.HelloRequest) returns (stream helloworld.HelloReply);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Describing a Message using a Local File:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
granc http://localhost:50051 --file-descriptor-set ./descriptors.bin describe helloworld.HelloRequest
|
||||||
|
```
|
||||||
|
|
||||||
|
```proto
|
||||||
|
message HelloRequest {
|
||||||
|
string name = 1;
|
||||||
|
int32 age = 2;
|
||||||
|
repeated string tags = 3;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Describing an Enum:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
granc http://localhost:50051 describe my.package.Status
|
||||||
|
```
|
||||||
|
|
||||||
|
```proto
|
||||||
|
enum Status {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
ACTIVE = 1;
|
||||||
|
INACTIVE = 2;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔮 Roadmap
|
## 🔮 Roadmap
|
||||||
|
|
@ -138,7 +178,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.
|
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**](./granc-core/README.md) for examples on how to use the `GrancClient` programmatically.
|
* **Documentation & Usage**: See the **[`granc-core` README](https://www.google.com/search?q=./granc-core/README.md)** for examples on how to use the `GrancClient` programmatically.
|
||||||
* **Crate**: [`granc-core`](https://crates.io/crates/granc_core)
|
* **Crate**: [`granc-core`](https://crates.io/crates/granc_core)
|
||||||
|
|
||||||
## ⚠️ Common Errors
|
## ⚠️ Common Errors
|
||||||
|
|
@ -164,6 +204,7 @@ Contributions are welcome! Please run the Makefile checks before submitting a PR
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo make ci # Checks formatting, lints, and runs tests
|
cargo make ci # Checks formatting, lints, and runs tests
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📄 License
|
## 📄 License
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ thiserror = "2.0.18"
|
||||||
tokio = { workspace = true, features = ["sync"] }
|
tokio = { workspace = true, features = ["sync"] }
|
||||||
tokio-stream = "0.1.18"
|
tokio-stream = "0.1.18"
|
||||||
tonic = { workspace = true }
|
tonic = { workspace = true }
|
||||||
tonic-prost = { workspace = true }
|
|
||||||
tonic-reflection = { workspace = true }
|
tonic-reflection = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,11 @@ Instead of strictly typed Rust structs, this library bridges standard `serde_jso
|
||||||
|
|
||||||
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 acts as an orchestrator that connects to a gRPC server and provides methods for both executing requests and inspecting the server's schema.
|
||||||
|
|
||||||
### 1. Making a Dynamic Call
|
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`.
|
||||||
|
|
||||||
The `dynamic` method handles the full request lifecycle:
|
### 1. Using Server Reflection (Default)
|
||||||
|
|
||||||
1. Resolves the schema (either from a local file or via Server Reflection).
|
By default, when you connect, the client is ready to use the server's reflection service to resolve methods and types dynamically.
|
||||||
2. Determines the method type (Unary, Server Streaming, etc.).
|
|
||||||
3. Executes the request using JSON.
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use granc_core::client::{GrancClient, DynamicRequest, DynamicResponse};
|
use granc_core::client::{GrancClient, DynamicRequest, DynamicResponse};
|
||||||
|
|
@ -26,35 +24,23 @@ use serde_json::json;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Connect to the server
|
// Connect (starts in Reflection mode)
|
||||||
let mut client = GrancClient::connect("http://localhost:50051").await?;
|
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 {
|
let request = DynamicRequest {
|
||||||
service: "helloworld.Greeter".to_string(),
|
service: "helloworld.Greeter".to_string(),
|
||||||
method: "SayHello".to_string(),
|
method: "SayHello".to_string(),
|
||||||
body: json!({ "name": "World" }),
|
body: json!({ "name": "World" }),
|
||||||
headers: vec![],
|
headers: vec![],
|
||||||
file_descriptor_set: None, // Uses Server Reflection
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Execute (Schema is fetched automatically via reflection)
|
||||||
let response = client.dynamic(request).await?;
|
let response = client.dynamic(request).await?;
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
DynamicResponse::Unary(Ok(value)) => {
|
DynamicResponse::Unary(Ok(value)) => println!("Response: {}", value),
|
||||||
println!("Response: {}", value);
|
DynamicResponse::Unary(Err(status)) => eprintln!("gRPC Error: {:?}", status),
|
||||||
}
|
_ => {}
|
||||||
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(())
|
Ok(())
|
||||||
|
|
@ -62,22 +48,66 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Schema Introspection
|
### 2. Using a Local Descriptor File
|
||||||
|
|
||||||
`GrancClient` exposes several methods to inspect the server's available services and types using reflection.
|
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
|
```rust
|
||||||
// List all services exposed by the server
|
use granc_core::client::GrancClient;
|
||||||
let services = client.list_services().await?;
|
|
||||||
println!("Available Services: {:?}", services);
|
|
||||||
|
|
||||||
// Get the descriptor for a specific type
|
#[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?;
|
let descriptor = client.get_descriptor_by_symbol("helloworld.Greeter").await?;
|
||||||
|
|
||||||
match descriptor {
|
match descriptor {
|
||||||
Descriptor::MessageDescriptor(descriptor)) => println!("{}", descriptor.name())
|
Descriptor::ServiceDescriptor(svc) => println!("Service: {}", svc.name()),
|
||||||
Descriptor::ServiceDescriptor(descriptor)) => println!("{}", descriptor.name())
|
Descriptor::MessageDescriptor(msg) => println!("Message: {}", msg.name()),
|
||||||
Descriptor::EnumDescriptor(descriptor)) => println!("{}", descriptor.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");
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -121,21 +151,6 @@ 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`),
|
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.
|
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.
|
||||||
|
|
||||||
```rust
|
|
||||||
use granc_core::reflection::client::ReflectionClient;
|
|
||||||
|
|
||||||
let mut reflection = ReflectionClient::new(channel);
|
|
||||||
|
|
||||||
// List services
|
|
||||||
let services = reflection.list_services().await?;
|
|
||||||
|
|
||||||
// Fetch full schema for a symbol
|
|
||||||
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
|
## ⚖️ License
|
||||||
|
|
||||||
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
|
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
|
||||||
|
|
|
||||||
|
|
@ -1,103 +1,72 @@
|
||||||
//! # Granc Client
|
//! # Granc Client
|
||||||
//!
|
//!
|
||||||
//! This module implements the high-level logic for executing dynamic gRPC requests
|
//! This module implements the high-level logic for executing dynamic gRPC requests.
|
||||||
//! and offers support for reflection operations if the server supports it.
|
|
||||||
//!
|
//!
|
||||||
//! The [`GrancClient`] is the primary entry point for consumers of this library.
|
//! The [`GrancClient`] uses a **Typestate Pattern** to ensure safety and correctness regarding
|
||||||
//! It abstracts away the complexity of connection management, schema resolution (reflection vs. file descriptors),
|
//! how the Protobuf schema is resolved. It has two possible states:
|
||||||
//! and generic gRPC transport.
|
|
||||||
//!
|
//!
|
||||||
//! ## Example Usage
|
//! 1. **[`WithServerReflection`]**: The default state. The client is connected
|
||||||
|
//! to a server and uses the gRPC Server Reflection Protocol (`grpc.reflection.v1`) to discover
|
||||||
|
//! services and fetch schemas on the fly.
|
||||||
|
//! 2. **[`WithFileDescriptor`]**: The client has been provided with a specific
|
||||||
|
//! binary `FileDescriptorSet` (e.g., loaded from a `.bin` file). In this state, reflection is
|
||||||
|
//! disabled, and all lookups are performed against the provided file.
|
||||||
|
//!
|
||||||
|
//! ## Example: State Transition
|
||||||
//!
|
//!
|
||||||
//! ```rust,no_run
|
//! ```rust,no_run
|
||||||
//! use granc_core::client::{GrancClient, DynamicRequest};
|
//! use granc_core::client::GrancClient;
|
||||||
//! use serde_json::json;
|
|
||||||
//!
|
//!
|
||||||
//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
//! // 1. Connect to the server
|
//! // Connect (starts in Reflection state)
|
||||||
//! let mut client = GrancClient::connect("http://localhost:50051").await?;
|
//! let mut client_reflection = GrancClient::connect("http://localhost:50051").await?;
|
||||||
//!
|
//!
|
||||||
//! // 2. Prepare the request (using server reflection)
|
//! // The API here is async
|
||||||
//! let request = DynamicRequest {
|
//! let services = client_reflection.list_services().await?;
|
||||||
//! service: "helloworld.Greeter".to_string(),
|
|
||||||
//! method: "SayHello".to_string(),
|
|
||||||
//! body: json!({ "name": "Ferris" }),
|
|
||||||
//! headers: vec![],
|
|
||||||
//! file_descriptor_set: None,
|
|
||||||
//! };
|
|
||||||
//!
|
//!
|
||||||
//! // 3. Execute the call
|
//! // 2Transition to File Descriptor state
|
||||||
//! let response = client.dynamic(request).await?;
|
//! let bytes = std::fs::read("descriptor.bin")?;
|
||||||
//! println!("Response: {:?}", response);
|
//! let mut client_fd = client_reflection.with_file_descriptor(bytes)?;
|
||||||
|
//!
|
||||||
|
//! // Now operations use the local file and are sync
|
||||||
|
//! let services = client_fd.list_services();
|
||||||
//! # Ok(())
|
//! # Ok(())
|
||||||
//! # }
|
//! # }
|
||||||
//! ```
|
//! ```
|
||||||
use crate::{
|
pub mod with_file_descriptor;
|
||||||
BoxError,
|
pub mod with_server_reflection;
|
||||||
grpc::client::{GrpcClient, GrpcRequestError},
|
|
||||||
reflection::client::{ReflectionClient, ReflectionResolveError},
|
|
||||||
};
|
|
||||||
use futures_util::Stream;
|
|
||||||
use http_body::Body as HttpBody;
|
|
||||||
use prost_reflect::{
|
|
||||||
DescriptorError, DescriptorPool, EnumDescriptor, MessageDescriptor, ServiceDescriptor,
|
|
||||||
};
|
|
||||||
use tokio_stream::StreamExt;
|
|
||||||
use tonic::{
|
|
||||||
Code,
|
|
||||||
transport::{Channel, Endpoint},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
use crate::{grpc::client::GrpcClient, reflection::client::ReflectionClient};
|
||||||
pub enum ClientConnectError {
|
use prost_reflect::{DescriptorPool, EnumDescriptor, MessageDescriptor, ServiceDescriptor};
|
||||||
#[error("Invalid URL '{0}': {1}")]
|
use std::fmt::Debug;
|
||||||
InvalidUrl(String, #[source] tonic::transport::Error),
|
use tonic::transport::Channel;
|
||||||
#[error("Failed to connect to '{0}': {1}")]
|
|
||||||
ConnectionFailed(String, #[source] tonic::transport::Error),
|
/// The main client for interacting with gRPC servers dynamically.
|
||||||
|
///
|
||||||
|
/// The generic parameter `T` represents the current state of the client, determining
|
||||||
|
/// its capabilities and how it resolves Protobuf schemas.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct GrancClient<T> {
|
||||||
|
state: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
/// The state for a client that uses a local `DescriptorPool` for schema resolution.
|
||||||
pub enum ListServicesError {
|
#[derive(Debug, Clone)]
|
||||||
#[error("Reflection resolution failed: '{0}'")]
|
pub struct WithFileDescriptor<S = Channel> {
|
||||||
ReflectionResolve(#[from] ReflectionResolveError),
|
grpc_client: GrpcClient<S>,
|
||||||
|
pool: DescriptorPool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
/// The state for a client that uses Server Reflection for schema resolution.
|
||||||
pub enum GetDescriptorError {
|
#[derive(Debug, Clone)]
|
||||||
#[error("Reflection resolution failed: '{0}'")]
|
pub struct WithServerReflection<S = Channel> {
|
||||||
ReflectionResolve(#[from] ReflectionResolveError),
|
reflection_client: ReflectionClient<S>,
|
||||||
#[error("Failed to decode file descriptor set: '{0}'")]
|
grpc_client: GrpcClient<S>,
|
||||||
DescriptorError(#[from] DescriptorError),
|
|
||||||
#[error("Descriptor at path '{0}' not found")]
|
|
||||||
NotFound(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum DynamicCallError {
|
|
||||||
#[error("Invalid input: '{0}'")]
|
|
||||||
InvalidInput(String),
|
|
||||||
|
|
||||||
#[error("Service '{0}' not found")]
|
|
||||||
ServiceNotFound(String),
|
|
||||||
|
|
||||||
#[error("Method '{0}' not found")]
|
|
||||||
MethodNotFound(String),
|
|
||||||
|
|
||||||
#[error("Reflection resolution failed: '{0}'")]
|
|
||||||
ReflectionResolve(#[from] ReflectionResolveError),
|
|
||||||
|
|
||||||
#[error("Failed to decode file descriptor set: '{0}'")]
|
|
||||||
DescriptorError(#[from] DescriptorError),
|
|
||||||
|
|
||||||
#[error("gRPC client request error: '{0}'")]
|
|
||||||
GrpcRequestError(#[from] GrpcRequestError),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A request object encapsulating all necessary information to perform a dynamic gRPC call.
|
/// A request object encapsulating all necessary information to perform a dynamic gRPC call.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct DynamicRequest {
|
pub struct DynamicRequest {
|
||||||
/// Optional binary `FileDescriptorSet` (e.g. generated by `protoc`).
|
|
||||||
/// If `None`, the client will attempt to use Server Reflection.
|
|
||||||
pub file_descriptor_set: Option<Vec<u8>>,
|
|
||||||
/// The JSON body of the request.
|
/// The JSON body of the request.
|
||||||
/// - For Unary/ServerStreaming: An Object `{}`.
|
/// - For Unary/ServerStreaming: An Object `{}`.
|
||||||
/// - For ClientStreaming/Bidirectional: An Array of Objects `[{}]`.
|
/// - For ClientStreaming/Bidirectional: An Array of Objects `[{}]`.
|
||||||
|
|
@ -111,6 +80,7 @@ pub struct DynamicRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The result of a dynamic gRPC call.
|
/// The result of a dynamic gRPC call.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub enum DynamicResponse {
|
pub enum DynamicResponse {
|
||||||
/// A single response message (for Unary and Client Streaming calls).
|
/// A single response message (for Unary and Client Streaming calls).
|
||||||
Unary(Result<serde_json::Value, tonic::Status>),
|
Unary(Result<serde_json::Value, tonic::Status>),
|
||||||
|
|
@ -118,7 +88,10 @@ pub enum DynamicResponse {
|
||||||
Streaming(Result<Vec<Result<serde_json::Value, tonic::Status>>, tonic::Status>),
|
Streaming(Result<Vec<Result<serde_json::Value, tonic::Status>>, tonic::Status>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A file descriptor of either a message, service or enum
|
/// A generic wrapper for different types of Protobuf descriptors.
|
||||||
|
///
|
||||||
|
/// This enum allows the client to return a single type when resolving symbols,
|
||||||
|
/// regardless of whether the symbol points to a Service, a Message, or an Enum.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Descriptor {
|
pub enum Descriptor {
|
||||||
MessageDescriptor(MessageDescriptor),
|
MessageDescriptor(MessageDescriptor),
|
||||||
|
|
@ -127,6 +100,7 @@ pub enum Descriptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Descriptor {
|
impl Descriptor {
|
||||||
|
/// Returns the inner [`MessageDescriptor`] if this variant is `MessageDescriptor`.
|
||||||
pub fn message_descriptor(&self) -> Option<&MessageDescriptor> {
|
pub fn message_descriptor(&self) -> Option<&MessageDescriptor> {
|
||||||
match self {
|
match self {
|
||||||
Descriptor::MessageDescriptor(message_descriptor) => Some(message_descriptor),
|
Descriptor::MessageDescriptor(message_descriptor) => Some(message_descriptor),
|
||||||
|
|
@ -134,6 +108,7 @@ impl Descriptor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the inner [`ServiceDescriptor`] if this variant is `ServiceDescriptor`.
|
||||||
pub fn service_descriptor(&self) -> Option<&ServiceDescriptor> {
|
pub fn service_descriptor(&self) -> Option<&ServiceDescriptor> {
|
||||||
match self {
|
match self {
|
||||||
Descriptor::ServiceDescriptor(service_descriptor) => Some(service_descriptor),
|
Descriptor::ServiceDescriptor(service_descriptor) => Some(service_descriptor),
|
||||||
|
|
@ -141,6 +116,7 @@ impl Descriptor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the inner [`EnumDescriptor`] if this variant is `EnumDescriptor`.
|
||||||
pub fn enum_descriptor(&self) -> Option<&EnumDescriptor> {
|
pub fn enum_descriptor(&self) -> Option<&EnumDescriptor> {
|
||||||
match self {
|
match self {
|
||||||
Descriptor::EnumDescriptor(enum_descriptor) => Some(enum_descriptor),
|
Descriptor::EnumDescriptor(enum_descriptor) => Some(enum_descriptor),
|
||||||
|
|
@ -148,212 +124,3 @@ impl Descriptor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The main client for interacting with gRPC servers dynamically.
|
|
||||||
///
|
|
||||||
/// It combines a [`ReflectionClient`] for schema discovery and a [`GrpcClient`] for
|
|
||||||
/// generic transport.
|
|
||||||
pub struct GrancClient<S = Channel> {
|
|
||||||
reflection_client: ReflectionClient<S>,
|
|
||||||
grpc_client: GrpcClient<S>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GrancClient<Channel> {
|
|
||||||
/// Connects to a gRPC server at the specified address.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `addr` - The URI of the server (e.g., `http://localhost:50051`).
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns a [`ClientConnectError`] if the URL is invalid or the connection cannot be established.
|
|
||||||
pub async fn connect(addr: &str) -> Result<Self, ClientConnectError> {
|
|
||||||
let endpoint = Endpoint::new(addr.to_string())
|
|
||||||
.map_err(|e| ClientConnectError::InvalidUrl(addr.to_string(), e))?;
|
|
||||||
|
|
||||||
let channel = endpoint
|
|
||||||
.connect()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ClientConnectError::ConnectionFailed(addr.to_string(), e))?;
|
|
||||||
|
|
||||||
Ok(Self::new(channel))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> GrancClient<S>
|
|
||||||
where
|
|
||||||
S: tonic::client::GrpcService<tonic::body::Body> + Clone,
|
|
||||||
S::ResponseBody: HttpBody<Data = tonic::codegen::Bytes> + Send + 'static,
|
|
||||||
<S::ResponseBody as HttpBody>::Error: Into<BoxError> + Send,
|
|
||||||
{
|
|
||||||
/// Creates a new `GrancClient` wrapping an existing Tonic service (e.g., a `Channel`).
|
|
||||||
pub fn new(service: S) -> Self {
|
|
||||||
let reflection_client = ReflectionClient::new(service.clone());
|
|
||||||
let grpc_client = GrpcClient::new(service);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
reflection_client,
|
|
||||||
grpc_client,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetches the list of all available services exposed by the server.
|
|
||||||
///
|
|
||||||
/// This method relies on the server supporting the gRPC Reflection Protocol (`grpc.reflection.v1`).
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// A list of fully qualified service names (e.g., `["grpc.reflection.v1.ServerReflection", "my.app.Greeter"]`).
|
|
||||||
pub async fn list_services(&mut self) -> Result<Vec<String>, ListServicesError> {
|
|
||||||
self.reflection_client
|
|
||||||
.list_services()
|
|
||||||
.await
|
|
||||||
.map_err(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolves and fetches the [`Descriptor`] for a specific symbol using reflection.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `symbol` - The fully qualified name of the type (e.g., `my.package.Service`).
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns an error if the descriptor cannot be found via reflection or if the resolved descriptor set is invalid.
|
|
||||||
pub async fn get_descriptor_by_symbol(
|
|
||||||
&mut self,
|
|
||||||
symbol: &str,
|
|
||||||
) -> Result<Descriptor, GetDescriptorError> {
|
|
||||||
let fd_set = self
|
|
||||||
.reflection_client
|
|
||||||
.file_descriptor_set_by_symbol(symbol)
|
|
||||||
.await
|
|
||||||
.map_err(|err| match err {
|
|
||||||
ReflectionResolveError::ServerStreamFailure(status)
|
|
||||||
if status.code() == Code::NotFound =>
|
|
||||||
{
|
|
||||||
GetDescriptorError::NotFound(symbol.to_string())
|
|
||||||
}
|
|
||||||
err => GetDescriptorError::ReflectionResolve(err),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let pool = DescriptorPool::from_file_descriptor_set(fd_set)?;
|
|
||||||
|
|
||||||
if let Some(descriptor) = pool.get_service_by_name(symbol) {
|
|
||||||
return Ok(Descriptor::ServiceDescriptor(descriptor));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(descriptor) = pool.get_message_by_name(symbol) {
|
|
||||||
return Ok(Descriptor::MessageDescriptor(descriptor));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(descriptor) = pool.get_enum_by_name(symbol) {
|
|
||||||
return Ok(Descriptor::EnumDescriptor(descriptor));
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(GetDescriptorError::NotFound(symbol.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Executes a dynamic gRPC request.
|
|
||||||
///
|
|
||||||
/// This is the core method of the client. It bridges the user's intent (JSON data)
|
|
||||||
/// to the network (gRPC/Protobuf) by resolving schemas and dispatching the call.
|
|
||||||
///
|
|
||||||
/// # The Process
|
|
||||||
///
|
|
||||||
/// 1. **Schema Resolution**: It builds a [`DescriptorPool`] either by decoding the provided
|
|
||||||
/// `file_descriptor_set` (if present in `request`) or by querying the server's reflection
|
|
||||||
/// endpoint for the requested `service` symbol.
|
|
||||||
/// 2. **Method Lookup**: It searches the pool for the specified `service` and `method`.
|
|
||||||
/// 3. **Dispatch**: Based on whether the method is Client Streaming, Server Streaming, etc.,
|
|
||||||
/// it invokes the appropriate low-level transport method on [`GrpcClient`].
|
|
||||||
/// 4. **Transcoding**: The internal codec handles the conversion between `serde_json::Value`
|
|
||||||
/// and Protobuf bytes on the fly.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns [`DynamicCallError`] if:
|
|
||||||
/// * The file descriptor set can't be decoded.
|
|
||||||
/// * A file descriptor set can't be resolved via reflection (In case a file descriptor set it not passed).
|
|
||||||
/// * The service or method does not exist in the file descriptor set.
|
|
||||||
/// * The input JSON is not valid for the type of call (e.g Using a single JSON for a client stream request).
|
|
||||||
/// * The gRPC request fails.
|
|
||||||
pub async fn dynamic(
|
|
||||||
&mut self,
|
|
||||||
request: DynamicRequest,
|
|
||||||
) -> Result<DynamicResponse, DynamicCallError> {
|
|
||||||
let pool = match request.file_descriptor_set {
|
|
||||||
Some(bytes) => DescriptorPool::decode(bytes.as_slice())?,
|
|
||||||
// If no proto-set file is passed, we'll try to reach the server reflection service
|
|
||||||
None => {
|
|
||||||
let fd_set = self
|
|
||||||
.reflection_client
|
|
||||||
.file_descriptor_set_by_symbol(&request.service)
|
|
||||||
.await?;
|
|
||||||
DescriptorPool::from_file_descriptor_set(fd_set)?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let method = pool
|
|
||||||
.get_service_by_name(&request.service)
|
|
||||||
.ok_or_else(|| DynamicCallError::ServiceNotFound(request.service))?
|
|
||||||
.methods()
|
|
||||||
.find(|m| m.name() == request.method)
|
|
||||||
.ok_or_else(|| DynamicCallError::MethodNotFound(request.method))?;
|
|
||||||
|
|
||||||
match (method.is_client_streaming(), method.is_server_streaming()) {
|
|
||||||
(false, false) => {
|
|
||||||
let result = self
|
|
||||||
.grpc_client
|
|
||||||
.unary(method, request.body, request.headers)
|
|
||||||
.await?;
|
|
||||||
Ok(DynamicResponse::Unary(result))
|
|
||||||
}
|
|
||||||
|
|
||||||
(false, true) => {
|
|
||||||
match self
|
|
||||||
.grpc_client
|
|
||||||
.server_streaming(method, request.body, request.headers)
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
Ok(stream) => Ok(DynamicResponse::Streaming(Ok(stream.collect().await))),
|
|
||||||
Err(status) => Ok(DynamicResponse::Streaming(Err(status))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(true, false) => {
|
|
||||||
let input_stream =
|
|
||||||
json_array_to_stream(request.body).map_err(DynamicCallError::InvalidInput)?;
|
|
||||||
let result = self
|
|
||||||
.grpc_client
|
|
||||||
.client_streaming(method, input_stream, request.headers)
|
|
||||||
.await?;
|
|
||||||
Ok(DynamicResponse::Unary(result))
|
|
||||||
}
|
|
||||||
|
|
||||||
(true, true) => {
|
|
||||||
let input_stream =
|
|
||||||
json_array_to_stream(request.body).map_err(DynamicCallError::InvalidInput)?;
|
|
||||||
match self
|
|
||||||
.grpc_client
|
|
||||||
.bidirectional_streaming(method, input_stream, request.headers)
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
Ok(stream) => Ok(DynamicResponse::Streaming(Ok(stream.collect().await))),
|
|
||||||
Err(status) => Ok(DynamicResponse::Streaming(Err(status))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper to convert a JSON Array into a Stream of JSON Values.
|
|
||||||
/// Required for Client and Bidirectional streaming.
|
|
||||||
fn json_array_to_stream(
|
|
||||||
json: serde_json::Value,
|
|
||||||
) -> Result<impl Stream<Item = serde_json::Value> + Send + 'static, String> {
|
|
||||||
match json {
|
|
||||||
serde_json::Value::Array(items) => Ok(tokio_stream::iter(items)),
|
|
||||||
_ => Err("Client streaming requires a JSON Array body".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
178
granc-core/src/client/with_file_descriptor.rs
Normal file
178
granc-core/src/client/with_file_descriptor.rs
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
//! # Client State: File Descriptor
|
||||||
|
//!
|
||||||
|
//! This module defines the `GrancClient` behavior when it is using a local, in-memory
|
||||||
|
//! `DescriptorPool` (loaded from a file) to resolve schemas.
|
||||||
|
//!
|
||||||
|
//! In this state, the client does **not** use server reflection for schema lookup.
|
||||||
|
use super::WithFileDescriptor;
|
||||||
|
use super::{Descriptor, DynamicRequest, DynamicResponse, GrancClient};
|
||||||
|
use crate::{
|
||||||
|
BoxError,
|
||||||
|
grpc::client::{GrpcClient, GrpcRequestError},
|
||||||
|
};
|
||||||
|
use futures_util::Stream;
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
use http_body::Body as HttpBody;
|
||||||
|
use prost_reflect::DescriptorPool;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum DynamicCallError {
|
||||||
|
#[error("Invalid input: '{0}'")]
|
||||||
|
InvalidInput(String),
|
||||||
|
|
||||||
|
#[error("Service '{0}' not found")]
|
||||||
|
ServiceNotFound(String),
|
||||||
|
|
||||||
|
#[error("Method '{0}' not found")]
|
||||||
|
MethodNotFound(String),
|
||||||
|
|
||||||
|
#[error("gRPC client request error: '{0}'")]
|
||||||
|
GrpcRequestError(#[from] GrpcRequestError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> GrancClient<WithFileDescriptor<S>>
|
||||||
|
where
|
||||||
|
S: Clone,
|
||||||
|
{
|
||||||
|
pub(crate) fn new(grpc_client: GrpcClient<S>, pool: DescriptorPool) -> Self {
|
||||||
|
Self {
|
||||||
|
state: WithFileDescriptor { grpc_client, pool },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> GrancClient<WithFileDescriptor<S>>
|
||||||
|
where
|
||||||
|
S: tonic::client::GrpcService<tonic::body::Body> + Clone,
|
||||||
|
S::ResponseBody: HttpBody<Data = tonic::codegen::Bytes> + Send + 'static,
|
||||||
|
<S::ResponseBody as HttpBody>::Error: Into<BoxError> + Send,
|
||||||
|
{
|
||||||
|
/// Lists all services defined in the loaded `DescriptorPool`.
|
||||||
|
///
|
||||||
|
/// Unlike the reflection client, this is a synchronous operation that returns
|
||||||
|
/// immediately from memory.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// A list of fully qualified service names (e.g. `helloworld.Greeter`).
|
||||||
|
pub fn list_services(&mut self) -> Vec<String> {
|
||||||
|
self.state
|
||||||
|
.pool
|
||||||
|
.services()
|
||||||
|
.map(|s| s.full_name().to_string())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Looks up a specific symbol in the loaded `DescriptorPool`.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `symbol` - The fully qualified name of the symbol (Service, Message, or Enum).
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Some(Descriptor)` - The resolved descriptor if found.
|
||||||
|
/// * `None` - If the symbol does not exist in the pool.
|
||||||
|
pub fn get_descriptor_by_symbol(&mut self, symbol: &str) -> Option<Descriptor> {
|
||||||
|
let pool = &self.state.pool;
|
||||||
|
|
||||||
|
if let Some(descriptor) = pool.get_service_by_name(symbol) {
|
||||||
|
return Some(Descriptor::ServiceDescriptor(descriptor));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(descriptor) = pool.get_message_by_name(symbol) {
|
||||||
|
return Some(Descriptor::MessageDescriptor(descriptor));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(descriptor) = pool.get_enum_by_name(symbol) {
|
||||||
|
return Some(Descriptor::EnumDescriptor(descriptor));
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes a dynamic gRPC request using the loaded `DescriptorPool`.
|
||||||
|
///
|
||||||
|
/// It looks up the service and method definitions in the local pool, validates the JSON, and sends the request.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `request` - The [`DynamicRequest`] containing the method to call and the JSON body.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(DynamicResponse)` - The result of the gRPC call.
|
||||||
|
/// * `Err(DynamicCallError)` - If the service/method is not in the pool, the JSON is invalid, or the call fails.
|
||||||
|
pub async fn dynamic(
|
||||||
|
&mut self,
|
||||||
|
request: DynamicRequest,
|
||||||
|
) -> Result<DynamicResponse, DynamicCallError> {
|
||||||
|
let method = self
|
||||||
|
.state
|
||||||
|
.pool
|
||||||
|
.get_service_by_name(&request.service)
|
||||||
|
.ok_or_else(|| DynamicCallError::ServiceNotFound(request.service))?
|
||||||
|
.methods()
|
||||||
|
.find(|m| m.name() == request.method)
|
||||||
|
.ok_or_else(|| DynamicCallError::MethodNotFound(request.method))?;
|
||||||
|
|
||||||
|
match (method.is_client_streaming(), method.is_server_streaming()) {
|
||||||
|
(false, false) => {
|
||||||
|
let result = self
|
||||||
|
.state
|
||||||
|
.grpc_client
|
||||||
|
.unary(method, request.body, request.headers)
|
||||||
|
.await?;
|
||||||
|
Ok(DynamicResponse::Unary(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
(false, true) => {
|
||||||
|
match self
|
||||||
|
.state
|
||||||
|
.grpc_client
|
||||||
|
.server_streaming(method, request.body, request.headers)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Ok(stream) => Ok(DynamicResponse::Streaming(Ok(stream.collect().await))),
|
||||||
|
Err(status) => Ok(DynamicResponse::Streaming(Err(status))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(true, false) => {
|
||||||
|
let input_stream =
|
||||||
|
json_array_to_stream(request.body).map_err(DynamicCallError::InvalidInput)?;
|
||||||
|
let result = self
|
||||||
|
.state
|
||||||
|
.grpc_client
|
||||||
|
.client_streaming(method, input_stream, request.headers)
|
||||||
|
.await?;
|
||||||
|
Ok(DynamicResponse::Unary(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
(true, true) => {
|
||||||
|
let input_stream =
|
||||||
|
json_array_to_stream(request.body).map_err(DynamicCallError::InvalidInput)?;
|
||||||
|
match self
|
||||||
|
.state
|
||||||
|
.grpc_client
|
||||||
|
.bidirectional_streaming(method, input_stream, request.headers)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Ok(stream) => Ok(DynamicResponse::Streaming(Ok(stream.collect().await))),
|
||||||
|
Err(status) => Ok(DynamicResponse::Streaming(Err(status))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to convert a JSON Array into a Stream of JSON Values.
|
||||||
|
/// Required for Client and Bidirectional streaming.
|
||||||
|
fn json_array_to_stream(
|
||||||
|
json: serde_json::Value,
|
||||||
|
) -> Result<impl Stream<Item = serde_json::Value> + Send + 'static, String> {
|
||||||
|
match json {
|
||||||
|
serde_json::Value::Array(items) => Ok(tokio_stream::iter(items)),
|
||||||
|
_ => Err("Client streaming requires a JSON Array body".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
206
granc-core/src/client/with_server_reflection.rs
Normal file
206
granc-core/src/client/with_server_reflection.rs
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
//! # Client State: Server Reflection
|
||||||
|
//!
|
||||||
|
//! This module defines the `GrancClient` behavior when it is using the server's reflection service
|
||||||
|
//! to resolve schemas.
|
||||||
|
use super::{
|
||||||
|
Descriptor, DynamicRequest, DynamicResponse, GrancClient, WithFileDescriptor,
|
||||||
|
WithServerReflection,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
BoxError,
|
||||||
|
grpc::client::GrpcClient,
|
||||||
|
reflection::client::{ReflectionClient, ReflectionResolveError},
|
||||||
|
};
|
||||||
|
use http_body::Body as HttpBody;
|
||||||
|
use prost_reflect::DescriptorError;
|
||||||
|
use prost_reflect::DescriptorPool;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use tonic::{
|
||||||
|
Code,
|
||||||
|
transport::{Channel, Endpoint},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ClientConnectError {
|
||||||
|
#[error("Invalid URL '{0}': {1}")]
|
||||||
|
InvalidUrl(String, #[source] tonic::transport::Error),
|
||||||
|
#[error("Failed to connect to '{0}': {1}")]
|
||||||
|
ConnectionFailed(String, #[source] tonic::transport::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum DynamicCallError {
|
||||||
|
#[error("Reflection resolution failed: '{0}'")]
|
||||||
|
ReflectionResolve(#[from] ReflectionResolveError),
|
||||||
|
|
||||||
|
#[error("Failed to decode file descriptor set: '{0}'")]
|
||||||
|
DescriptorError(#[from] DescriptorError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
DynamicCallError(#[from] super::with_file_descriptor::DynamicCallError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum GetDescriptorError {
|
||||||
|
#[error("Reflection resolution failed: '{0}'")]
|
||||||
|
ReflectionResolve(#[from] ReflectionResolveError),
|
||||||
|
#[error("Failed to decode file descriptor set: '{0}'")]
|
||||||
|
DescriptorError(#[from] DescriptorError),
|
||||||
|
#[error("Descriptor at path '{0}' not found")]
|
||||||
|
NotFound(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GrancClient<WithServerReflection<Channel>> {
|
||||||
|
/// Connects to a gRPC server at the specified address.
|
||||||
|
///
|
||||||
|
/// This initializes the client in the **Reflection** state. It establishes a TCP connection
|
||||||
|
/// but does not yet perform any reflection calls.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `addr` - The URI of the server (e.g., `http://localhost:50051`).
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(GrancClient<WithServerReflection>)` - The connected client ready to use reflection.
|
||||||
|
/// * `Err(ClientConnectError)` - If the URL is invalid or the connection cannot be established.
|
||||||
|
pub async fn connect(addr: &str) -> Result<Self, ClientConnectError> {
|
||||||
|
let endpoint = Endpoint::new(addr.to_string())
|
||||||
|
.map_err(|e| ClientConnectError::InvalidUrl(addr.to_string(), e))?;
|
||||||
|
|
||||||
|
let channel = endpoint
|
||||||
|
.connect()
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientConnectError::ConnectionFailed(addr.to_string(), e))?;
|
||||||
|
|
||||||
|
Ok(Self::from_service(channel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> GrancClient<WithServerReflection<S>>
|
||||||
|
where
|
||||||
|
S: tonic::client::GrpcService<tonic::body::Body> + Clone,
|
||||||
|
S::ResponseBody: HttpBody<Data = tonic::codegen::Bytes> + Send + 'static,
|
||||||
|
<S::ResponseBody as HttpBody>::Error: Into<BoxError> + Send,
|
||||||
|
{
|
||||||
|
/// Creates a new `GrancClient` wrapping an existing Tonic service (e.g., a `Channel` or `InterceptedService`).
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `service` - The generic gRPC service implementation to use for transport.
|
||||||
|
pub fn from_service(service: S) -> Self {
|
||||||
|
let reflection_client = ReflectionClient::new(service.clone());
|
||||||
|
let grpc_client = GrpcClient::new(service);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
state: WithServerReflection {
|
||||||
|
reflection_client,
|
||||||
|
grpc_client,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transitions the client to the **File Descriptor** state.
|
||||||
|
///
|
||||||
|
/// This method consumes the current client and returns a new client that uses the provided
|
||||||
|
/// binary `FileDescriptorSet` for all schema lookups, disabling server reflection.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `file_descriptor` - A vector of bytes containing the encoded `FileDescriptorSet` (protobuf binary format).
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(GrancClient<WithFileDescriptor>)` - The new client state.
|
||||||
|
/// * `Err(DescriptorError)` - If the provided bytes could not be decoded into a valid `DescriptorPool`.
|
||||||
|
pub fn with_file_descriptor(
|
||||||
|
self,
|
||||||
|
file_descriptor: Vec<u8>,
|
||||||
|
) -> Result<GrancClient<WithFileDescriptor<S>>, DescriptorError> {
|
||||||
|
let pool = DescriptorPool::decode(file_descriptor.as_slice())?;
|
||||||
|
Ok(GrancClient::new(self.state.grpc_client, pool))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lists all services exposed by the server by querying the reflection endpoint.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(Vec<String>)` - A list of fully qualified service names (e.g. `helloworld.Greeter`).
|
||||||
|
/// * `Err(ReflectionResolveError)` - If the reflection call fails or the server doesn't support reflection.
|
||||||
|
pub async fn list_services(&mut self) -> Result<Vec<String>, ReflectionResolveError> {
|
||||||
|
self.state.reflection_client.list_services().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves and fetches the descriptor for a specific symbol using reflection.
|
||||||
|
///
|
||||||
|
/// This will recursively fetch the file defining the symbol and all its dependencies
|
||||||
|
/// from the server.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `symbol` - The fully qualified name of the symbol (Service, Message, or Enum).
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(Descriptor)` - The resolved descriptor wrapper.
|
||||||
|
/// * `Err(GetDescriptorError)` - If the symbol is not found or reflection fails.
|
||||||
|
pub async fn get_descriptor_by_symbol(
|
||||||
|
&mut self,
|
||||||
|
symbol: &str,
|
||||||
|
) -> Result<Descriptor, GetDescriptorError> {
|
||||||
|
let fd_set = self
|
||||||
|
.state
|
||||||
|
.reflection_client
|
||||||
|
.file_descriptor_set_by_symbol(symbol)
|
||||||
|
.await
|
||||||
|
.map_err(|err| match err {
|
||||||
|
ReflectionResolveError::ServerStreamFailure(status)
|
||||||
|
if status.code() == Code::NotFound =>
|
||||||
|
{
|
||||||
|
GetDescriptorError::NotFound(symbol.to_string())
|
||||||
|
}
|
||||||
|
err => GetDescriptorError::ReflectionResolve(err),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let pool = DescriptorPool::from_file_descriptor_set(fd_set)?;
|
||||||
|
|
||||||
|
let mut client =
|
||||||
|
GrancClient::<WithFileDescriptor<S>>::new(self.state.grpc_client.clone(), pool);
|
||||||
|
|
||||||
|
client
|
||||||
|
.get_descriptor_by_symbol(symbol)
|
||||||
|
.ok_or_else(|| GetDescriptorError::NotFound(symbol.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes a dynamic gRPC request using reflection.
|
||||||
|
///
|
||||||
|
/// 1. It fetches the schema for the requested `service` via reflection.
|
||||||
|
/// 2. It builds a temporary `WithFileDescriptor` client using that schema.
|
||||||
|
/// 3. It delegates the call to that client.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `request` - The [`DynamicRequest`] containing the method to call and the JSON body.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(DynamicResponse)` - The result of the gRPC call (Unary or Streaming).
|
||||||
|
/// * `Err(DynamicCallError)` - If schema resolution, validation, or the network call fails.
|
||||||
|
pub async fn dynamic(
|
||||||
|
&mut self,
|
||||||
|
request: DynamicRequest,
|
||||||
|
) -> Result<DynamicResponse, DynamicCallError> {
|
||||||
|
let fd_set = self
|
||||||
|
.state
|
||||||
|
.reflection_client
|
||||||
|
.file_descriptor_set_by_symbol(&request.service)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let pool = DescriptorPool::from_file_descriptor_set(fd_set)?;
|
||||||
|
|
||||||
|
let mut client =
|
||||||
|
GrancClient::<WithFileDescriptor<S>>::new(self.state.grpc_client.clone(), pool);
|
||||||
|
|
||||||
|
Ok(client.dynamic(request).await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -48,6 +48,7 @@ pub enum GrpcRequestError {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A generic client for the gRPC Server Reflection Protocol.
|
/// A generic client for the gRPC Server Reflection Protocol.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct GrpcClient<S = Channel> {
|
pub struct GrpcClient<S = Channel> {
|
||||||
client: tonic::client::Grpc<S>,
|
client: tonic::client::Grpc<S>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ pub enum ReflectionResolveError {
|
||||||
const EMPTY_HOST: &str = "";
|
const EMPTY_HOST: &str = "";
|
||||||
|
|
||||||
/// A client for interacting with the gRPC Server Reflection Service.
|
/// A client for interacting with the gRPC Server Reflection Service.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct ReflectionClient<T = Channel> {
|
pub struct ReflectionClient<T = Channel> {
|
||||||
client: ServerReflectionClient<T>,
|
client: ServerReflectionClient<T>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
use echo_service::EchoService;
|
|
||||||
use echo_service::pb::{EchoRequest, EchoResponse};
|
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
|
||||||
use tonic::{Request, Response, Status, Streaming};
|
|
||||||
|
|
||||||
// A minimal service that satisfies the EchoService trait.
|
|
||||||
// We don't need real logic here, just enough to compile.
|
|
||||||
pub struct DummyEchoService;
|
|
||||||
|
|
||||||
#[tonic::async_trait]
|
|
||||||
impl EchoService for DummyEchoService {
|
|
||||||
type ServerStreamingEchoStream = ReceiverStream<Result<EchoResponse, Status>>;
|
|
||||||
type BidirectionalEchoStream = ReceiverStream<Result<EchoResponse, Status>>;
|
|
||||||
|
|
||||||
async fn unary_echo(
|
|
||||||
&self,
|
|
||||||
_req: Request<EchoRequest>,
|
|
||||||
) -> Result<Response<EchoResponse>, Status> {
|
|
||||||
unimplemented!("This will never be used")
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn server_streaming_echo(
|
|
||||||
&self,
|
|
||||||
_req: Request<EchoRequest>,
|
|
||||||
) -> Result<Response<Self::ServerStreamingEchoStream>, Status> {
|
|
||||||
unimplemented!("This will never be used")
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn client_streaming_echo(
|
|
||||||
&self,
|
|
||||||
_req: Request<Streaming<EchoRequest>>,
|
|
||||||
) -> Result<Response<EchoResponse>, Status> {
|
|
||||||
unimplemented!("This will never be used")
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn bidirectional_echo(
|
|
||||||
&self,
|
|
||||||
_req: Request<Streaming<EchoRequest>>,
|
|
||||||
) -> Result<Response<Self::BidirectionalEchoStream>, Status> {
|
|
||||||
unimplemented!("This will never be used")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
use echo_service::EchoService;
|
use echo_service::EchoService;
|
||||||
use echo_service::pb::{EchoRequest, EchoResponse};
|
use echo_service::pb::{EchoRequest, EchoResponse};
|
||||||
use futures_util::Stream;
|
use futures_util::Stream;
|
||||||
|
use futures_util::StreamExt;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio_stream::{StreamExt, wrappers::ReceiverStream};
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
use tonic::{Request, Response, Status, Streaming};
|
use tonic::{Request, Response, Status, Streaming};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct EchoServiceImpl;
|
pub struct EchoServiceImpl;
|
||||||
|
|
||||||
#[tonic::async_trait]
|
#[tonic::async_trait]
|
||||||
|
|
|
||||||
186
granc-core/tests/granc_client_file_descriptor_test.rs
Normal file
186
granc-core/tests/granc_client_file_descriptor_test.rs
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
use echo_service::{EchoServiceServer, FILE_DESCRIPTOR_SET};
|
||||||
|
use echo_service_impl::EchoServiceImpl;
|
||||||
|
use granc_core::client::{
|
||||||
|
Descriptor, DynamicRequest, DynamicResponse, GrancClient, WithFileDescriptor,
|
||||||
|
with_file_descriptor,
|
||||||
|
};
|
||||||
|
use tonic::Code;
|
||||||
|
|
||||||
|
mod echo_service_impl;
|
||||||
|
|
||||||
|
fn setup_client() -> GrancClient<WithFileDescriptor<EchoServiceServer<EchoServiceImpl>>> {
|
||||||
|
let service = EchoServiceServer::new(EchoServiceImpl);
|
||||||
|
let client_reflection = GrancClient::from_service(service);
|
||||||
|
|
||||||
|
client_reflection
|
||||||
|
.with_file_descriptor(FILE_DESCRIPTOR_SET.to_vec())
|
||||||
|
.expect("Failed to load file descriptor set")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_list_services() {
|
||||||
|
let mut client = setup_client();
|
||||||
|
|
||||||
|
let services = client.list_services();
|
||||||
|
|
||||||
|
assert_eq!(services.as_slice(), ["echo.EchoService"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_describe_descriptors() {
|
||||||
|
let mut client = setup_client();
|
||||||
|
|
||||||
|
// Describe Service
|
||||||
|
let desc = client
|
||||||
|
.get_descriptor_by_symbol("echo.EchoService")
|
||||||
|
.expect("Service not found");
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
desc,
|
||||||
|
Descriptor::ServiceDescriptor(s) if s.name() == "EchoService"
|
||||||
|
));
|
||||||
|
|
||||||
|
// Describe Message
|
||||||
|
let desc = client
|
||||||
|
.get_descriptor_by_symbol("echo.EchoRequest")
|
||||||
|
.expect("Message not found");
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
desc,
|
||||||
|
Descriptor::MessageDescriptor(m) if m.name() == "EchoRequest"
|
||||||
|
));
|
||||||
|
|
||||||
|
// Error Case: Returns None
|
||||||
|
let desc = client.get_descriptor_by_symbol("echo.Ghost");
|
||||||
|
|
||||||
|
assert!(desc.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_dynamic_calls() {
|
||||||
|
let mut client = setup_client();
|
||||||
|
|
||||||
|
// Unary Call
|
||||||
|
let req = DynamicRequest {
|
||||||
|
service: "echo.EchoService".to_string(),
|
||||||
|
method: "UnaryEcho".to_string(),
|
||||||
|
body: serde_json::json!({ "message": "hello" }),
|
||||||
|
headers: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = client.dynamic(req).await.unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(res, DynamicResponse::Unary(Ok(val)) if val["message"] == "hello"));
|
||||||
|
|
||||||
|
// Server Streaming
|
||||||
|
let req = DynamicRequest {
|
||||||
|
service: "echo.EchoService".to_string(),
|
||||||
|
method: "ServerStreamingEcho".to_string(),
|
||||||
|
body: serde_json::json!({ "message": "stream" }),
|
||||||
|
headers: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = client.dynamic(req).await.unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(res, DynamicResponse::Streaming(Ok(stream)) if stream.len() == 3));
|
||||||
|
|
||||||
|
// Client Streaming
|
||||||
|
let req = DynamicRequest {
|
||||||
|
service: "echo.EchoService".to_string(),
|
||||||
|
method: "ClientStreamingEcho".to_string(),
|
||||||
|
body: serde_json::json!([
|
||||||
|
{ "message": "A" },
|
||||||
|
{ "message": "B" }
|
||||||
|
]),
|
||||||
|
headers: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = client.dynamic(req).await.unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(res, DynamicResponse::Unary(Ok(val)) if val["message"] == "AB"));
|
||||||
|
|
||||||
|
// Bidirectional Streaming
|
||||||
|
let req = DynamicRequest {
|
||||||
|
service: "echo.EchoService".to_string(),
|
||||||
|
method: "BidirectionalEcho".to_string(),
|
||||||
|
body: serde_json::json!([
|
||||||
|
{ "message": "Ping" }
|
||||||
|
]),
|
||||||
|
headers: vec![],
|
||||||
|
};
|
||||||
|
let res = client.dynamic(req).await.unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(res,
|
||||||
|
DynamicResponse::Streaming(Ok(stream))
|
||||||
|
if stream.len() == 1
|
||||||
|
&& stream[0].as_ref().unwrap()["message"] == "echo: Ping"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_error_cases() {
|
||||||
|
let mut client = setup_client();
|
||||||
|
|
||||||
|
// Service Not Found
|
||||||
|
let req = DynamicRequest {
|
||||||
|
service: "echo.GhostService".to_string(),
|
||||||
|
method: "UnaryEcho".to_string(),
|
||||||
|
body: serde_json::json!({}),
|
||||||
|
headers: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = client.dynamic(req).await;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
result,
|
||||||
|
Err(with_file_descriptor::DynamicCallError::ServiceNotFound(name)) if name == "echo.GhostService"
|
||||||
|
));
|
||||||
|
|
||||||
|
// Method Not Found
|
||||||
|
let req = DynamicRequest {
|
||||||
|
service: "echo.EchoService".to_string(),
|
||||||
|
method: "GhostMethod".to_string(),
|
||||||
|
body: serde_json::json!({}),
|
||||||
|
headers: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = client.dynamic(req).await;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
result,
|
||||||
|
Err(with_file_descriptor::DynamicCallError::MethodNotFound(name)) if name == "GhostMethod"
|
||||||
|
));
|
||||||
|
|
||||||
|
// Invalid JSON Structure (Streaming requires Array)
|
||||||
|
let req = DynamicRequest {
|
||||||
|
service: "echo.EchoService".to_string(),
|
||||||
|
method: "ClientStreamingEcho".to_string(),
|
||||||
|
body: serde_json::json!({ "message": "I should be an array" }),
|
||||||
|
headers: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = client.dynamic(req).await;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
result,
|
||||||
|
Err(with_file_descriptor::DynamicCallError::InvalidInput(_))
|
||||||
|
));
|
||||||
|
|
||||||
|
// Schema Mismatch (Unary)
|
||||||
|
// Field mismatch causes encoding error -> Status::InvalidArgument
|
||||||
|
let req = DynamicRequest {
|
||||||
|
service: "echo.EchoService".to_string(),
|
||||||
|
method: "UnaryEcho".to_string(),
|
||||||
|
body: serde_json::json!({ "unknown_field": 123 }),
|
||||||
|
headers: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = client.dynamic(req).await;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
result,
|
||||||
|
Ok(DynamicResponse::Unary(Err(status)))
|
||||||
|
if status.code() == Code::Internal
|
||||||
|
&& status.message().contains("JSON structure does not match")
|
||||||
|
));
|
||||||
|
}
|
||||||
214
granc-core/tests/granc_client_reflection_test.rs
Normal file
214
granc-core/tests/granc_client_reflection_test.rs
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
use echo_service::{EchoServiceServer, FILE_DESCRIPTOR_SET};
|
||||||
|
use echo_service_impl::EchoServiceImpl;
|
||||||
|
use granc_core::client::{
|
||||||
|
Descriptor, DynamicRequest, DynamicResponse, GrancClient, WithServerReflection,
|
||||||
|
with_file_descriptor, with_server_reflection,
|
||||||
|
};
|
||||||
|
use granc_core::reflection::client::ReflectionResolveError;
|
||||||
|
use tonic::Code;
|
||||||
|
use tonic::service::Routes;
|
||||||
|
|
||||||
|
mod echo_service_impl;
|
||||||
|
|
||||||
|
async fn setup_client() -> GrancClient<WithServerReflection<Routes>> {
|
||||||
|
let reflection_service = tonic_reflection::server::Builder::configure()
|
||||||
|
.register_encoded_file_descriptor_set(FILE_DESCRIPTOR_SET)
|
||||||
|
.build_v1()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let echo_service = EchoServiceServer::new(EchoServiceImpl);
|
||||||
|
|
||||||
|
let service = Routes::new(reflection_service).add_service(echo_service);
|
||||||
|
|
||||||
|
GrancClient::from_service(service)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_reflection_list_services() {
|
||||||
|
let mut client = setup_client().await;
|
||||||
|
|
||||||
|
let mut services = client.list_services().await.unwrap();
|
||||||
|
|
||||||
|
services.sort();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
services.as_slice(),
|
||||||
|
["echo.EchoService", "grpc.reflection.v1.ServerReflection"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_reflection_describe_descriptors() {
|
||||||
|
let mut client = setup_client().await;
|
||||||
|
|
||||||
|
let desc = client
|
||||||
|
.get_descriptor_by_symbol("echo.EchoService")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
desc,
|
||||||
|
Descriptor::ServiceDescriptor(s)
|
||||||
|
if s.name() == "EchoService"
|
||||||
|
&& s.methods().any(|m| m.name() == "UnaryEcho")
|
||||||
|
));
|
||||||
|
|
||||||
|
let desc = client
|
||||||
|
.get_descriptor_by_symbol("echo.EchoRequest")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
desc,
|
||||||
|
Descriptor::MessageDescriptor(m)
|
||||||
|
if m.name() == "EchoRequest"
|
||||||
|
&& m.fields().any(|f| f.name() == "message")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_reflection_describe_error() {
|
||||||
|
let mut client = setup_client().await;
|
||||||
|
|
||||||
|
let result = client.get_descriptor_by_symbol("echo.Ghost").await;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
result,
|
||||||
|
Err(with_server_reflection::GetDescriptorError::NotFound(name)) if name == "echo.Ghost"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_reflection_dynamic_calls() {
|
||||||
|
let mut client = setup_client().await;
|
||||||
|
|
||||||
|
// Unary Call
|
||||||
|
let req = DynamicRequest {
|
||||||
|
service: "echo.EchoService".to_string(),
|
||||||
|
method: "UnaryEcho".to_string(),
|
||||||
|
body: serde_json::json!({ "message": "hello" }),
|
||||||
|
headers: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = client.dynamic(req).await.unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(res, DynamicResponse::Unary(Ok(val)) if val["message"] == "hello"));
|
||||||
|
|
||||||
|
// Server Streaming
|
||||||
|
let req = DynamicRequest {
|
||||||
|
service: "echo.EchoService".to_string(),
|
||||||
|
method: "ServerStreamingEcho".to_string(),
|
||||||
|
body: serde_json::json!({ "message": "stream" }),
|
||||||
|
headers: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = client.dynamic(req).await.unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(res, DynamicResponse::Streaming(Ok(stream)) if stream.len() == 3));
|
||||||
|
|
||||||
|
// Client Streaming
|
||||||
|
let req = DynamicRequest {
|
||||||
|
service: "echo.EchoService".to_string(),
|
||||||
|
method: "ClientStreamingEcho".to_string(),
|
||||||
|
body: serde_json::json!([
|
||||||
|
{ "message": "A" },
|
||||||
|
{ "message": "B" }
|
||||||
|
]),
|
||||||
|
headers: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = client.dynamic(req).await.unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(res, DynamicResponse::Unary(Ok(val)) if val["message"] == "AB"));
|
||||||
|
|
||||||
|
// Bidirectional Streaming
|
||||||
|
let req = DynamicRequest {
|
||||||
|
service: "echo.EchoService".to_string(),
|
||||||
|
method: "BidirectionalEcho".to_string(),
|
||||||
|
body: serde_json::json!([
|
||||||
|
{ "message": "Ping" }
|
||||||
|
]),
|
||||||
|
headers: vec![],
|
||||||
|
};
|
||||||
|
let res = client.dynamic(req).await.unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(res,
|
||||||
|
DynamicResponse::Streaming(Ok(stream))
|
||||||
|
if stream.len() == 1
|
||||||
|
&& stream[0].as_ref().unwrap()["message"] == "echo: Ping"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_reflection_dynamic_error_cases() {
|
||||||
|
let mut client = setup_client().await;
|
||||||
|
|
||||||
|
// Invalid Service Name
|
||||||
|
let req = DynamicRequest {
|
||||||
|
service: "echo.GhostService".to_string(),
|
||||||
|
method: "UnaryEcho".to_string(),
|
||||||
|
body: serde_json::json!({}),
|
||||||
|
headers: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = client.dynamic(req).await;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
result,
|
||||||
|
Err(with_server_reflection::DynamicCallError::ReflectionResolve(
|
||||||
|
ReflectionResolveError::ServerStreamFailure(status)
|
||||||
|
)) if status.code() == Code::NotFound
|
||||||
|
));
|
||||||
|
|
||||||
|
// Invalid Method Name
|
||||||
|
let req = DynamicRequest {
|
||||||
|
service: "echo.EchoService".to_string(),
|
||||||
|
method: "GhostMethod".to_string(),
|
||||||
|
body: serde_json::json!({}),
|
||||||
|
headers: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = client.dynamic(req).await;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
result,
|
||||||
|
Err(with_server_reflection::DynamicCallError::DynamicCallError(
|
||||||
|
with_file_descriptor::DynamicCallError::MethodNotFound(name)
|
||||||
|
)) if name == "GhostMethod"
|
||||||
|
));
|
||||||
|
|
||||||
|
// Invalid JSON Structure (Streaming requires Array, Object provided)
|
||||||
|
// This triggers `DynamicCallError::InvalidInput` before the request is sent.
|
||||||
|
let req = DynamicRequest {
|
||||||
|
service: "echo.EchoService".to_string(),
|
||||||
|
method: "ClientStreamingEcho".to_string(),
|
||||||
|
body: serde_json::json!({ "message": "I should be an array" }),
|
||||||
|
headers: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = client.dynamic(req).await;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
result,
|
||||||
|
Err(with_server_reflection::DynamicCallError::DynamicCallError(
|
||||||
|
with_file_descriptor::DynamicCallError::InvalidInput(_)
|
||||||
|
))
|
||||||
|
));
|
||||||
|
|
||||||
|
// Schema Mismatch (Unary)
|
||||||
|
// Passing a field that doesn't exist. This fails at encoding time inside the Codec.
|
||||||
|
// Tonic wraps encoding errors as Code::Internal.
|
||||||
|
let req = DynamicRequest {
|
||||||
|
service: "echo.EchoService".to_string(),
|
||||||
|
method: "UnaryEcho".to_string(),
|
||||||
|
body: serde_json::json!({ "non_existent_field": "oops" }),
|
||||||
|
headers: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = client.dynamic(req).await;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
result,
|
||||||
|
Ok(DynamicResponse::Unary(Err(status))) if status.code() == Code::Internal
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
@ -1,213 +0,0 @@
|
||||||
use echo_service::EchoServiceServer;
|
|
||||||
use echo_service::FILE_DESCRIPTOR_SET;
|
|
||||||
use echo_service_impl::EchoServiceImpl;
|
|
||||||
use granc_core::client::{DynamicRequest, DynamicResponse, GrancClient};
|
|
||||||
use tonic_reflection::server::v1::ServerReflectionServer;
|
|
||||||
|
|
||||||
mod echo_service_impl;
|
|
||||||
|
|
||||||
fn reflection_service()
|
|
||||||
-> ServerReflectionServer<impl tonic_reflection::server::v1::ServerReflection> {
|
|
||||||
tonic_reflection::server::Builder::configure()
|
|
||||||
.register_encoded_file_descriptor_set(FILE_DESCRIPTOR_SET)
|
|
||||||
.build_v1()
|
|
||||||
.expect("Failed to setup Reflection Service")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_unary() {
|
|
||||||
let payload = serde_json::json!({ "message": "hello" });
|
|
||||||
|
|
||||||
let request = DynamicRequest {
|
|
||||||
file_descriptor_set: Some(FILE_DESCRIPTOR_SET.to_vec()),
|
|
||||||
body: payload.clone(),
|
|
||||||
headers: vec![],
|
|
||||||
service: "echo.EchoService".to_string(),
|
|
||||||
method: "UnaryEcho".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut client = GrancClient::new(EchoServiceServer::new(EchoServiceImpl));
|
|
||||||
|
|
||||||
let res = client.dynamic(request).await.unwrap();
|
|
||||||
|
|
||||||
match res {
|
|
||||||
DynamicResponse::Unary(Ok(value)) => assert_eq!(value, payload),
|
|
||||||
DynamicResponse::Unary(Err(_)) => {
|
|
||||||
panic!("Received error status for valid unary request")
|
|
||||||
}
|
|
||||||
_ => panic!("Received stream response for unary request"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_server_streaming() {
|
|
||||||
let payload = serde_json::json!({ "message": "stream" });
|
|
||||||
|
|
||||||
let request = DynamicRequest {
|
|
||||||
file_descriptor_set: Some(FILE_DESCRIPTOR_SET.to_vec()),
|
|
||||||
body: payload.clone(),
|
|
||||||
headers: vec![],
|
|
||||||
service: "echo.EchoService".to_string(),
|
|
||||||
method: "ServerStreamingEcho".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut client = GrancClient::new(EchoServiceServer::new(EchoServiceImpl));
|
|
||||||
|
|
||||||
let res = client.dynamic(request).await.unwrap();
|
|
||||||
|
|
||||||
match res {
|
|
||||||
DynamicResponse::Streaming(Ok(elems)) => {
|
|
||||||
let results: Vec<_> = elems.into_iter().map(|r| r.unwrap()).collect();
|
|
||||||
|
|
||||||
assert_eq!(results.len(), 3);
|
|
||||||
assert_eq!(results[0]["message"], "stream - seq 0");
|
|
||||||
assert_eq!(results[1]["message"], "stream - seq 1");
|
|
||||||
assert_eq!(results[2]["message"], "stream - seq 2");
|
|
||||||
}
|
|
||||||
DynamicResponse::Streaming(Err(_)) => {
|
|
||||||
panic!("Received error status for valid server streaming request")
|
|
||||||
}
|
|
||||||
_ => panic!("Received unary response for server streaming request"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_client_streaming() {
|
|
||||||
let payload = serde_json::json!([
|
|
||||||
{ "message": "A" },
|
|
||||||
{ "message": "B" },
|
|
||||||
{ "message": "C" }
|
|
||||||
]);
|
|
||||||
|
|
||||||
let request = DynamicRequest {
|
|
||||||
file_descriptor_set: Some(FILE_DESCRIPTOR_SET.to_vec()),
|
|
||||||
body: payload.clone(),
|
|
||||||
headers: vec![],
|
|
||||||
service: "echo.EchoService".to_string(),
|
|
||||||
method: "ClientStreamingEcho".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut client = GrancClient::new(EchoServiceServer::new(EchoServiceImpl));
|
|
||||||
|
|
||||||
let res = client.dynamic(request).await.unwrap();
|
|
||||||
|
|
||||||
match res {
|
|
||||||
DynamicResponse::Unary(Ok(value)) => {
|
|
||||||
assert_eq!(value, serde_json::json!({"message": "ABC"}))
|
|
||||||
}
|
|
||||||
DynamicResponse::Unary(Err(_)) => {
|
|
||||||
panic!("Received error status for valid client stream request")
|
|
||||||
}
|
|
||||||
_ => panic!("Received stream response for client stream request"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_bidirectional_streaming() {
|
|
||||||
let payload = serde_json::json!([
|
|
||||||
{ "message": "Ping" },
|
|
||||||
{ "message": "Pong" }
|
|
||||||
]);
|
|
||||||
|
|
||||||
let request = DynamicRequest {
|
|
||||||
file_descriptor_set: Some(FILE_DESCRIPTOR_SET.to_vec()),
|
|
||||||
body: payload.clone(),
|
|
||||||
headers: vec![],
|
|
||||||
service: "echo.EchoService".to_string(),
|
|
||||||
method: "BidirectionalEcho".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut client = GrancClient::new(EchoServiceServer::new(EchoServiceImpl));
|
|
||||||
|
|
||||||
let res = client.dynamic(request).await.unwrap();
|
|
||||||
|
|
||||||
match res {
|
|
||||||
DynamicResponse::Streaming(Ok(elems)) => {
|
|
||||||
let results: Vec<_> = elems.into_iter().map(|r| r.unwrap()).collect();
|
|
||||||
|
|
||||||
assert_eq!(results.len(), 2);
|
|
||||||
assert_eq!(results[0]["message"], "echo: Ping");
|
|
||||||
assert_eq!(results[1]["message"], "echo: Pong");
|
|
||||||
}
|
|
||||||
DynamicResponse::Streaming(Err(_)) => {
|
|
||||||
panic!("Received error status for valid bidirectional streaming request")
|
|
||||||
}
|
|
||||||
_ => panic!("Received unary response for bidirectional streaming request"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_list_services_success() {
|
|
||||||
let mut client = GrancClient::new(reflection_service());
|
|
||||||
|
|
||||||
let services = client
|
|
||||||
.list_services()
|
|
||||||
.await
|
|
||||||
.expect("Failed to list services");
|
|
||||||
|
|
||||||
// We expect "echo.EchoService" because we registered it.
|
|
||||||
// The list usually also includes the reflection service itself ("grpc.reflection.v1.ServerReflection").
|
|
||||||
assert!(
|
|
||||||
services.contains(&"echo.EchoService".to_string()),
|
|
||||||
"Services list did not contain 'echo.EchoService'. Found: {:?}",
|
|
||||||
services
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_get_service_descriptor_success() {
|
|
||||||
let mut client = GrancClient::new(reflection_service());
|
|
||||||
|
|
||||||
let descriptor = client
|
|
||||||
.get_descriptor_by_symbol("echo.EchoService")
|
|
||||||
.await
|
|
||||||
.expect("Failed to get service descriptor");
|
|
||||||
|
|
||||||
let descriptor = descriptor.service_descriptor().unwrap();
|
|
||||||
|
|
||||||
assert_eq!(descriptor.name(), "EchoService");
|
|
||||||
assert_eq!(descriptor.full_name(), "echo.EchoService");
|
|
||||||
|
|
||||||
// Verify methods are present
|
|
||||||
let method_names: Vec<String> = descriptor.methods().map(|m| m.name().to_string()).collect();
|
|
||||||
assert!(method_names.contains(&"UnaryEcho".to_string()));
|
|
||||||
assert!(method_names.contains(&"ServerStreamingEcho".to_string()));
|
|
||||||
assert!(method_names.contains(&"ClientStreamingEcho".to_string()));
|
|
||||||
assert!(method_names.contains(&"BidirectionalEcho".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_get_message_descriptor_success() {
|
|
||||||
let mut client = GrancClient::new(reflection_service());
|
|
||||||
|
|
||||||
let desc = client
|
|
||||||
.get_descriptor_by_symbol("echo.EchoRequest")
|
|
||||||
.await
|
|
||||||
.expect("Failed to get message descriptor");
|
|
||||||
|
|
||||||
let desc = desc.message_descriptor().unwrap();
|
|
||||||
|
|
||||||
assert_eq!(desc.name(), "EchoRequest");
|
|
||||||
assert_eq!(desc.full_name(), "echo.EchoRequest");
|
|
||||||
|
|
||||||
// Verify fields
|
|
||||||
let fields: Vec<String> = desc.fields().map(|f| f.name().to_string()).collect();
|
|
||||||
assert!(fields.contains(&"message".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_get_descriptor_not_found() {
|
|
||||||
let mut client = GrancClient::new(reflection_service());
|
|
||||||
|
|
||||||
// "echo.GhostService" does not exist in the registered descriptors.
|
|
||||||
// The reflection client should fail to find the symbol, resulting in a ResolutionError.
|
|
||||||
let err = client
|
|
||||||
.get_descriptor_by_symbol("echo.GhostService")
|
|
||||||
.await
|
|
||||||
.unwrap_err();
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
err,
|
|
||||||
granc_core::client::GetDescriptorError::NotFound(_)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
use dummy_echo_service_impl::DummyEchoService;
|
|
||||||
use echo_service::{EchoServiceServer, FILE_DESCRIPTOR_SET};
|
use echo_service::{EchoServiceServer, FILE_DESCRIPTOR_SET};
|
||||||
|
use echo_service_impl::EchoServiceImpl;
|
||||||
use granc_core::reflection::client::{ReflectionClient, ReflectionResolveError};
|
use granc_core::reflection::client::{ReflectionClient, ReflectionResolveError};
|
||||||
use prost_reflect::DescriptorPool;
|
use prost_reflect::DescriptorPool;
|
||||||
use tonic::Code;
|
use tonic::Code;
|
||||||
use tonic_reflection::server::v1::ServerReflectionServer;
|
use tonic_reflection::server::v1::ServerReflectionServer;
|
||||||
|
|
||||||
mod dummy_echo_service_impl;
|
mod echo_service_impl;
|
||||||
|
|
||||||
fn setup_reflection_client()
|
fn setup_reflection_client()
|
||||||
-> ReflectionClient<ServerReflectionServer<impl tonic_reflection::server::v1::ServerReflection>> {
|
-> ReflectionClient<ServerReflectionServer<impl tonic_reflection::server::v1::ServerReflection>> {
|
||||||
|
|
@ -115,7 +115,7 @@ async fn test_reflection_service_not_found_error() {
|
||||||
async fn test_server_does_not_support_reflection() {
|
async fn test_server_does_not_support_reflection() {
|
||||||
// Create a server that ONLY hosts the EchoService.
|
// Create a server that ONLY hosts the EchoService.
|
||||||
// This server does NOT have the Reflection service registered.
|
// This server does NOT have the Reflection service registered.
|
||||||
let server = EchoServiceServer::new(DummyEchoService);
|
let server = EchoServiceServer::new(EchoServiceImpl);
|
||||||
let mut client = ReflectionClient::new(server);
|
let mut client = ReflectionClient::new(server);
|
||||||
|
|
||||||
// The client will attempt to call `/grpc.reflection.v1.ServerReflection/ServerReflectionInfo` on this service.
|
// The client will attempt to call `/grpc.reflection.v1.ServerReflection/ServerReflectionInfo` on this service.
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ version = "0.5.1"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5.54", features = ["derive"] }
|
clap = { version = "4.5.54", features = ["derive"] }
|
||||||
colored = "3.1.1"
|
colored = "3.1.1"
|
||||||
granc_core = "0.4.1"
|
granc_core = { path = "../granc-core" }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||||
tonic = { workspace = true }
|
|
||||||
|
|
|
||||||
|
|
@ -13,20 +13,19 @@ pub struct Cli {
|
||||||
/// The server URL to connect to (e.g. http://localhost:50051)
|
/// The server URL to connect to (e.g. http://localhost:50051)
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
|
||||||
|
/// Path to the descriptor set (.bin)
|
||||||
|
#[arg(long)]
|
||||||
|
pub file_descriptor_set: Option<PathBuf>,
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: Commands,
|
pub command: Commands,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum Commands {
|
pub enum Commands {
|
||||||
/// Perform a gRPC call to a server
|
/// Perform a gRPC call to a server
|
||||||
///
|
///
|
||||||
/// This command connects to a gRPC server and executes a method using a JSON body.
|
/// This command connects to a gRPC server and executes a method using a JSON body.
|
||||||
///
|
|
||||||
/// ## Examples:
|
|
||||||
///
|
|
||||||
/// ```bash
|
|
||||||
/// granc call http://localhost:50051 my.pkg.Service/Method --body '{"key": "value"}'
|
|
||||||
/// ```
|
|
||||||
Call {
|
Call {
|
||||||
/// Endpoint (package.Service/Method)
|
/// Endpoint (package.Service/Method)
|
||||||
#[arg(value_parser = parse_endpoint)]
|
#[arg(value_parser = parse_endpoint)]
|
||||||
|
|
@ -37,10 +36,6 @@ pub enum Commands {
|
||||||
|
|
||||||
#[arg(short = 'H', long = "header", value_parser = parse_header)]
|
#[arg(short = 'H', long = "header", value_parser = parse_header)]
|
||||||
headers: Vec<(String, String)>,
|
headers: Vec<(String, String)>,
|
||||||
|
|
||||||
/// Path to the descriptor set (.bin)
|
|
||||||
#[arg(long)]
|
|
||||||
file_descriptor_set: Option<PathBuf>,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/// List available services
|
/// List available services
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
use colored::*;
|
use colored::*;
|
||||||
use granc_core::{
|
use granc_core::{
|
||||||
client::{ClientConnectError, DynamicCallError, GetDescriptorError, ListServicesError},
|
client::{with_file_descriptor, with_server_reflection},
|
||||||
prost_reflect::{EnumDescriptor, Kind, MessageDescriptor, MethodDescriptor, ServiceDescriptor},
|
prost_reflect::{
|
||||||
|
self, EnumDescriptor, Kind, MessageDescriptor, MethodDescriptor, ServiceDescriptor,
|
||||||
|
},
|
||||||
|
tonic::Status,
|
||||||
};
|
};
|
||||||
use tonic::Status;
|
use std::fmt::Display;
|
||||||
|
|
||||||
/// A wrapper struct for a formatted, colored string.
|
/// A wrapper struct for a formatted, colored string.
|
||||||
///
|
///
|
||||||
|
|
@ -12,6 +15,8 @@ pub struct FormattedString(pub String);
|
||||||
|
|
||||||
pub struct ServiceList(pub Vec<String>);
|
pub struct ServiceList(pub Vec<String>);
|
||||||
|
|
||||||
|
pub struct GenericError<T: Display>(pub &'static str, pub T);
|
||||||
|
|
||||||
impl std::fmt::Display for FormattedString {
|
impl std::fmt::Display for FormattedString {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
writeln!(f)?;
|
writeln!(f)?;
|
||||||
|
|
@ -37,6 +42,30 @@ impl From<Status> for FormattedString {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error from Reflection-based calls
|
||||||
|
impl From<with_server_reflection::DynamicCallError> for FormattedString {
|
||||||
|
fn from(err: with_server_reflection::DynamicCallError) -> Self {
|
||||||
|
FormattedString(format!("{}\n\n'{}'", "Call Failed:".red().bold(), err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error from FileDescriptor-based calls
|
||||||
|
impl From<with_file_descriptor::DynamicCallError> for FormattedString {
|
||||||
|
fn from(err: with_file_descriptor::DynamicCallError) -> Self {
|
||||||
|
FormattedString(format!("{}\n\n'{}'", "Call Failed:".red().bold(), err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<prost_reflect::DescriptorError> for FormattedString {
|
||||||
|
fn from(err: prost_reflect::DescriptorError) -> Self {
|
||||||
|
FormattedString(format!(
|
||||||
|
"{}\n\n'{}'",
|
||||||
|
"Failed to parse file descriptor:".red().bold(),
|
||||||
|
err
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<std::io::Error> for FormattedString {
|
impl From<std::io::Error> for FormattedString {
|
||||||
fn from(err: std::io::Error) -> Self {
|
fn from(err: std::io::Error) -> Self {
|
||||||
FormattedString(format!(
|
FormattedString(format!(
|
||||||
|
|
@ -47,24 +76,20 @@ impl From<std::io::Error> for FormattedString {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ClientConnectError> for FormattedString {
|
impl<T: Display> From<GenericError<T>> for FormattedString {
|
||||||
fn from(err: ClientConnectError) -> Self {
|
fn from(GenericError(msg, err): GenericError<T>) -> Self {
|
||||||
|
FormattedString(format!("{}:\n\n'{}'", msg.red().bold(), err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<with_server_reflection::ClientConnectError> for FormattedString {
|
||||||
|
fn from(err: with_server_reflection::ClientConnectError) -> Self {
|
||||||
FormattedString(format!("{}\n\n'{}'", "Connection Error:".red().bold(), err))
|
FormattedString(format!("{}\n\n'{}'", "Connection Error:".red().bold(), err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ListServicesError> for FormattedString {
|
impl From<with_server_reflection::GetDescriptorError> for FormattedString {
|
||||||
fn from(err: ListServicesError) -> Self {
|
fn from(err: with_server_reflection::GetDescriptorError) -> Self {
|
||||||
FormattedString(format!(
|
|
||||||
"{}\n\n'{}'",
|
|
||||||
"Failed to list services:".red().bold(),
|
|
||||||
err
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<GetDescriptorError> for FormattedString {
|
|
||||||
fn from(err: GetDescriptorError) -> Self {
|
|
||||||
FormattedString(format!(
|
FormattedString(format!(
|
||||||
"{}\n\n'{}'",
|
"{}\n\n'{}'",
|
||||||
"Symbol Lookup Failed:".red().bold(),
|
"Symbol Lookup Failed:".red().bold(),
|
||||||
|
|
@ -73,12 +98,6 @@ impl From<GetDescriptorError> for FormattedString {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DynamicCallError> for FormattedString {
|
|
||||||
fn from(err: DynamicCallError) -> Self {
|
|
||||||
FormattedString(format!("{}\n\n'{}'", "Call Failed:".red().bold(), err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ServiceList> for FormattedString {
|
impl From<ServiceList> for FormattedString {
|
||||||
fn from(ServiceList(services): ServiceList) -> Self {
|
fn from(ServiceList(services): ServiceList) -> Self {
|
||||||
if services.is_empty() {
|
if services.is_empty() {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
//!
|
//!
|
||||||
//! 1. **Initialization**: Parses command-line arguments using [`cli::Cli`].
|
//! 1. **Initialization**: Parses command-line arguments using [`cli::Cli`].
|
||||||
//! 2. **Connection**: Establishes a TCP connection to the target server via `granc_core`.
|
//! 2. **Connection**: Establishes a TCP connection to the target server via `granc_core`.
|
||||||
//! 3. **Execution**: Delegates the request processing to the `GrancClient`.
|
//! 3. **Execution**: Delegates the request processing to the `GrancClient` (handling state transitions).
|
||||||
//! 4. **Presentation**: Formats and prints the resulting data or errors to standard output/error.
|
//! 4. **Presentation**: Formats and prints the resulting data or errors to standard output/error.
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
|
|
@ -12,122 +12,140 @@ mod formatter;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::{Cli, Commands};
|
use cli::{Cli, Commands};
|
||||||
use formatter::FormattedString;
|
use formatter::{FormattedString, GenericError, ServiceList};
|
||||||
use granc_core::client::{Descriptor, DynamicRequest, DynamicResponse, GrancClient};
|
use granc_core::client::{
|
||||||
|
Descriptor, DynamicRequest, DynamicResponse, GrancClient, WithFileDescriptor,
|
||||||
|
WithServerReflection,
|
||||||
|
};
|
||||||
|
use granc_core::tonic::transport::Channel;
|
||||||
use std::process;
|
use std::process;
|
||||||
|
|
||||||
use crate::formatter::ServiceList;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let args = Cli::parse();
|
let args = Cli::parse();
|
||||||
// The URL is now a global argument, available for all commands
|
|
||||||
let url = args.url;
|
|
||||||
|
|
||||||
match args.command {
|
let client = unwrap_or_exit(GrancClient::connect(&args.url).await);
|
||||||
|
|
||||||
|
if let Some(path) = args.file_descriptor_set {
|
||||||
|
let bytes = unwrap_or_exit(std::fs::read(&path));
|
||||||
|
let client = unwrap_or_exit(client.with_file_descriptor(bytes));
|
||||||
|
handle_file_descriptor_mode(client, args.command).await;
|
||||||
|
} else {
|
||||||
|
handle_reflection_mode(client, args.command).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_reflection_mode(
|
||||||
|
mut client: GrancClient<WithServerReflection<Channel>>,
|
||||||
|
command: Commands,
|
||||||
|
) {
|
||||||
|
match command {
|
||||||
Commands::Call {
|
Commands::Call {
|
||||||
endpoint,
|
endpoint,
|
||||||
body,
|
body,
|
||||||
headers,
|
headers,
|
||||||
file_descriptor_set,
|
|
||||||
} => {
|
} => {
|
||||||
let (service, method) = endpoint;
|
let (service, method) = endpoint;
|
||||||
run_call(url, service, method, body, headers, file_descriptor_set).await;
|
let request = DynamicRequest {
|
||||||
|
body,
|
||||||
|
headers,
|
||||||
|
service,
|
||||||
|
method,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = unwrap_or_exit(client.dynamic(request).await);
|
||||||
|
print_response(response);
|
||||||
}
|
}
|
||||||
Commands::List => list_services(&url).await,
|
Commands::List => {
|
||||||
Commands::Describe { symbol } => describe_type(&url, &symbol).await,
|
let services = unwrap_or_exit(
|
||||||
}
|
client
|
||||||
}
|
.list_services()
|
||||||
|
.await
|
||||||
async fn connect_or_exit(url: &str) -> GrancClient {
|
.map_err(|err| GenericError("Failed to list services:", err)),
|
||||||
match GrancClient::connect(url).await {
|
);
|
||||||
Ok(client) => client,
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("{}", FormattedString::from(err));
|
|
||||||
process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn list_services(url: &str) {
|
|
||||||
let mut client = connect_or_exit(url).await;
|
|
||||||
|
|
||||||
match client.list_services().await {
|
|
||||||
Ok(services) => {
|
|
||||||
println!("{}", FormattedString::from(ServiceList(services)));
|
println!("{}", FormattedString::from(ServiceList(services)));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Commands::Describe { symbol } => {
|
||||||
eprintln!("{}", FormattedString::from(e));
|
let descriptor = unwrap_or_exit(client.get_descriptor_by_symbol(&symbol).await);
|
||||||
process::exit(1);
|
print_descriptor(descriptor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn describe_type(url: &str, symbol: &str) {
|
// --- Handler for File Descriptor Mode ---
|
||||||
let mut client = connect_or_exit(url).await;
|
|
||||||
|
|
||||||
match client.get_descriptor_by_symbol(symbol).await {
|
async fn handle_file_descriptor_mode(
|
||||||
Ok(Descriptor::MessageDescriptor(descriptor)) => {
|
mut client: GrancClient<WithFileDescriptor<Channel>>,
|
||||||
println!("{}", FormattedString::from(descriptor))
|
command: Commands,
|
||||||
}
|
|
||||||
Ok(Descriptor::ServiceDescriptor(descriptor)) => {
|
|
||||||
println!("{}", FormattedString::from(descriptor))
|
|
||||||
}
|
|
||||||
Ok(Descriptor::EnumDescriptor(descriptor)) => {
|
|
||||||
println!("{}", FormattedString::from(descriptor))
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{}", FormattedString::from(e));
|
|
||||||
process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run_call(
|
|
||||||
url: String,
|
|
||||||
service: String,
|
|
||||||
method: String,
|
|
||||||
body: serde_json::Value,
|
|
||||||
headers: Vec<(String, String)>,
|
|
||||||
file_descriptor_set: Option<std::path::PathBuf>,
|
|
||||||
) {
|
) {
|
||||||
let file_descriptor_set = match file_descriptor_set.map(std::fs::read).transpose() {
|
match command {
|
||||||
Ok(fd) => fd,
|
Commands::Call {
|
||||||
Err(err) => {
|
endpoint,
|
||||||
eprintln!("{}", FormattedString::from(err));
|
body,
|
||||||
|
headers,
|
||||||
|
} => {
|
||||||
|
let (service, method) = endpoint;
|
||||||
|
let request = DynamicRequest {
|
||||||
|
body,
|
||||||
|
headers,
|
||||||
|
service,
|
||||||
|
method,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = unwrap_or_exit(client.dynamic(request).await);
|
||||||
|
print_response(response);
|
||||||
|
}
|
||||||
|
Commands::List => {
|
||||||
|
let services = client.list_services();
|
||||||
|
println!("{}", FormattedString::from(ServiceList(services)));
|
||||||
|
}
|
||||||
|
Commands::Describe { symbol } => {
|
||||||
|
let descriptor = unwrap_or_exit(
|
||||||
|
client
|
||||||
|
.get_descriptor_by_symbol(&symbol)
|
||||||
|
.ok_or(GenericError("Symbol not found", symbol)),
|
||||||
|
);
|
||||||
|
print_descriptor(descriptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to return the Ok value or print the error and exit.
|
||||||
|
fn unwrap_or_exit<T, E>(result: Result<T, E>) -> T
|
||||||
|
where
|
||||||
|
E: Into<FormattedString>,
|
||||||
|
{
|
||||||
|
match result {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{}", Into::<FormattedString>::into(e));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let request = DynamicRequest {
|
fn print_descriptor(descriptor: Descriptor) {
|
||||||
file_descriptor_set,
|
match descriptor {
|
||||||
body,
|
Descriptor::MessageDescriptor(d) => println!("{}", FormattedString::from(d)),
|
||||||
headers,
|
Descriptor::ServiceDescriptor(d) => println!("{}", FormattedString::from(d)),
|
||||||
service,
|
Descriptor::EnumDescriptor(d) => println!("{}", FormattedString::from(d)),
|
||||||
method,
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
let mut client = connect_or_exit(&url).await;
|
fn print_response(response: DynamicResponse) {
|
||||||
|
match response {
|
||||||
match client.dynamic(request).await {
|
DynamicResponse::Unary(Ok(value)) => println!("{}", FormattedString::from(value)),
|
||||||
Ok(DynamicResponse::Unary(Ok(value))) => println!("{}", FormattedString::from(value)),
|
DynamicResponse::Unary(Err(status)) => println!("{}", FormattedString::from(status)),
|
||||||
Ok(DynamicResponse::Unary(Err(status))) => println!("{}", FormattedString::from(status)),
|
DynamicResponse::Streaming(Ok(values)) => {
|
||||||
Ok(DynamicResponse::Streaming(Ok(values))) => print_stream(&values),
|
for elem in values {
|
||||||
Ok(DynamicResponse::Streaming(Err(status))) => {
|
match elem {
|
||||||
|
Ok(val) => println!("{}", FormattedString::from(val)),
|
||||||
|
Err(status) => println!("{}", FormattedString::from(status)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DynamicResponse::Streaming(Err(status)) => {
|
||||||
println!("{}", FormattedString::from(status))
|
println!("{}", FormattedString::from(status))
|
||||||
}
|
}
|
||||||
Err(err) => {
|
|
||||||
eprintln!("{}", FormattedString::from(err));
|
|
||||||
process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_stream(stream: &[Result<serde_json::Value, tonic::Status>]) {
|
|
||||||
for elem in stream {
|
|
||||||
match elem {
|
|
||||||
Ok(val) => println!("{}", FormattedString::from(val.clone())),
|
|
||||||
Err(status) => println!("{}", FormattedString::from(status.clone())),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue