mirror of
https://codeberg.org/JasterV/granc.git
synced 2026-04-26 18:40:05 +00:00
feat: implement list and describe commands (#26)
This pull request introduces major improvements to the `granc` gRPC CLI, focusing on enhanced introspection and discovery features, a more user-friendly command-line interface, and improved error and output formatting. The changes include new commands for listing and describing services, methods, and messages, a restructured CLI argument parser, and a new formatter for colored, readable output. Additionally, the core client is extended to support these new features, and error handling is refactored for clarity. **New CLI features and UX improvements:** * Added new `list` and `describe` commands to the CLI, allowing users to discover available services and inspect service/message definitions directly from the server using reflection. The CLI argument structure is now subcommand-based for better usability. [[1]](diffhunk://#diff-dfa67e7f5e147119fe8d665da6b31b3605f5e196734ec7407aab2bcc9e2f656cL8-R84) [[2]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L71-R139) * Updated the README with documentation for the new commands and improved usage instructions. [[1]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R21) [[2]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L71-R139) **Core client and reflection enhancements:** * Implemented new methods in the core client for listing services and fetching symbol descriptors via reflection, including robust error types for each operation. [[1]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adL25-R27) [[2]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adL38-R78) [[3]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adR149-R211) [[4]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adL125-R229) [[5]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adL154-R252) [[6]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adL164-R262) [[7]](diffhunk://#diff-13deee04dd97de938cc46f0ef4faca083f3b471800e94cf45937122b83f01d57R19) [[8]](diffhunk://#diff-13deee04dd97de938cc46f0ef4faca083f3b471800e94cf45937122b83f01d57R124-R161) **Output formatting and error handling:** * Added a new `formatter` module for producing colored, human-friendly output for all major CLI operations, including pretty-printing of service lists, descriptors, and errors. * Improved error handling throughout the client and CLI, with more specific error types and user-facing messages. [[1]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adL38-R78) [[2]](diffhunk://#diff-0de6f761cf394791a15b0707e1a41f54559b5626f7aedb06ef339bc1a7ca6287R1-R248) **Dependency and project structure updates:** * Updated dependencies and added the `colored` crate for output styling.
This commit is contained in:
parent
191120c1d4
commit
26e46a4003
10 changed files with 819 additions and 194 deletions
36
Cargo.lock
generated
36
Cargo.lock
generated
|
|
@ -203,6 +203,15 @@ version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colored"
|
||||||
|
version = "3.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "echo-service"
|
name = "echo-service"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
|
|
@ -327,10 +336,11 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "granc"
|
name = "granc"
|
||||||
version = "0.3.1"
|
version = "0.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"granc_core 0.2.4",
|
"colored",
|
||||||
|
"granc_core",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tonic",
|
"tonic",
|
||||||
|
|
@ -345,27 +355,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "granc_core"
|
name = "granc_core"
|
||||||
version = "0.2.4"
|
version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2b70761ae61e50a3da701f80dec0a5768e43420895922e233894865ce3969cea"
|
|
||||||
dependencies = [
|
|
||||||
"futures-util",
|
|
||||||
"http",
|
|
||||||
"http-body",
|
|
||||||
"prost",
|
|
||||||
"prost-reflect",
|
|
||||||
"prost-types",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
|
||||||
"tokio-stream",
|
|
||||||
"tonic",
|
|
||||||
"tonic-prost",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "granc_core"
|
|
||||||
version = "0.3.1"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"echo-service",
|
"echo-service",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
|
|
||||||
93
README.md
93
README.md
|
|
@ -20,25 +20,14 @@ It is heavily inspired by tools like `grpcurl` but built to leverage the safety
|
||||||
* **Server Reflection**: Can fetch schemas directly from the server, removing the need to pass a local file descriptor set file (`.bin` or `.pb`).
|
* **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.
|
||||||
* **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.
|
||||||
|
|
||||||
## 📦 Installation
|
## 📦 Installation
|
||||||
|
|
||||||
### From Crates.io
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo install granc
|
cargo install --locked granc
|
||||||
```
|
|
||||||
|
|
||||||
### From Source
|
|
||||||
|
|
||||||
Ensure you have Rust and Cargo installed.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/JasterV/granc
|
|
||||||
cd granc
|
|
||||||
cargo install --path .
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🛠️ Prerequisites
|
## 🛠️ Prerequisites
|
||||||
|
|
@ -68,69 +57,79 @@ protoc \
|
||||||
**Syntax:**
|
**Syntax:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
granc [OPTIONS] <URL> <ENDPOINT>
|
granc <URL> <COMMAND> [ARGS]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arguments
|
### Global Arguments
|
||||||
|
|
||||||
| Argument | Description | Required |
|
| Argument | Description | Required |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `<URL>` | Server address (e.g., `http://[::1]:50051`). | **Yes** |
|
| `<URL>` | Server address (e.g., `http://[::1]:50051`). Must be the first argument. | **Yes** |
|
||||||
| `<ENDPOINT>` | Fully qualified method name (e.g., `my.package.Service/Method`). | **Yes** |
|
|
||||||
|
|
||||||
### Options
|
### Commands
|
||||||
|
|
||||||
| Flag | Short | Description | Required |
|
#### 1. `call` (Make Requests)
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `--proto-set` | | Path to the binary FileDescriptorSet (`.bin`). | **No** |
|
|
||||||
| `--body` | | The request body in JSON format. | **Yes** |
|
|
||||||
| `--header` | `-H` | Custom header `key:value`. Can be used multiple times. | No |
|
|
||||||
|
|
||||||
### Automatic Server Reflection
|
Performs a gRPC call using a JSON body.
|
||||||
|
|
||||||
If you omit the `--proto-set` flag, Granc will automatically attempt to connect to the server's reflection service to download the necessary schemas.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Using Reflection (no descriptor file needed)
|
granc http://localhost:50051 call <ENDPOINT> --body <JSON> [OPTIONS]
|
||||||
granc \
|
|
||||||
--body '{"name": "Ferris"}' \
|
|
||||||
http://localhost:50051 \
|
|
||||||
helloworld.Greeter/SayHello
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
| Argument/Flag | Description | Required |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `<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** |
|
||||||
|
| `--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
|
##### JSON Body Format
|
||||||
|
|
||||||
* **Unary / Server Streaming**: Provide a single JSON object `{ ... }`.
|
* **Unary / Server Streaming**: Provide a single JSON object `{ ... }`.
|
||||||
* **Client / Bidirectional Streaming**: Provide a JSON array of objects `[ { ... }, { ... } ]`.
|
* **Client / Bidirectional Streaming**: Provide a JSON array of objects `[ { ... }, { ... } ]`.
|
||||||
|
|
||||||
### Examples
|
##### Automatic Server Reflection
|
||||||
|
|
||||||
**1. Unary Call (using local descriptor)**
|
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 \
|
granc http://localhost:50051 call --body '{"name": "Ferris"}' helloworld.Greeter/SayHello
|
||||||
--proto-set ./descriptor.bin \
|
|
||||||
--body '{"name": "Ferris"}' \
|
|
||||||
http://localhost:50051 \
|
|
||||||
helloworld.Greeter/SayHello
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**2. Bidirectional Streaming (Chat)**
|
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.
|
||||||
|
|
||||||
|
#### 2. `list` (Service Discovery) (Server reflection required)
|
||||||
|
|
||||||
|
Lists all services exposed by the server.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
granc \
|
granc http://localhost:50051 list
|
||||||
--body '[{"text": "Hello"}, {"text": "How are you?"}]' \
|
```
|
||||||
-H "authorization: Bearer token123" \
|
|
||||||
http://localhost:50051 \
|
#### 3. `describe` (Introspection) (Server reflection required)
|
||||||
chat.ChatService/StreamMessages
|
|
||||||
|
Inspects services, messages or enums and prints their Protobuf definition.
|
||||||
|
|
||||||
|
**Describe Service:**
|
||||||
|
|
||||||
|
Describe in detail all methods of a service.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
granc http://localhost:50051 describe my.package.Greeter
|
||||||
|
```
|
||||||
|
|
||||||
|
**Describe Message:**
|
||||||
|
|
||||||
|
Shows the fields of a specific message type.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
granc http://localhost:50051 describe my.package.HelloRequest
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔮 Roadmap
|
## 🔮 Roadmap
|
||||||
|
|
||||||
* **Interactive Mode**: A REPL for streaming requests interactively.
|
* **Interactive Mode**: A REPL for streaming requests interactively.
|
||||||
* **Pretty Printing**: Enhanced colored output for JSON responses.
|
* **Pretty Printing JSON**: Enhanced colored output for JSON responses.
|
||||||
* **TLS Support**: Configurable root certificates and client identity.
|
* **TLS Support**: Configurable root certificates and client identity.
|
||||||
|
|
||||||
## 🧩 Using as a Library
|
## 🧩 Using as a Library
|
||||||
|
|
|
||||||
|
|
@ -8,27 +8,17 @@
|
||||||
|
|
||||||
Instead of strictly typed Rust structs, this library bridges standard `serde_json::Value` payloads directly to Protobuf binary wire format at runtime.
|
Instead of strictly typed Rust structs, this library bridges standard `serde_json::Value` payloads directly to Protobuf binary wire format at runtime.
|
||||||
|
|
||||||
## 📦 Installation
|
|
||||||
|
|
||||||
Add this to your `Cargo.toml`:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[dependencies]
|
|
||||||
granc_core = "0.2.3"
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
serde_json = "1"
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 High-Level Usage
|
## 🚀 High-Level Usage
|
||||||
|
|
||||||
The primary entry point is the [`GrancClient`]. It acts as an orchestrator that:
|
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. Connects to a gRPC server.
|
### 1. Making a Dynamic Call
|
||||||
2. Resolves the schema (either from a local file or via Server Reflection).
|
|
||||||
3. Determines the method type (Unary, Server Streaming, etc.).
|
|
||||||
4. Execute the request using JSON.
|
|
||||||
|
|
||||||
### Example: Making a Dynamic Call
|
The `dynamic` method handles the full request lifecycle:
|
||||||
|
|
||||||
|
1. Resolves the schema (either from a local file or via Server Reflection).
|
||||||
|
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};
|
||||||
|
|
@ -72,6 +62,25 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 2. Schema Introspection
|
||||||
|
|
||||||
|
`GrancClient` exposes several methods to inspect the server's available services and types using reflection.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// List all services exposed by the server
|
||||||
|
let services = client.list_services().await?;
|
||||||
|
println!("Available Services: {:?}", services);
|
||||||
|
|
||||||
|
// Get the descriptor for a specific type
|
||||||
|
let descriptor = client.get_descriptor_by_symbol("helloworld.Greeter").await?;
|
||||||
|
|
||||||
|
match descriptor {
|
||||||
|
Descriptor::MessageDescriptor(descriptor)) => println!("{}", descriptor.name())
|
||||||
|
Descriptor::ServiceDescriptor(descriptor)) => println!("{}", descriptor.name())
|
||||||
|
Descriptor::EnumDescriptor(descriptor)) => println!("{}", descriptor.name())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## 🛠️ Internal Components
|
## 🛠️ Internal Components
|
||||||
|
|
||||||
We expose the internal building blocks of `granc` for developers who need more granular control or want to build their own tools on top of our dynamic transport layer.
|
We expose the internal building blocks of `granc` for developers who need more granular control or want to build their own tools on top of our dynamic transport layer.
|
||||||
|
|
@ -110,13 +119,19 @@ The magic behind the dynamic serialization. This implementation of `tonic::codec
|
||||||
A client for `grpc.reflection.v1`. It enables runtime schema discovery.
|
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 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
|
```rust
|
||||||
use granc_core::reflection::client::ReflectionClient;
|
use granc_core::reflection::client::ReflectionClient;
|
||||||
|
|
||||||
let mut reflection = ReflectionClient::new(channel);
|
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?;
|
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.
|
You can then build a `prost_reflect::DescriptorPool` with the returned `prost_types::FileDescriptorSet` to be able to inspect in detail the descriptor.
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,37 @@
|
||||||
//! # 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
|
||||||
//! It acts as the bridge between the user's intent (a JSON body and a method name)
|
//! and offers support for reflection operations if the server supports it.
|
||||||
//! and the low-level gRPC transport.
|
|
||||||
//!
|
//!
|
||||||
//! ## Responsibilities
|
//! The [`GrancClient`] is the primary entry point for consumers of this library.
|
||||||
|
//! It abstracts away the complexity of connection management, schema resolution (reflection vs. file descriptors),
|
||||||
|
//! and generic gRPC transport.
|
||||||
//!
|
//!
|
||||||
//! 1. **Schema Resolution**: It determines whether to use a provided `FileDescriptorSet`
|
//! ## Example Usage
|
||||||
//! or to fetch the schema dynamically using the [`crate::reflection::client::ReflectionClient`].
|
//!
|
||||||
//! 2. **Method Lookup**: It validates that the requested service and method exist within
|
//! ```rust,no_run
|
||||||
//! the resolved schema.
|
//! use granc_core::client::{GrancClient, DynamicRequest};
|
||||||
//! 3. **Dispatch**: It inspects the method descriptor to determine the correct gRPC access
|
//! use serde_json::json;
|
||||||
//! pattern (Unary, Server Streaming, Client Streaming, or Bidirectional) and routes
|
//!
|
||||||
//! the request accordingly.
|
//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
//! 4. **Input Adaptation**: It converts input JSON data into the appropriate stream format
|
//! // 1. Connect to the server
|
||||||
//! required by the underlying transport.
|
//! let mut client = GrancClient::connect("http://localhost:50051").await?;
|
||||||
|
//!
|
||||||
|
//! // 2. Prepare the request (using server reflection)
|
||||||
|
//! let request = DynamicRequest {
|
||||||
|
//! service: "helloworld.Greeter".to_string(),
|
||||||
|
//! method: "SayHello".to_string(),
|
||||||
|
//! body: json!({ "name": "Ferris" }),
|
||||||
|
//! headers: vec![],
|
||||||
|
//! file_descriptor_set: None,
|
||||||
|
//! };
|
||||||
|
//!
|
||||||
|
//! // 3. Execute the call
|
||||||
|
//! let response = client.dynamic(request).await?;
|
||||||
|
//! println!("Response: {:?}", response);
|
||||||
|
//! # Ok(())
|
||||||
|
//! # }
|
||||||
|
//! ```
|
||||||
use crate::{
|
use crate::{
|
||||||
BoxError,
|
BoxError,
|
||||||
grpc::client::{GrpcClient, GrpcRequestError},
|
grpc::client::{GrpcClient, GrpcRequestError},
|
||||||
|
|
@ -22,9 +39,14 @@ use crate::{
|
||||||
};
|
};
|
||||||
use futures_util::Stream;
|
use futures_util::Stream;
|
||||||
use http_body::Body as HttpBody;
|
use http_body::Body as HttpBody;
|
||||||
use prost_reflect::{DescriptorError, DescriptorPool};
|
use prost_reflect::{
|
||||||
|
DescriptorError, DescriptorPool, EnumDescriptor, MessageDescriptor, ServiceDescriptor,
|
||||||
|
};
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
use tonic::transport::{Channel, Endpoint};
|
use tonic::{
|
||||||
|
Code,
|
||||||
|
transport::{Channel, Endpoint},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum ClientConnectError {
|
pub enum ClientConnectError {
|
||||||
|
|
@ -35,13 +57,26 @@ pub enum ClientConnectError {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum DynamicRequestError {
|
pub enum ListServicesError {
|
||||||
|
#[error("Reflection resolution failed: '{0}'")]
|
||||||
|
ReflectionResolve(#[from] ReflectionResolveError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum DynamicCallError {
|
||||||
#[error("Invalid input: '{0}'")]
|
#[error("Invalid input: '{0}'")]
|
||||||
InvalidInput(String),
|
InvalidInput(String),
|
||||||
|
|
||||||
#[error("Failed to read descriptor file: '{0}'")]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
|
|
||||||
#[error("Service '{0}' not found")]
|
#[error("Service '{0}' not found")]
|
||||||
ServiceNotFound(String),
|
ServiceNotFound(String),
|
||||||
|
|
||||||
|
|
@ -58,25 +93,81 @@ pub enum DynamicRequestError {
|
||||||
GrpcRequestError(#[from] GrpcRequestError),
|
GrpcRequestError(#[from] GrpcRequestError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A request object encapsulating all necessary information to perform a dynamic gRPC call.
|
||||||
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>>,
|
pub file_descriptor_set: Option<Vec<u8>>,
|
||||||
|
/// The JSON body of the request.
|
||||||
|
/// - For Unary/ServerStreaming: An Object `{}`.
|
||||||
|
/// - For ClientStreaming/Bidirectional: An Array of Objects `[{}]`.
|
||||||
pub body: serde_json::Value,
|
pub body: serde_json::Value,
|
||||||
|
/// Custom gRPC metadata (headers) to attach to the request.
|
||||||
pub headers: Vec<(String, String)>,
|
pub headers: Vec<(String, String)>,
|
||||||
|
/// The fully qualified name of the service (e.g., `my.package.Service`).
|
||||||
pub service: String,
|
pub service: String,
|
||||||
|
/// The name of the method to call (e.g., `SayHello`).
|
||||||
pub method: String,
|
pub method: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The result of a dynamic gRPC call.
|
||||||
pub enum DynamicResponse {
|
pub enum DynamicResponse {
|
||||||
|
/// A single response message (for Unary and Client Streaming calls).
|
||||||
Unary(Result<serde_json::Value, tonic::Status>),
|
Unary(Result<serde_json::Value, tonic::Status>),
|
||||||
|
/// A stream of response messages (for Server Streaming and Bidirectional calls).
|
||||||
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
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Descriptor {
|
||||||
|
MessageDescriptor(MessageDescriptor),
|
||||||
|
ServiceDescriptor(ServiceDescriptor),
|
||||||
|
EnumDescriptor(EnumDescriptor),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Descriptor {
|
||||||
|
pub fn message_descriptor(&self) -> Option<&MessageDescriptor> {
|
||||||
|
match self {
|
||||||
|
Descriptor::MessageDescriptor(message_descriptor) => Some(message_descriptor),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn service_descriptor(&self) -> Option<&ServiceDescriptor> {
|
||||||
|
match self {
|
||||||
|
Descriptor::ServiceDescriptor(service_descriptor) => Some(service_descriptor),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enum_descriptor(&self) -> Option<&EnumDescriptor> {
|
||||||
|
match self {
|
||||||
|
Descriptor::EnumDescriptor(enum_descriptor) => Some(enum_descriptor),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
pub struct GrancClient<S = Channel> {
|
||||||
reflection_client: ReflectionClient<S>,
|
reflection_client: ReflectionClient<S>,
|
||||||
grpc_client: GrpcClient<S>,
|
grpc_client: GrpcClient<S>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GrancClient<Channel> {
|
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> {
|
pub async fn connect(addr: &str) -> Result<Self, ClientConnectError> {
|
||||||
let endpoint = Endpoint::new(addr.to_string())
|
let endpoint = Endpoint::new(addr.to_string())
|
||||||
.map_err(|e| ClientConnectError::InvalidUrl(addr.to_string(), e))?;
|
.map_err(|e| ClientConnectError::InvalidUrl(addr.to_string(), e))?;
|
||||||
|
|
@ -96,6 +187,7 @@ where
|
||||||
S::ResponseBody: HttpBody<Data = tonic::codegen::Bytes> + Send + 'static,
|
S::ResponseBody: HttpBody<Data = tonic::codegen::Bytes> + Send + 'static,
|
||||||
<S::ResponseBody as HttpBody>::Error: Into<BoxError> + Send,
|
<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 {
|
pub fn new(service: S) -> Self {
|
||||||
let reflection_client = ReflectionClient::new(service.clone());
|
let reflection_client = ReflectionClient::new(service.clone());
|
||||||
let grpc_client = GrpcClient::new(service);
|
let grpc_client = GrpcClient::new(service);
|
||||||
|
|
@ -106,10 +198,91 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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(
|
pub async fn dynamic(
|
||||||
&mut self,
|
&mut self,
|
||||||
request: DynamicRequest,
|
request: DynamicRequest,
|
||||||
) -> Result<DynamicResponse, DynamicRequestError> {
|
) -> Result<DynamicResponse, DynamicCallError> {
|
||||||
let pool = match request.file_descriptor_set {
|
let pool = match request.file_descriptor_set {
|
||||||
Some(bytes) => DescriptorPool::decode(bytes.as_slice())?,
|
Some(bytes) => DescriptorPool::decode(bytes.as_slice())?,
|
||||||
// If no proto-set file is passed, we'll try to reach the server reflection service
|
// If no proto-set file is passed, we'll try to reach the server reflection service
|
||||||
|
|
@ -122,14 +295,12 @@ where
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let service = pool
|
let method = pool
|
||||||
.get_service_by_name(&request.service)
|
.get_service_by_name(&request.service)
|
||||||
.ok_or_else(|| DynamicRequestError::ServiceNotFound(request.service))?;
|
.ok_or_else(|| DynamicCallError::ServiceNotFound(request.service))?
|
||||||
|
|
||||||
let method = service
|
|
||||||
.methods()
|
.methods()
|
||||||
.find(|m| m.name() == request.method)
|
.find(|m| m.name() == request.method)
|
||||||
.ok_or_else(|| DynamicRequestError::MethodNotFound(request.method))?;
|
.ok_or_else(|| DynamicCallError::MethodNotFound(request.method))?;
|
||||||
|
|
||||||
match (method.is_client_streaming(), method.is_server_streaming()) {
|
match (method.is_client_streaming(), method.is_server_streaming()) {
|
||||||
(false, false) => {
|
(false, false) => {
|
||||||
|
|
@ -151,8 +322,8 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(true, false) => {
|
(true, false) => {
|
||||||
let input_stream = json_array_to_stream(request.body)
|
let input_stream =
|
||||||
.map_err(DynamicRequestError::InvalidInput)?;
|
json_array_to_stream(request.body).map_err(DynamicCallError::InvalidInput)?;
|
||||||
let result = self
|
let result = self
|
||||||
.grpc_client
|
.grpc_client
|
||||||
.client_streaming(method, input_stream, request.headers)
|
.client_streaming(method, input_stream, request.headers)
|
||||||
|
|
@ -161,8 +332,8 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
(true, true) => {
|
(true, true) => {
|
||||||
let input_stream = json_array_to_stream(request.body)
|
let input_stream =
|
||||||
.map_err(DynamicRequestError::InvalidInput)?;
|
json_array_to_stream(request.body).map_err(DynamicCallError::InvalidInput)?;
|
||||||
match self
|
match self
|
||||||
.grpc_client
|
.grpc_client
|
||||||
.bidirectional_streaming(method, input_stream, request.headers)
|
.bidirectional_streaming(method, input_stream, request.headers)
|
||||||
|
|
@ -176,6 +347,8 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper to convert a JSON Array into a Stream of JSON Values.
|
||||||
|
/// Required for Client and Bidirectional streaming.
|
||||||
fn json_array_to_stream(
|
fn json_array_to_stream(
|
||||||
json: serde_json::Value,
|
json: serde_json::Value,
|
||||||
) -> Result<impl Stream<Item = serde_json::Value> + Send + 'static, String> {
|
) -> Result<impl Stream<Item = serde_json::Value> + Send + 'static, String> {
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,28 @@
|
||||||
//! # Reflection Client
|
//! # Reflection Client
|
||||||
//!
|
//!
|
||||||
//! A client implementation for `grpc.reflection.v1`.
|
//! This module provides a client implementation for the gRPC Server Reflection Protocol (`grpc.reflection.v1`).
|
||||||
//!
|
//!
|
||||||
//! This client is responsible for building a complete `FileDescriptorSet` by querying
|
//! The [`ReflectionClient`] allows `granc` to inspect the schema of a running gRPC server at runtime.
|
||||||
//! a server that supports reflection. It handles the complexity of dependency management by inspecting
|
//! It is capable of:
|
||||||
//! imports and recursively fetching missing files until the entire schema tree for a
|
//!
|
||||||
//! requested symbol is resolved.
|
//! 1. **Listing Services**: Querying the server for all exposed service names.
|
||||||
|
//! 2. **Symbol Resolution**: Fetching the `FileDescriptorProto` for a specific symbol (Service or Message).
|
||||||
|
//! 3. **Dependency Management**: Automatically identifying missing imports in a file descriptor and recursively
|
||||||
|
//! fetching them from the server to build a complete, self-contained `FileDescriptorSet`.
|
||||||
|
//!
|
||||||
|
//! This client is designed to be resilient and handles the recursive graph traversal required to reconstruct
|
||||||
|
//! the full proto set from individual file descriptors.
|
||||||
//!
|
//!
|
||||||
//! ## References
|
//! ## References
|
||||||
//!
|
//!
|
||||||
//! * [gRPC Server Reflection Protocol](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md)//!
|
//! * [gRPC Server Reflection Protocol](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md)
|
||||||
use super::generated::reflection_v1::{
|
use super::generated::reflection_v1::{
|
||||||
ServerReflectionRequest, ServerReflectionResponse,
|
ServerReflectionRequest, ServerReflectionResponse,
|
||||||
server_reflection_client::ServerReflectionClient, server_reflection_request::MessageRequest,
|
server_reflection_client::ServerReflectionClient, server_reflection_request::MessageRequest,
|
||||||
server_reflection_response::MessageResponse,
|
server_reflection_response::MessageResponse,
|
||||||
};
|
};
|
||||||
use crate::BoxError;
|
use crate::BoxError;
|
||||||
|
use futures_util::stream::once;
|
||||||
use http_body::Body as HttpBody;
|
use http_body::Body as HttpBody;
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
use prost_types::{FileDescriptorProto, FileDescriptorSet};
|
use prost_types::{FileDescriptorProto, FileDescriptorSet};
|
||||||
|
|
@ -25,6 +32,7 @@ use tokio_stream::wrappers::ReceiverStream;
|
||||||
use tonic::transport::Channel;
|
use tonic::transport::Channel;
|
||||||
use tonic::{Streaming, client::GrpcService};
|
use tonic::{Streaming, client::GrpcService};
|
||||||
|
|
||||||
|
/// Errors that can occur during reflection resolution.
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum ReflectionResolveError {
|
pub enum ReflectionResolveError {
|
||||||
#[error(
|
#[error(
|
||||||
|
|
@ -56,7 +64,7 @@ pub enum ReflectionResolveError {
|
||||||
// So we won't enforce it from the user.
|
// So we won't enforce it from the user.
|
||||||
const EMPTY_HOST: &str = "";
|
const EMPTY_HOST: &str = "";
|
||||||
|
|
||||||
/// A generic client for the gRPC Server Reflection Protocol.
|
/// A client for interacting with the gRPC Server Reflection Service.
|
||||||
pub struct ReflectionClient<T = Channel> {
|
pub struct ReflectionClient<T = Channel> {
|
||||||
client: ServerReflectionClient<T>,
|
client: ServerReflectionClient<T>,
|
||||||
}
|
}
|
||||||
|
|
@ -68,25 +76,34 @@ where
|
||||||
S::ResponseBody: HttpBody<Data = tonic::codegen::Bytes> + Send + 'static,
|
S::ResponseBody: HttpBody<Data = tonic::codegen::Bytes> + Send + 'static,
|
||||||
<S::ResponseBody as HttpBody>::Error: Into<BoxError> + Send,
|
<S::ResponseBody as HttpBody>::Error: Into<BoxError> + Send,
|
||||||
{
|
{
|
||||||
|
/// Creates a new `ReflectionClient` using the provided gRPC service (e.g., a `Channel`).
|
||||||
pub fn new(channel: S) -> Self {
|
pub fn new(channel: S) -> Self {
|
||||||
let client = ServerReflectionClient::new(channel);
|
let client = ServerReflectionClient::new(channel);
|
||||||
Self { client }
|
Self { client }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Asks the reflection service for the file containing the requested symbol (e.g., `my.package.MyService`).
|
/// Fetches the complete `FileDescriptorSet` containing the definition for the given symbol.
|
||||||
///
|
///
|
||||||
/// **Recursive Resolution**:
|
/// This method performs a recursive lookup:
|
||||||
/// - The server returns a `FileDescriptorProto`.
|
/// 1. It asks the server for the file defining `service_name`.
|
||||||
/// - The client inspects the imports (dependencies) of that file.
|
/// 2. It parses the response and identifies any imported files (dependencies).
|
||||||
/// - It recursively requests any missing dependencies until the full `FileDescriptorSet` is built.
|
/// 3. It recursively requests those dependencies if they haven't been fetched yet.
|
||||||
|
/// 4. It aggregates all fetched files into a single `FileDescriptorSet`.
|
||||||
|
///
|
||||||
|
/// This ensures that the returned set is self-contained and can be used to build a
|
||||||
|
/// `prost_reflect::DescriptorPool`.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `symbol` - The fully qualified symbol name to resolve (e.g., `my.package.MyService`, `my.package.Message`).
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// * `Ok(fd_set)` - Successful reflection requests execution.
|
/// * `Ok(FileDescriptorSet)` - A set containing the file defining the symbol and all its transitive dependencies.
|
||||||
/// * `Err(ReflectionResolveError)` - Failed to request file descriptors to the reflection service.
|
/// * `Err(ReflectionResolveError)` - If the symbol is not found, the server doesn't support reflection, or a protocol error occurs.
|
||||||
pub async fn file_descriptor_set_by_symbol(
|
pub async fn file_descriptor_set_by_symbol(
|
||||||
&mut self,
|
&mut self,
|
||||||
service_name: &str,
|
symbol: &str,
|
||||||
) -> Result<FileDescriptorSet, ReflectionResolveError> {
|
) -> Result<FileDescriptorSet, ReflectionResolveError> {
|
||||||
// Initialize Stream
|
// Initialize Stream
|
||||||
let (tx, rx) = mpsc::channel(100);
|
let (tx, rx) = mpsc::channel(100);
|
||||||
|
|
@ -101,9 +118,7 @@ where
|
||||||
// Send Initial Request
|
// Send Initial Request
|
||||||
let req = ServerReflectionRequest {
|
let req = ServerReflectionRequest {
|
||||||
host: EMPTY_HOST.to_string(),
|
host: EMPTY_HOST.to_string(),
|
||||||
message_request: Some(MessageRequest::FileContainingSymbol(
|
message_request: Some(MessageRequest::FileContainingSymbol(symbol.to_string())),
|
||||||
service_name.to_string(),
|
|
||||||
)),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
tx.send(req)
|
tx.send(req)
|
||||||
|
|
@ -120,6 +135,51 @@ where
|
||||||
|
|
||||||
Ok(fd_set)
|
Ok(fd_set)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lists all services exposed by the server.
|
||||||
|
///
|
||||||
|
/// Sends a `ListServices` request to the reflection endpoint.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(Vec<String>)` - A string list where each string is a fully qualified service name (e.g., `grpc.reflection.v1.ServerReflection`, `helloworld.Greeter`).
|
||||||
|
/// * `Err(ReflectionResolveError)` - If the server doesn't support reflection or a protocol error occurs.
|
||||||
|
pub async fn list_services(&mut self) -> Result<Vec<String>, ReflectionResolveError> {
|
||||||
|
let req = ServerReflectionRequest {
|
||||||
|
host: EMPTY_HOST.to_string(),
|
||||||
|
message_request: Some(MessageRequest::ListServices(String::new())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut response_stream = self
|
||||||
|
.client
|
||||||
|
.server_reflection_info(once(async { req }))
|
||||||
|
.await
|
||||||
|
.map_err(ReflectionResolveError::ServerStreamInitFailed)?
|
||||||
|
.into_inner();
|
||||||
|
|
||||||
|
let response = response_stream
|
||||||
|
.message()
|
||||||
|
.await
|
||||||
|
.map_err(ReflectionResolveError::ServerStreamFailure)?
|
||||||
|
.ok_or(ReflectionResolveError::StreamClosed)?;
|
||||||
|
|
||||||
|
match response.message_response {
|
||||||
|
Some(MessageResponse::ListServicesResponse(resp)) => {
|
||||||
|
let services = resp.service.into_iter().map(|s| s.name).collect();
|
||||||
|
Ok(services)
|
||||||
|
}
|
||||||
|
Some(MessageResponse::ErrorResponse(e)) => Err(ReflectionResolveError::ServerError {
|
||||||
|
code: e.error_code,
|
||||||
|
message: e.error_message,
|
||||||
|
}),
|
||||||
|
Some(other) => Err(ReflectionResolveError::UnexpectedResponseType(format!(
|
||||||
|
"{other:?}",
|
||||||
|
))),
|
||||||
|
None => Err(ReflectionResolveError::UnexpectedResponseType(
|
||||||
|
"Empty Message".into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn collect_descriptors(
|
async fn collect_descriptors(
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,18 @@ use echo_service::EchoServiceServer;
|
||||||
use echo_service::FILE_DESCRIPTOR_SET;
|
use echo_service::FILE_DESCRIPTOR_SET;
|
||||||
use echo_service_impl::EchoServiceImpl;
|
use echo_service_impl::EchoServiceImpl;
|
||||||
use granc_core::client::{DynamicRequest, DynamicResponse, GrancClient};
|
use granc_core::client::{DynamicRequest, DynamicResponse, GrancClient};
|
||||||
|
use tonic_reflection::server::v1::ServerReflectionServer;
|
||||||
|
|
||||||
mod echo_service_impl;
|
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]
|
#[tokio::test]
|
||||||
async fn test_unary() {
|
async fn test_unary() {
|
||||||
let payload = serde_json::json!({ "message": "hello" });
|
let payload = serde_json::json!({ "message": "hello" });
|
||||||
|
|
@ -126,3 +135,79 @@ async fn test_bidirectional_streaming() {
|
||||||
_ => panic!("Received unary response for 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(_)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ version = "0.4.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5.54", features = ["derive"] }
|
clap = { version = "4.5.54", features = ["derive"] }
|
||||||
granc_core = "0.3.2"
|
colored = "3.1.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 }
|
tonic = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -5,25 +5,52 @@
|
||||||
//! It is responsible for parsing user input and performing validation (e.g., ensuring headers are `key:value`);
|
//! It is responsible for parsing user input and performing validation (e.g., ensuring headers are `key:value`);
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "granc", version, about = "Dynamic gRPC CLI")]
|
#[command(name = "granc", version, about = "Dynamic gRPC CLI")]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
#[arg(long, help = "Path to the descriptor set (.bin)")]
|
/// The server URL to connect to (e.g. http://localhost:50051)
|
||||||
pub file_descriptor_set: Option<PathBuf>,
|
|
||||||
|
|
||||||
#[arg(long, help = "JSON body (Object for Unary, Array for Streaming)", value_parser = parse_body)]
|
|
||||||
pub body: serde_json::Value,
|
|
||||||
|
|
||||||
#[arg(short = 'H', long = "header", value_parser = parse_header)]
|
|
||||||
pub headers: Vec<(String, String)>,
|
|
||||||
|
|
||||||
#[arg(help = "Server URL (http://host:port)")]
|
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
|
||||||
#[arg(help = "Endpoint (package.Service/Method)", value_parser = parse_endpoint)]
|
#[command(subcommand)]
|
||||||
pub endpoint: (String, String),
|
pub command: Commands,
|
||||||
|
}
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum Commands {
|
||||||
|
/// Perform a gRPC call to a server
|
||||||
|
///
|
||||||
|
/// 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 {
|
||||||
|
/// Endpoint (package.Service/Method)
|
||||||
|
#[arg(value_parser = parse_endpoint)]
|
||||||
|
endpoint: (String, String),
|
||||||
|
/// "JSON body (Object for Unary, Array for Streaming)"
|
||||||
|
#[arg(long, value_parser = parse_body)]
|
||||||
|
body: serde_json::Value,
|
||||||
|
|
||||||
|
#[arg(short = 'H', long = "header", value_parser = parse_header)]
|
||||||
|
headers: Vec<(String, String)>,
|
||||||
|
|
||||||
|
/// Path to the descriptor set (.bin)
|
||||||
|
#[arg(long)]
|
||||||
|
file_descriptor_set: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// List available services
|
||||||
|
List,
|
||||||
|
|
||||||
|
/// Describe a service, message or enum passing the full path
|
||||||
|
Describe {
|
||||||
|
/// Fully qualified name (e.g. my.package.Service)
|
||||||
|
symbol: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_endpoint(value: &str) -> Result<(String, String), String> {
|
fn parse_endpoint(value: &str) -> Result<(String, String), String> {
|
||||||
|
|
|
||||||
223
granc/src/formatter.rs
Normal file
223
granc/src/formatter.rs
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
use colored::*;
|
||||||
|
use granc_core::{
|
||||||
|
client::{ClientConnectError, DynamicCallError, GetDescriptorError, ListServicesError},
|
||||||
|
prost_reflect::{EnumDescriptor, Kind, MessageDescriptor, MethodDescriptor, ServiceDescriptor},
|
||||||
|
};
|
||||||
|
use tonic::Status;
|
||||||
|
|
||||||
|
/// A wrapper struct for a formatted, colored string.
|
||||||
|
///
|
||||||
|
/// Implements `Display` so it can be printed directly.
|
||||||
|
pub struct FormattedString(pub String);
|
||||||
|
|
||||||
|
pub struct ServiceList(pub Vec<String>);
|
||||||
|
|
||||||
|
impl std::fmt::Display for FormattedString {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
writeln!(f)?;
|
||||||
|
writeln!(f, "{}", self.0)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Value> for FormattedString {
|
||||||
|
fn from(value: serde_json::Value) -> Self {
|
||||||
|
FormattedString(serde_json::to_string_pretty(&value).unwrap_or_else(|_| value.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Status> for FormattedString {
|
||||||
|
fn from(status: Status) -> Self {
|
||||||
|
FormattedString(format!(
|
||||||
|
"{} code={:?} message={:?}",
|
||||||
|
"gRPC Failed:".red().bold(),
|
||||||
|
status.code(),
|
||||||
|
status.message()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for FormattedString {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
FormattedString(format!(
|
||||||
|
"{}\n\n'{}'",
|
||||||
|
"Failed to read file:".red().bold(),
|
||||||
|
err
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ClientConnectError> for FormattedString {
|
||||||
|
fn from(err: ClientConnectError) -> Self {
|
||||||
|
FormattedString(format!("{}\n\n'{}'", "Connection Error:".red().bold(), err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ListServicesError> for FormattedString {
|
||||||
|
fn from(err: ListServicesError) -> Self {
|
||||||
|
FormattedString(format!(
|
||||||
|
"{}\n\n'{}'",
|
||||||
|
"Failed to list services:".red().bold(),
|
||||||
|
err
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GetDescriptorError> for FormattedString {
|
||||||
|
fn from(err: GetDescriptorError) -> Self {
|
||||||
|
FormattedString(format!(
|
||||||
|
"{}\n\n'{}'",
|
||||||
|
"Symbol Lookup Failed:".red().bold(),
|
||||||
|
err
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DynamicCallError> for FormattedString {
|
||||||
|
fn from(err: DynamicCallError) -> Self {
|
||||||
|
FormattedString(format!("{}\n\n'{}'", "Call Failed:".red().bold(), err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ServiceList> for FormattedString {
|
||||||
|
fn from(ServiceList(services): ServiceList) -> Self {
|
||||||
|
if services.is_empty() {
|
||||||
|
return FormattedString("No services found.".yellow().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out = String::new();
|
||||||
|
out.push_str("Available Services:\n");
|
||||||
|
for svc in services {
|
||||||
|
out.push_str(&format!(" - {}\n", svc.green()));
|
||||||
|
}
|
||||||
|
FormattedString(out.trim_end().to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ServiceDescriptor> for FormattedString {
|
||||||
|
fn from(service: ServiceDescriptor) -> Self {
|
||||||
|
let mut out = String::new();
|
||||||
|
out.push_str(&format!(
|
||||||
|
"{} {} {{\n",
|
||||||
|
"service".cyan(),
|
||||||
|
service.name().green()
|
||||||
|
));
|
||||||
|
|
||||||
|
for method in service.methods() {
|
||||||
|
out.push_str(" ");
|
||||||
|
// Reuse the From<MethodDescriptor> implementation
|
||||||
|
let method_fmt = FormattedString::from(method);
|
||||||
|
out.push_str(&method_fmt.0);
|
||||||
|
out.push_str("\n\n");
|
||||||
|
}
|
||||||
|
out.push('}');
|
||||||
|
FormattedString(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MethodDescriptor> for FormattedString {
|
||||||
|
fn from(method: MethodDescriptor) -> Self {
|
||||||
|
let input_stream = if method.is_client_streaming() {
|
||||||
|
format!("{} ", "stream".cyan())
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
};
|
||||||
|
let output_stream = if method.is_server_streaming() {
|
||||||
|
format!("{} ", "stream".cyan())
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
FormattedString(format!(
|
||||||
|
"{} {}({}{}) {} ({}{});",
|
||||||
|
"rpc".cyan(),
|
||||||
|
method.name().green(),
|
||||||
|
input_stream,
|
||||||
|
method.input().full_name().yellow(),
|
||||||
|
"returns".cyan(),
|
||||||
|
output_stream,
|
||||||
|
method.output().full_name().yellow()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MessageDescriptor> for FormattedString {
|
||||||
|
fn from(message: MessageDescriptor) -> Self {
|
||||||
|
let mut out = String::new();
|
||||||
|
out.push_str(&format!(
|
||||||
|
"{} {} {{\n",
|
||||||
|
"message".cyan(),
|
||||||
|
message.name().green()
|
||||||
|
));
|
||||||
|
|
||||||
|
for field in message.fields() {
|
||||||
|
let label = if field.is_list() {
|
||||||
|
format!("{} ", "repeated".cyan())
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let type_name = match field.kind() {
|
||||||
|
Kind::Double => "double".yellow(),
|
||||||
|
Kind::Float => "float".yellow(),
|
||||||
|
Kind::Int32 => "int32".yellow(),
|
||||||
|
Kind::Int64 => "int64".yellow(),
|
||||||
|
Kind::Uint32 => "uint32".yellow(),
|
||||||
|
Kind::Uint64 => "uint64".yellow(),
|
||||||
|
Kind::Sint32 => "sint32".yellow(),
|
||||||
|
Kind::Sint64 => "sint64".yellow(),
|
||||||
|
Kind::Fixed32 => "fixed32".yellow(),
|
||||||
|
Kind::Fixed64 => "fixed64".yellow(),
|
||||||
|
Kind::Sfixed32 => "sfixed32".yellow(),
|
||||||
|
Kind::Sfixed64 => "sfixed64".yellow(),
|
||||||
|
Kind::Bool => "bool".yellow(),
|
||||||
|
Kind::String => "string".yellow(),
|
||||||
|
Kind::Bytes => "bytes".yellow(),
|
||||||
|
Kind::Message(m) => m.full_name().yellow(),
|
||||||
|
Kind::Enum(e) => e.full_name().yellow(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if field.is_map() {
|
||||||
|
out.push_str(&format!(
|
||||||
|
" // map entry: {} {} = {};\n",
|
||||||
|
type_name,
|
||||||
|
field.name(),
|
||||||
|
field.number()
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
out.push_str(&format!(
|
||||||
|
" {}{}{} {} = {};\n",
|
||||||
|
label,
|
||||||
|
type_name,
|
||||||
|
" ".normal(), // Reset color
|
||||||
|
field.name(),
|
||||||
|
field.number()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push('}');
|
||||||
|
FormattedString(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EnumDescriptor> for FormattedString {
|
||||||
|
fn from(enum_desc: EnumDescriptor) -> Self {
|
||||||
|
let mut out = String::new();
|
||||||
|
out.push_str(&format!(
|
||||||
|
"{} {} {{\n",
|
||||||
|
"enum".cyan(),
|
||||||
|
enum_desc.name().green()
|
||||||
|
));
|
||||||
|
|
||||||
|
for val in enum_desc.values() {
|
||||||
|
out.push_str(&format!(
|
||||||
|
" {} = {};\n",
|
||||||
|
val.name(),
|
||||||
|
val.number().to_string().purple()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
out.push('}');
|
||||||
|
|
||||||
|
FormattedString(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,77 +5,129 @@
|
||||||
//! 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`.
|
||||||
//! 4. **Presentation**: Formats and prints the resulting JSON or error status to standard output/error.
|
//! 4. **Presentation**: Formats and prints the resulting data or errors to standard output/error.
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
|
mod formatter;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::Cli;
|
use cli::{Cli, Commands};
|
||||||
use granc_core::client::{DynamicRequest, DynamicResponse, GrancClient};
|
use formatter::FormattedString;
|
||||||
|
use granc_core::client::{Descriptor, DynamicRequest, DynamicResponse, GrancClient};
|
||||||
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;
|
||||||
|
|
||||||
let file_descriptor_set = match args.file_descriptor_set.map(std::fs::read).transpose() {
|
match args.command {
|
||||||
Ok(fd) => fd,
|
Commands::Call {
|
||||||
Err(err) => {
|
endpoint,
|
||||||
eprintln!("Error reading file descriptor set: '{err}'");
|
body,
|
||||||
process::exit(1);
|
headers,
|
||||||
|
file_descriptor_set,
|
||||||
|
} => {
|
||||||
|
let (service, method) = endpoint;
|
||||||
|
run_call(url, service, method, body, headers, file_descriptor_set).await;
|
||||||
}
|
}
|
||||||
};
|
Commands::List => list_services(&url).await,
|
||||||
|
Commands::Describe { symbol } => describe_type(&url, &symbol).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let (service, method) = args.endpoint;
|
async fn connect_or_exit(url: &str) -> GrancClient {
|
||||||
|
match GrancClient::connect(url).await {
|
||||||
let request = DynamicRequest {
|
|
||||||
file_descriptor_set,
|
|
||||||
body: args.body,
|
|
||||||
headers: args.headers,
|
|
||||||
service,
|
|
||||||
method,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut client = match GrancClient::connect(&args.url).await {
|
|
||||||
Ok(client) => client,
|
Ok(client) => client,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Error: {err}");
|
eprintln!("{}", FormattedString::from(err));
|
||||||
process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match client.dynamic(request).await {
|
|
||||||
Ok(DynamicResponse::Unary(Ok(value))) => print_json(&value),
|
|
||||||
Ok(DynamicResponse::Unary(Err(status))) => print_status(&status),
|
|
||||||
Ok(DynamicResponse::Streaming(Ok(values))) => print_stream(&values),
|
|
||||||
Ok(DynamicResponse::Streaming(Err(status))) => print_status(&status),
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("Error: {err}");
|
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_json(val: &serde_json::Value) {
|
async fn list_services(url: &str) {
|
||||||
println!(
|
let mut client = connect_or_exit(url).await;
|
||||||
"{}",
|
|
||||||
serde_json::to_string_pretty(val).unwrap_or_else(|_| val.to_string())
|
match client.list_services().await {
|
||||||
);
|
Ok(services) => {
|
||||||
|
println!("{}", FormattedString::from(ServiceList(services)));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{}", FormattedString::from(e));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_status(status: &tonic::Status) {
|
async fn describe_type(url: &str, symbol: &str) {
|
||||||
eprintln!(
|
let mut client = connect_or_exit(url).await;
|
||||||
"gRPC Failed: code={:?} message={:?}",
|
|
||||||
status.code(),
|
match client.get_descriptor_by_symbol(symbol).await {
|
||||||
status.message()
|
Ok(Descriptor::MessageDescriptor(descriptor)) => {
|
||||||
);
|
println!("{}", FormattedString::from(descriptor))
|
||||||
|
}
|
||||||
|
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() {
|
||||||
|
Ok(fd) => fd,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("{}", FormattedString::from(err));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let request = DynamicRequest {
|
||||||
|
file_descriptor_set,
|
||||||
|
body,
|
||||||
|
headers,
|
||||||
|
service,
|
||||||
|
method,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut client = connect_or_exit(&url).await;
|
||||||
|
|
||||||
|
match client.dynamic(request).await {
|
||||||
|
Ok(DynamicResponse::Unary(Ok(value))) => println!("{}", FormattedString::from(value)),
|
||||||
|
Ok(DynamicResponse::Unary(Err(status))) => println!("{}", FormattedString::from(status)),
|
||||||
|
Ok(DynamicResponse::Streaming(Ok(values))) => print_stream(&values),
|
||||||
|
Ok(DynamicResponse::Streaming(Err(status))) => {
|
||||||
|
println!("{}", FormattedString::from(status))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("{}", FormattedString::from(err));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_stream(stream: &[Result<serde_json::Value, tonic::Status>]) {
|
fn print_stream(stream: &[Result<serde_json::Value, tonic::Status>]) {
|
||||||
for elem in stream {
|
for elem in stream {
|
||||||
match elem {
|
match elem {
|
||||||
Ok(val) => print_json(val),
|
Ok(val) => println!("{}", FormattedString::from(val.clone())),
|
||||||
Err(status) => print_status(status),
|
Err(status) => println!("{}", FormattedString::from(status.clone())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue