mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-17 23:14:03 +01:00
gRPC schema from files!
This commit is contained in:
92
src-tauri/Cargo.lock
generated
92
src-tauri/Cargo.lock
generated
@@ -1161,12 +1161,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.6"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e"
|
||||
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1696,12 +1696,14 @@ dependencies = [
|
||||
"prost",
|
||||
"prost-reflect",
|
||||
"prost-types",
|
||||
"protoc-bin-vendored",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tonic",
|
||||
"tonic-reflection",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2379,9 +2381,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.150"
|
||||
version = "0.2.153"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
|
||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -2422,9 +2424,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.11"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
|
||||
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
@@ -3430,6 +3432,56 @@ dependencies = [
|
||||
"prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "005ca8623e5633e298ad1f917d8be0a44bcf406bf3cde3b80e63003e49a3f27d"
|
||||
dependencies = [
|
||||
"protoc-bin-vendored-linux-aarch_64",
|
||||
"protoc-bin-vendored-linux-ppcle_64",
|
||||
"protoc-bin-vendored-linux-x86_32",
|
||||
"protoc-bin-vendored-linux-x86_64",
|
||||
"protoc-bin-vendored-macos-x86_64",
|
||||
"protoc-bin-vendored-win32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-linux-aarch_64"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fb9fc9cce84c8694b6ea01cc6296617b288b703719b725b8c9c65f7c5874435"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-linux-ppcle_64"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02d2a07dcf7173a04d49974930ccbfb7fd4d74df30ecfc8762cf2f895a094516"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-linux-x86_32"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d54fef0b04fcacba64d1d80eed74a20356d96847da8497a59b0a0a436c9165b0"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-linux-x86_64"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8782f2ce7d43a9a5c74ea4936f001e9e8442205c244f7a3d4286bd4c37bc924"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-macos-x86_64"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5de656c7ee83f08e0ae5b81792ccfdc1d04e7876b1d9a38e6876a9e09e02537"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-win32"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9653c3ed92974e34c5a6e0a510864dab979760481714c172e0a34e437cb98804"
|
||||
|
||||
[[package]]
|
||||
name = "psl-types"
|
||||
version = "2.0.11"
|
||||
@@ -3777,15 +3829,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.21"
|
||||
version = "0.38.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
|
||||
checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4926,15 +4978,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.8.1"
|
||||
version = "3.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
|
||||
checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"redox_syscall 0.4.1",
|
||||
"rustix",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5457,9 +5508,9 @@ checksum = "64a8922555b9500e3d865caed19330172cd67cbf82203f1a3311d8c305cc9f33"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.5.0"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
|
||||
checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
|
||||
dependencies = [
|
||||
"getrandom 0.2.11",
|
||||
]
|
||||
@@ -5890,6 +5941,15 @@ dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
|
||||
@@ -16,5 +16,7 @@ prost-reflect = { version = "0.12.0", features = ["serde", "derive"] }
|
||||
log = "0.4.20"
|
||||
once_cell = { version = "1.19.0", features = [] }
|
||||
anyhow = "1.0.79"
|
||||
hyper = { version = "0.14"}
|
||||
hyper = { version = "0.14" }
|
||||
hyper-rustls = { version = "0.24.0", features = ["http2"] }
|
||||
protoc-bin-vendored = "3.0.0"
|
||||
uuid = { version = "1.7.0", features = ["v4"] }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use hyper::client::HttpConnector;
|
||||
use hyper::Client;
|
||||
@@ -13,11 +14,9 @@ use tonic::transport::Uri;
|
||||
use tonic::{IntoRequest, IntoStreamingRequest, Streaming};
|
||||
|
||||
use crate::codec::DynamicCodec;
|
||||
use crate::proto::{fill_pool, method_desc_to_path};
|
||||
use crate::proto::{fill_pool, fill_pool_from_files, get_transport, method_desc_to_path};
|
||||
use crate::{json_schema, MethodDefinition, ServiceDefinition};
|
||||
|
||||
type Result<T> = std::result::Result<T, String>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GrpcConnection {
|
||||
pool: DescriptorPool,
|
||||
@@ -26,7 +25,12 @@ pub struct GrpcConnection {
|
||||
}
|
||||
|
||||
impl GrpcConnection {
|
||||
pub async fn unary(&self, service: &str, method: &str, message: &str) -> Result<String> {
|
||||
pub async fn unary(
|
||||
&self,
|
||||
service: &str,
|
||||
method: &str,
|
||||
message: &str,
|
||||
) -> Result<String, String> {
|
||||
let service = self.pool.get_service_by_name(service).unwrap();
|
||||
let method = &service.methods().find(|m| m.name() == method).unwrap();
|
||||
let input_message = method.input();
|
||||
@@ -55,7 +59,7 @@ impl GrpcConnection {
|
||||
service: &str,
|
||||
method: &str,
|
||||
stream: ReceiverStream<String>,
|
||||
) -> Result<Streaming<DynamicMessage>> {
|
||||
) -> Result<Streaming<DynamicMessage>, String> {
|
||||
let service = self.pool.get_service_by_name(service).unwrap();
|
||||
let method = &service.methods().find(|m| m.name() == method).unwrap();
|
||||
|
||||
@@ -87,7 +91,7 @@ impl GrpcConnection {
|
||||
service: &str,
|
||||
method: &str,
|
||||
stream: ReceiverStream<String>,
|
||||
) -> Result<DynamicMessage> {
|
||||
) -> Result<DynamicMessage, String> {
|
||||
let service = self.pool.get_service_by_name(service).unwrap();
|
||||
let method = &service.methods().find(|m| m.name() == method).unwrap();
|
||||
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
|
||||
@@ -121,7 +125,7 @@ impl GrpcConnection {
|
||||
service: &str,
|
||||
method: &str,
|
||||
message: &str,
|
||||
) -> Result<Streaming<DynamicMessage>> {
|
||||
) -> Result<Streaming<DynamicMessage>, String> {
|
||||
let service = self.pool.get_service_by_name(service).unwrap();
|
||||
let method = &service.methods().find(|m| m.name() == method).unwrap();
|
||||
let input_message = method.input();
|
||||
@@ -159,56 +163,56 @@ impl Default for GrpcHandle {
|
||||
}
|
||||
|
||||
impl GrpcHandle {
|
||||
pub async fn clean_reflect(&mut self, uri: &Uri) -> Result<Vec<ServiceDefinition>> {
|
||||
self.pools.remove(&uri.to_string());
|
||||
self.reflect(uri).await
|
||||
pub async fn services_from_files(
|
||||
&self,
|
||||
paths: Vec<PathBuf>,
|
||||
) -> Result<Vec<ServiceDefinition>, String> {
|
||||
let pool = fill_pool_from_files(paths).await?;
|
||||
Ok(self.services_from_pool(&pool))
|
||||
}
|
||||
pub async fn services_from_reflection(
|
||||
&mut self,
|
||||
uri: &Uri,
|
||||
) -> Result<Vec<ServiceDefinition>, String> {
|
||||
let pool = fill_pool(uri).await?;
|
||||
Ok(self.services_from_pool(&pool))
|
||||
}
|
||||
|
||||
pub async fn reflect(&mut self, uri: &Uri) -> Result<Vec<ServiceDefinition>> {
|
||||
let pool = match self.pools.get(&uri.to_string()) {
|
||||
Some(p) => p.clone(),
|
||||
None => {
|
||||
let (pool, _) = fill_pool(uri).await?;
|
||||
self.pools.insert(uri.to_string(), pool.clone());
|
||||
pool
|
||||
}
|
||||
};
|
||||
|
||||
let result =
|
||||
pool.services()
|
||||
.map(|s| {
|
||||
let mut def = ServiceDefinition {
|
||||
name: s.full_name().to_string(),
|
||||
methods: vec![],
|
||||
};
|
||||
for method in s.methods() {
|
||||
let input_message = method.input();
|
||||
def.methods.push(MethodDefinition {
|
||||
name: method.name().to_string(),
|
||||
server_streaming: method.is_server_streaming(),
|
||||
client_streaming: method.is_client_streaming(),
|
||||
schema: serde_json::to_string_pretty(
|
||||
&json_schema::message_to_json_schema(&pool, input_message),
|
||||
)
|
||||
.unwrap(),
|
||||
})
|
||||
}
|
||||
def
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(result)
|
||||
fn services_from_pool(&self, pool: &DescriptorPool) -> Vec<ServiceDefinition> {
|
||||
pool.services()
|
||||
.map(|s| {
|
||||
let mut def = ServiceDefinition {
|
||||
name: s.full_name().to_string(),
|
||||
methods: vec![],
|
||||
};
|
||||
for method in s.methods() {
|
||||
let input_message = method.input();
|
||||
def.methods.push(MethodDefinition {
|
||||
name: method.name().to_string(),
|
||||
server_streaming: method.is_server_streaming(),
|
||||
client_streaming: method.is_client_streaming(),
|
||||
schema: serde_json::to_string_pretty(&json_schema::message_to_json_schema(
|
||||
&pool,
|
||||
input_message,
|
||||
))
|
||||
.unwrap(),
|
||||
})
|
||||
}
|
||||
def
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub async fn server_streaming(
|
||||
&mut self,
|
||||
id: &str,
|
||||
uri: Uri,
|
||||
proto_files: Vec<PathBuf>,
|
||||
service: &str,
|
||||
method: &str,
|
||||
message: &str,
|
||||
) -> Result<Streaming<DynamicMessage>> {
|
||||
self.connect(id, uri)
|
||||
) -> Result<Streaming<DynamicMessage>, String> {
|
||||
self.connect(id, uri, proto_files)
|
||||
.await?
|
||||
.server_streaming(service, method, message)
|
||||
.await
|
||||
@@ -218,11 +222,12 @@ impl GrpcHandle {
|
||||
&mut self,
|
||||
id: &str,
|
||||
uri: Uri,
|
||||
proto_files: Vec<PathBuf>,
|
||||
service: &str,
|
||||
method: &str,
|
||||
stream: ReceiverStream<String>,
|
||||
) -> Result<DynamicMessage> {
|
||||
self.connect(id, uri)
|
||||
) -> Result<DynamicMessage, String> {
|
||||
self.connect(id, uri, proto_files)
|
||||
.await?
|
||||
.client_streaming(service, method, stream)
|
||||
.await
|
||||
@@ -232,18 +237,36 @@ impl GrpcHandle {
|
||||
&mut self,
|
||||
id: &str,
|
||||
uri: Uri,
|
||||
proto_files: Vec<PathBuf>,
|
||||
service: &str,
|
||||
method: &str,
|
||||
stream: ReceiverStream<String>,
|
||||
) -> Result<Streaming<DynamicMessage>> {
|
||||
self.connect(id, uri)
|
||||
) -> Result<Streaming<DynamicMessage>, String> {
|
||||
self.connect(id, uri, proto_files)
|
||||
.await?
|
||||
.streaming(service, method, stream)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn connect(&mut self, id: &str, uri: Uri) -> Result<GrpcConnection> {
|
||||
let (pool, conn) = fill_pool(&uri).await?;
|
||||
pub async fn connect(
|
||||
&mut self,
|
||||
id: &str,
|
||||
uri: Uri,
|
||||
proto_files: Vec<PathBuf>,
|
||||
) -> Result<GrpcConnection, String> {
|
||||
let pool = match self.pools.get(id) {
|
||||
Some(p) => p.clone(),
|
||||
None => match proto_files.len() {
|
||||
0 => fill_pool(&uri).await?,
|
||||
_ => {
|
||||
let pool = fill_pool_from_files(proto_files).await?;
|
||||
self.pools.insert(id.to_string(), pool.clone());
|
||||
pool
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let conn = get_transport();
|
||||
let connection = GrpcConnection { pool, conn, uri };
|
||||
self.connections.insert(id.to_string(), connection.clone());
|
||||
Ok(connection)
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
use std::env::temp_dir;
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use hyper::client::HttpConnector;
|
||||
use hyper::Client;
|
||||
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
|
||||
use log::warn;
|
||||
use log::{debug, warn};
|
||||
use prost::Message;
|
||||
use prost_reflect::{DescriptorPool, MethodDescriptor};
|
||||
use prost_types::FileDescriptorProto;
|
||||
use prost_types::{FileDescriptorProto, FileDescriptorSet};
|
||||
use tokio::fs;
|
||||
use tokio_stream::StreamExt;
|
||||
use tonic::body::BoxBody;
|
||||
@@ -23,36 +25,71 @@ use tonic_reflection::pb::ServerReflectionRequest;
|
||||
|
||||
pub async fn fill_pool_from_files(paths: Vec<PathBuf>) -> Result<DescriptorPool, String> {
|
||||
let mut pool = DescriptorPool::new();
|
||||
let random_file_name = format!("{}.desc", uuid::Uuid::new_v4());
|
||||
let desc_path = temp_dir().join(random_file_name);
|
||||
let bin = protoc_bin_vendored::protoc_bin_path().unwrap();
|
||||
|
||||
let mut cmd = Command::new(bin.clone());
|
||||
cmd.arg("--include_imports")
|
||||
.arg("--include_source_info")
|
||||
.arg("-o")
|
||||
.arg(&desc_path);
|
||||
|
||||
for p in paths {
|
||||
let bytes = fs::read(p).await.unwrap();
|
||||
let fdp = FileDescriptorProto::decode(bytes.deref()).unwrap();
|
||||
pool.add_file_descriptor_proto(fdp)
|
||||
.map_err(|e| e.to_string())?;
|
||||
if p.as_path().exists() {
|
||||
cmd.arg(p.as_path().to_string_lossy().as_ref());
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
let parent = p.as_path().parent();
|
||||
if let Some(parent_path) = parent {
|
||||
cmd.arg("-I").arg(parent_path);
|
||||
} else {
|
||||
debug!("ignoring {:?} since it does not exist.", parent)
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Running: {:?}", cmd);
|
||||
|
||||
let output = cmd.output().map_err(|e| e.to_string())?;
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"protoc failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
let bytes = fs::read(desc_path.as_path())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let fdp = FileDescriptorSet::decode(bytes.deref()).map_err(|e| e.to_string())?;
|
||||
pool.add_file_descriptor_set(fdp)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
fs::remove_file(desc_path.as_path())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(pool)
|
||||
}
|
||||
pub async fn fill_pool(
|
||||
uri: &Uri,
|
||||
) -> Result<
|
||||
(
|
||||
DescriptorPool,
|
||||
Client<HttpsConnector<HttpConnector>, BoxBody>,
|
||||
),
|
||||
String,
|
||||
> {
|
||||
let mut pool = DescriptorPool::new();
|
||||
|
||||
pub fn get_transport() -> Client<HttpsConnector<HttpConnector>, BoxBody> {
|
||||
let connector = HttpsConnectorBuilder::new().with_native_roots();
|
||||
let connector = connector.https_or_http().enable_http2().wrap_connector({
|
||||
let mut http_connector = HttpConnector::new();
|
||||
http_connector.enforce_http(false);
|
||||
http_connector
|
||||
});
|
||||
let transport = Client::builder()
|
||||
Client::builder()
|
||||
.pool_max_idle_per_host(0)
|
||||
.http2_only(true)
|
||||
.build(connector);
|
||||
.build(connector)
|
||||
}
|
||||
|
||||
let mut client = ServerReflectionClient::with_origin(transport.clone(), uri.clone());
|
||||
pub async fn fill_pool(uri: &Uri) -> Result<DescriptorPool, String> {
|
||||
let mut pool = DescriptorPool::new();
|
||||
let mut client = ServerReflectionClient::with_origin(get_transport(), uri.clone());
|
||||
|
||||
for service in list_services(&mut client).await? {
|
||||
if service == "grpc.reflection.v1alpha.ServerReflection" {
|
||||
@@ -61,7 +98,7 @@ pub async fn fill_pool(
|
||||
file_descriptor_set_from_service_name(&service, &mut pool, &mut client).await;
|
||||
}
|
||||
|
||||
Ok((pool, transport))
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
async fn list_services(
|
||||
|
||||
@@ -11,6 +11,7 @@ extern crate objc;
|
||||
use std::collections::HashMap;
|
||||
use std::env::current_dir;
|
||||
use std::fs::{create_dir_all, read_to_string, File};
|
||||
use std::path::PathBuf;
|
||||
use std::process::exit;
|
||||
use std::str::FromStr;
|
||||
|
||||
@@ -100,8 +101,28 @@ async fn cmd_grpc_reflect(
|
||||
let req = get_grpc_request(&app_handle, request_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let uri = safe_uri(&req.url).map_err(|e| e.to_string())?;
|
||||
grpc_handle.lock().await.clean_reflect(&uri).await
|
||||
if req.proto_files.0.len() > 0 {
|
||||
println!("REFLECT FROM FILES");
|
||||
grpc_handle
|
||||
.lock()
|
||||
.await
|
||||
.services_from_files(
|
||||
req.proto_files
|
||||
.0
|
||||
.iter()
|
||||
.map(|p| PathBuf::from_str(p).unwrap())
|
||||
.collect(),
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
println!("REFLECT FROM URI");
|
||||
let uri = safe_uri(&req.url).map_err(|e| e.to_string())?;
|
||||
grpc_handle
|
||||
.lock()
|
||||
.await
|
||||
.services_from_reflection(&uri)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -150,7 +171,15 @@ async fn cmd_grpc_call_unary(
|
||||
let msg = match grpc_handle
|
||||
.lock()
|
||||
.await
|
||||
.connect(&conn.clone().id, uri)
|
||||
.connect(
|
||||
&req.clone().id,
|
||||
uri,
|
||||
req.proto_files
|
||||
.0
|
||||
.iter()
|
||||
.map(|p| PathBuf::from_str(p).unwrap())
|
||||
.collect(),
|
||||
)
|
||||
.await?
|
||||
.unary(
|
||||
&req.service.unwrap_or_default(),
|
||||
@@ -321,7 +350,18 @@ async fn cmd_grpc_client_streaming(
|
||||
let msg = grpc_handle
|
||||
.lock()
|
||||
.await
|
||||
.client_streaming(&conn.id, uri, &service, &method, in_msg_stream)
|
||||
.connect(
|
||||
&req.clone().id,
|
||||
uri,
|
||||
req.proto_files
|
||||
.0
|
||||
.iter()
|
||||
.map(|p| PathBuf::from_str(p).unwrap())
|
||||
.collect(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.client_streaming(&service, &method, in_msg_stream)
|
||||
.await
|
||||
.unwrap();
|
||||
let message = serde_json::to_string(&msg).unwrap();
|
||||
@@ -461,7 +501,17 @@ async fn cmd_grpc_streaming(
|
||||
let mut stream = grpc_handle
|
||||
.lock()
|
||||
.await
|
||||
.streaming(&conn.id, uri, &service, &method, in_msg_stream)
|
||||
.connect(
|
||||
&req.clone().id,
|
||||
uri,
|
||||
req.proto_files
|
||||
.0
|
||||
.iter()
|
||||
.map(|p| PathBuf::from_str(p).unwrap())
|
||||
.collect(),
|
||||
)
|
||||
.await?
|
||||
.streaming(&service, &method, in_msg_stream)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -664,7 +714,17 @@ async fn cmd_grpc_server_streaming(
|
||||
let mut stream = grpc_handle
|
||||
.lock()
|
||||
.await
|
||||
.server_streaming(&conn.id, uri, &service, &method, &req.message)
|
||||
.connect(
|
||||
&req.clone().id,
|
||||
uri,
|
||||
req.proto_files
|
||||
.0
|
||||
.iter()
|
||||
.map(|p| PathBuf::from_str(p).unwrap())
|
||||
.collect(),
|
||||
)
|
||||
.await?
|
||||
.server_streaming(&service, &method, &req.message)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -75,11 +75,6 @@ export function GrpcConnectionSetupPane({
|
||||
[updateRequest],
|
||||
);
|
||||
|
||||
const handleSelectProtoFiles = useCallback(
|
||||
(paths: string[]) => updateRequest.mutateAsync({ protoFiles: paths }),
|
||||
[updateRequest],
|
||||
);
|
||||
|
||||
const select = useMemo(() => {
|
||||
const options =
|
||||
services?.flatMap((s) =>
|
||||
@@ -159,10 +154,12 @@ export function GrpcConnectionSetupPane({
|
||||
shortLabel: o.label,
|
||||
}))}
|
||||
extraItems={[
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Custom',
|
||||
label: 'Refresh',
|
||||
type: 'default',
|
||||
key: 'custom',
|
||||
leftSlot: <Icon className="text-gray-600" size="sm" icon="refresh" />,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -234,7 +231,6 @@ export function GrpcConnectionSetupPane({
|
||||
reflectionError={reflectionError}
|
||||
reflectionLoading={reflectionLoading}
|
||||
onReflect={onReflectRefetch}
|
||||
onSelectProtoFiles={handleSelectProtoFiles}
|
||||
request={activeRequest}
|
||||
/>
|
||||
</VStack>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { open } from '@tauri-apps/api/dialog';
|
||||
import classNames from 'classnames';
|
||||
import type { EditorView } from 'codemirror';
|
||||
import { updateSchema } from 'codemirror-json-schema';
|
||||
import { useEffect, useRef } from 'react';
|
||||
@@ -7,14 +7,12 @@ import type { ReflectResponseService } from '../hooks/useGrpc';
|
||||
import { tryFormatJson } from '../lib/formatters';
|
||||
import type { GrpcRequest } from '../lib/models';
|
||||
import { count } from '../lib/pluralize';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
import type { EditorProps } from './core/Editor';
|
||||
import { Editor } from './core/Editor';
|
||||
import { FormattedError } from './core/FormattedError';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { Link } from './core/Link';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { useDialog } from './DialogContext';
|
||||
import { GrpcProtoSelection } from './GrpcProtoSelection';
|
||||
|
||||
@@ -23,16 +21,12 @@ type Props = Pick<EditorProps, 'heightMode' | 'onChange' | 'className'> & {
|
||||
reflectionError?: string;
|
||||
reflectionLoading?: boolean;
|
||||
request: GrpcRequest;
|
||||
onReflect: () => void;
|
||||
onSelectProtoFiles: (paths: string[]) => void;
|
||||
};
|
||||
|
||||
export function GrpcEditor({
|
||||
services,
|
||||
reflectionError,
|
||||
reflectionLoading,
|
||||
onReflect,
|
||||
onSelectProtoFiles,
|
||||
request,
|
||||
...extraEditorProps
|
||||
}: Props) {
|
||||
@@ -97,6 +91,7 @@ export function GrpcEditor({
|
||||
}, [alert, services, request.method, request.service]);
|
||||
|
||||
const reflectionUnavailable = reflectionError?.match(/unimplemented/i);
|
||||
const reflectionSuccess = !reflectionError && services != null && request.protoFiles.length === 0;
|
||||
reflectionError = reflectionUnavailable ? undefined : reflectionError;
|
||||
|
||||
return (
|
||||
@@ -110,7 +105,7 @@ export function GrpcEditor({
|
||||
placeholder="..."
|
||||
ref={editorViewRef}
|
||||
actions={[
|
||||
<div key="reflection" className="!opacity-100">
|
||||
<div key="reflection" className={classNames(!reflectionSuccess && '!opacity-100')}>
|
||||
<Button
|
||||
size="xs"
|
||||
color={
|
||||
@@ -128,39 +123,13 @@ export function GrpcEditor({
|
||||
title: 'Configure Schema',
|
||||
size: 'md',
|
||||
id: 'reflection-failed',
|
||||
render: ({ hide }) => (
|
||||
<VStack space={6} className="pb-5">
|
||||
{reflectionError && <FormattedError>{reflectionError}</FormattedError>}
|
||||
{reflectionUnavailable && request.protoFiles.length === 0 && (
|
||||
<Banner>
|
||||
<VStack space={3}>
|
||||
<p>
|
||||
<InlineCode>{request.url}</InlineCode> doesn't implement{' '}
|
||||
<Link href="https://github.com/grpc/grpc/blob/9aa3c5835a4ed6afae9455b63ed45c761d695bca/doc/server-reflection.md">
|
||||
Server Reflection
|
||||
</Link>{' '}
|
||||
. Please manually add the <InlineCode>.proto</InlineCode> files to get
|
||||
started.
|
||||
</p>
|
||||
<div>
|
||||
<Button
|
||||
size="xs"
|
||||
color="gray"
|
||||
variant="border"
|
||||
onClick={() => {
|
||||
hide();
|
||||
onReflect();
|
||||
}}
|
||||
>
|
||||
Retry Reflection
|
||||
</Button>
|
||||
</div>
|
||||
</VStack>
|
||||
</Banner>
|
||||
)}
|
||||
<GrpcProtoSelection requestId={request.id} />
|
||||
</VStack>
|
||||
),
|
||||
render: ({ hide }) => {
|
||||
return (
|
||||
<VStack space={6} className="pb-5">
|
||||
<GrpcProtoSelection onDone={hide} requestId={request.id} />
|
||||
</VStack>
|
||||
);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -170,10 +139,10 @@ export function GrpcEditor({
|
||||
? 'Select Proto Files'
|
||||
: reflectionError
|
||||
? 'Server Error'
|
||||
: services != null
|
||||
? 'Proto Schema'
|
||||
: request.protoFiles.length > 0
|
||||
? count('Proto File', request.protoFiles.length)
|
||||
? count('File', request.protoFiles.length)
|
||||
: services != null && request.protoFiles.length === 0
|
||||
? 'Schema Detected'
|
||||
: 'Select Schema'}
|
||||
</Button>
|
||||
</div>,
|
||||
|
||||
@@ -1,62 +1,42 @@
|
||||
import { open } from '@tauri-apps/api/dialog';
|
||||
import { useGrpc } from '../hooks/useGrpc';
|
||||
import { useGrpcRequest } from '../hooks/useGrpcRequest';
|
||||
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
|
||||
import { count } from '../lib/pluralize';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
import { FormattedError } from './core/FormattedError';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { Link } from './core/Link';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
|
||||
export function GrpcProtoSelection({ requestId }: { requestId: string }) {
|
||||
interface Props {
|
||||
requestId: string;
|
||||
onDone: () => void;
|
||||
}
|
||||
|
||||
export function GrpcProtoSelection({ requestId }: Props) {
|
||||
const request = useGrpcRequest(requestId);
|
||||
const grpc = useGrpc(request, null);
|
||||
const updateRequest = useUpdateGrpcRequest(request?.id ?? null);
|
||||
const services = grpc.reflect.data;
|
||||
const serverReflection = request?.protoFiles.length === 0 && services != null;
|
||||
let reflectError = grpc.reflect.error ?? null;
|
||||
const reflectionUnimplemented = `${reflectError}`.match(/unimplemented/i);
|
||||
|
||||
if (reflectionUnimplemented) {
|
||||
reflectError = null;
|
||||
}
|
||||
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{request.protoFiles.length > 0 && (
|
||||
<table className="w-full divide-y mb-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-gray-600">
|
||||
<span className="font-mono text-sm">*.proto</span> Files
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{request.protoFiles.map((f, i) => (
|
||||
<tr key={f + i} className="group">
|
||||
<td className="pl-1 text-sm font-mono">{f.split('/').pop()}</td>
|
||||
<td className="w-0 py-0.5">
|
||||
<IconButton
|
||||
title="Remove file"
|
||||
size="sm"
|
||||
icon="trash"
|
||||
className="ml-auto opacity-30 transition-opacity group-hover:opacity-100"
|
||||
onClick={() => {
|
||||
updateRequest.mutate({
|
||||
protoFiles: request.protoFiles.filter((p) => p !== f),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
<HStack space={2} justifyContent="end">
|
||||
<Button
|
||||
color="gray"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
updateRequest.mutate({ protoFiles: [] });
|
||||
}}
|
||||
>
|
||||
Clear Files
|
||||
</Button>
|
||||
<VStack className="flex-col-reverse" space={3}>
|
||||
{/* Buttons on top so they get focus first */}
|
||||
<HStack space={2} justifyContent="start" className="flex-row-reverse">
|
||||
<Button
|
||||
color="primary"
|
||||
size="sm"
|
||||
@@ -68,12 +48,101 @@ export function GrpcProtoSelection({ requestId }: { requestId: string }) {
|
||||
});
|
||||
if (files == null || typeof files === 'string') return;
|
||||
const newFiles = files.filter((f) => !request.protoFiles.includes(f));
|
||||
updateRequest.mutate({ protoFiles: [...request.protoFiles, ...newFiles] });
|
||||
await updateRequest.mutateAsync({ protoFiles: [...request.protoFiles, ...newFiles] });
|
||||
await grpc.reflect.refetch();
|
||||
}}
|
||||
>
|
||||
Add Files
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={grpc.reflect.isFetching}
|
||||
disabled={grpc.reflect.isFetching}
|
||||
color="gray"
|
||||
size="sm"
|
||||
onClick={() => grpc.reflect.refetch()}
|
||||
>
|
||||
Refresh Schema
|
||||
</Button>
|
||||
</HStack>
|
||||
</div>
|
||||
<VStack space={5}>
|
||||
{!serverReflection && services != null && services.length > 0 && (
|
||||
<Banner className="flex flex-col gap-2">
|
||||
<p>
|
||||
Found services
|
||||
{services?.slice(0, 5).map((s, i) => {
|
||||
return (
|
||||
<span key={i}>
|
||||
<InlineCode>{s.name}</InlineCode>
|
||||
{i === services.length - 1 ? '' : i === services.length - 2 ? ' and ' : ', '}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{services?.length > 5 && count('other', services?.length - 5)}
|
||||
</p>
|
||||
</Banner>
|
||||
)}
|
||||
{serverReflection && services != null && services.length > 0 && (
|
||||
<Banner className="flex flex-col gap-2">
|
||||
<p>
|
||||
Server reflection found services
|
||||
{services?.map((s, i) => {
|
||||
return (
|
||||
<span key={i}>
|
||||
<InlineCode>{s.name}</InlineCode>
|
||||
{i === services.length - 1 ? '' : i === services.length - 2 ? ' and ' : ', '}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
. You can override this schema by manually selecting <InlineCode>*.proto</InlineCode>{' '}
|
||||
files.
|
||||
</p>
|
||||
</Banner>
|
||||
)}
|
||||
|
||||
{request.protoFiles.length > 0 && (
|
||||
<table className="w-full divide-y">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-gray-600">
|
||||
<span className="font-mono text-sm">*.proto</span> Files
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{request.protoFiles.map((f, i) => (
|
||||
<tr key={f + i} className="group">
|
||||
<td className="pl-1 text-sm font-mono">{f.split('/').pop()}</td>
|
||||
<td className="w-0 py-0.5">
|
||||
<IconButton
|
||||
title="Remove file"
|
||||
size="sm"
|
||||
icon="trash"
|
||||
className="ml-auto opacity-30 transition-opacity group-hover:opacity-100"
|
||||
onClick={async () => {
|
||||
await updateRequest.mutateAsync({
|
||||
protoFiles: request.protoFiles.filter((p) => p !== f),
|
||||
});
|
||||
grpc.reflect.remove();
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
{reflectError && <FormattedError>{reflectError}</FormattedError>}
|
||||
{reflectionUnimplemented && request.protoFiles.length === 0 && (
|
||||
<Banner>
|
||||
<InlineCode>{request.url}</InlineCode> doesn't implement{' '}
|
||||
<Link href="https://github.com/grpc/grpc/blob/9aa3c5835a4ed6afae9455b63ed45c761d695bca/doc/server-reflection.md">
|
||||
Server Reflection
|
||||
</Link>{' '}
|
||||
. Please manually add the <InlineCode>.proto</InlineCode> files to get started.
|
||||
</Banner>
|
||||
)}
|
||||
</VStack>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,23 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
),
|
||||
render: ({ hide }) => {
|
||||
return (
|
||||
<HStack space={2} justifyContent="end" alignItems="center" className="mt-4 mb-6">
|
||||
<HStack
|
||||
space={2}
|
||||
justifyContent="start"
|
||||
alignItems="center"
|
||||
className="mt-4 mb-6 flex-row-reverse"
|
||||
>
|
||||
<Button
|
||||
className="focus"
|
||||
color="gray"
|
||||
onClick={async () => {
|
||||
hide();
|
||||
const environmentId = (await getRecentEnvironments(w.id))[0];
|
||||
routes.navigate('workspace', { workspaceId: w.id, environmentId });
|
||||
}}
|
||||
>
|
||||
This Window
|
||||
</Button>
|
||||
<Button
|
||||
className="focus"
|
||||
color="gray"
|
||||
@@ -66,18 +82,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
>
|
||||
New Window
|
||||
</Button>
|
||||
<Button
|
||||
autoFocus
|
||||
className="focus"
|
||||
color="gray"
|
||||
onClick={async () => {
|
||||
hide();
|
||||
const environmentId = (await getRecentEnvironments(w.id))[0];
|
||||
routes.navigate('workspace', { workspaceId: w.id, environmentId });
|
||||
}}
|
||||
>
|
||||
This Window
|
||||
</Button>
|
||||
</HStack>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -64,22 +64,14 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
variant === 'solid' && color === 'custom' && 'ring-blue-500/50',
|
||||
variant === 'solid' &&
|
||||
color === 'default' &&
|
||||
'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-1000 ring-blue-500/50',
|
||||
'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-800 ring-blue-500/50',
|
||||
variant === 'solid' &&
|
||||
color === 'gray' &&
|
||||
'text-gray-800 bg-highlight enabled:hocus:bg-gray-500/20 enabled:hocus:text-gray-1000 ring-blue-500/50',
|
||||
variant === 'solid' &&
|
||||
color === 'primary' &&
|
||||
'bg-blue-400 text-white enabled:hocus:bg-blue-500 ring-blue-500/50',
|
||||
variant === 'solid' &&
|
||||
color === 'secondary' &&
|
||||
'bg-violet-400 text-white enabled:hocus:bg-violet-500 ring-violet-500/50',
|
||||
variant === 'solid' &&
|
||||
color === 'warning' &&
|
||||
'bg-orange-400 text-white enabled:hocus:bg-orange-500 ring-orange-500/50',
|
||||
variant === 'solid' &&
|
||||
color === 'danger' &&
|
||||
'bg-red-400 text-white enabled:hocus:bg-red-500 ring-red-500/50',
|
||||
'text-gray-800 bg-highlight enabled:hocus:text-gray-1000 ring-gray-400',
|
||||
variant === 'solid' && color === 'primary' && 'bg-blue-400 text-white ring-blue-700',
|
||||
variant === 'solid' && color === 'secondary' && 'bg-violet-400 text-white ring-violet-700',
|
||||
variant === 'solid' && color === 'warning' && 'bg-orange-400 text-white ring-orange-700',
|
||||
variant === 'solid' && color === 'danger' && 'bg-red-400 text-white ring-red-700',
|
||||
// Borders
|
||||
variant === 'border' && 'border',
|
||||
variant === 'border' &&
|
||||
|
||||
@@ -399,7 +399,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
{items.map((item, i) => {
|
||||
if (item.type === 'separator') {
|
||||
return (
|
||||
<Separator key={i} className="ml-2 my-1.5">
|
||||
<Separator key={i} className={classNames('my-1.5', item.label && 'ml-2')}>
|
||||
{item.label}
|
||||
</Separator>
|
||||
);
|
||||
|
||||
@@ -30,13 +30,13 @@ export function Confirm({ onHide, onResult, variant = 'confirm' }: ConfirmProps)
|
||||
};
|
||||
|
||||
return (
|
||||
<HStack space={2} justifyContent="end" className="mt-2 mb-4">
|
||||
<HStack space={2} justifyContent="end" className="mt-2 mb-4 flex-row-reverse">
|
||||
<Button className="focus" color={colors[variant]} onClick={handleSuccess}>
|
||||
{confirmButtonTexts[variant]}
|
||||
</Button>
|
||||
<Button className="focus" color="gray" onClick={handleHide}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button autoFocus className="focus" color={colors[variant]} onClick={handleSuccess}>
|
||||
{confirmButtonTexts[variant]}
|
||||
</Button>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,14 +52,14 @@ export function useGrpc(req: GrpcRequest | null, conn: GrpcConnection | null) {
|
||||
});
|
||||
|
||||
const debouncedUrl = useDebouncedValue<string>(req?.url ?? 'n/a', 1000);
|
||||
const reflect = useQuery<ReflectResponseService[] | null>({
|
||||
enabled: req != null && req.protoFiles.length === 0,
|
||||
const reflect = useQuery<ReflectResponseService[] | null, string>({
|
||||
enabled: req != null,
|
||||
queryKey: ['grpc_reflect', debouncedUrl],
|
||||
refetchOnWindowFocus: false,
|
||||
queryFn: async () => {
|
||||
console.log('REFLECTING...');
|
||||
return (await minPromiseMillis(
|
||||
invoke('cmd_grpc_reflect', { requestId }),
|
||||
1000,
|
||||
300,
|
||||
)) as ReflectResponseService[];
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user