gRPC schema from files!

This commit is contained in:
Gregory Schier
2024-02-06 19:20:32 -08:00
parent 1293870e11
commit c6b5e4d5df
13 changed files with 442 additions and 230 deletions

92
src-tauri/Cargo.lock generated
View File

@@ -1161,12 +1161,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.6" version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.48.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -1696,12 +1696,14 @@ dependencies = [
"prost", "prost",
"prost-reflect", "prost-reflect",
"prost-types", "prost-types",
"protoc-bin-vendored",
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tonic", "tonic",
"tonic-reflection", "tonic-reflection",
"uuid",
] ]
[[package]] [[package]]
@@ -2379,9 +2381,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.150" version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]] [[package]]
name = "libm" name = "libm"
@@ -2422,9 +2424,9 @@ dependencies = [
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.11" version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]] [[package]]
name = "litemap" name = "litemap"
@@ -3430,6 +3432,56 @@ dependencies = [
"prost", "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]] [[package]]
name = "psl-types" name = "psl-types"
version = "2.0.11" version = "2.0.11"
@@ -3777,15 +3829,15 @@ dependencies = [
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.21" version = "0.38.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
dependencies = [ dependencies = [
"bitflags 2.4.1", "bitflags 2.4.1",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.48.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -4926,15 +4978,14 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.8.1" version = "3.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"fastrand", "fastrand",
"redox_syscall 0.4.1",
"rustix", "rustix",
"windows-sys 0.48.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -5457,9 +5508,9 @@ checksum = "64a8922555b9500e3d865caed19330172cd67cbf82203f1a3311d8c305cc9f33"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.5.0" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
dependencies = [ dependencies = [
"getrandom 0.2.11", "getrandom 0.2.11",
] ]
@@ -5890,6 +5941,15 @@ dependencies = [
"windows-targets 0.48.5", "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]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.42.2" version = "0.42.2"

View File

@@ -16,5 +16,7 @@ prost-reflect = { version = "0.12.0", features = ["serde", "derive"] }
log = "0.4.20" log = "0.4.20"
once_cell = { version = "1.19.0", features = [] } once_cell = { version = "1.19.0", features = [] }
anyhow = "1.0.79" anyhow = "1.0.79"
hyper = { version = "0.14"} hyper = { version = "0.14" }
hyper-rustls = { version = "0.24.0", features = ["http2"] } hyper-rustls = { version = "0.24.0", features = ["http2"] }
protoc-bin-vendored = "3.0.0"
uuid = { version = "1.7.0", features = ["v4"] }

View File

@@ -1,4 +1,5 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf;
use hyper::client::HttpConnector; use hyper::client::HttpConnector;
use hyper::Client; use hyper::Client;
@@ -13,11 +14,9 @@ use tonic::transport::Uri;
use tonic::{IntoRequest, IntoStreamingRequest, Streaming}; use tonic::{IntoRequest, IntoStreamingRequest, Streaming};
use crate::codec::DynamicCodec; 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}; use crate::{json_schema, MethodDefinition, ServiceDefinition};
type Result<T> = std::result::Result<T, String>;
#[derive(Clone)] #[derive(Clone)]
pub struct GrpcConnection { pub struct GrpcConnection {
pool: DescriptorPool, pool: DescriptorPool,
@@ -26,7 +25,12 @@ pub struct GrpcConnection {
} }
impl 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 service = self.pool.get_service_by_name(service).unwrap();
let method = &service.methods().find(|m| m.name() == method).unwrap(); let method = &service.methods().find(|m| m.name() == method).unwrap();
let input_message = method.input(); let input_message = method.input();
@@ -55,7 +59,7 @@ impl GrpcConnection {
service: &str, service: &str,
method: &str, method: &str,
stream: ReceiverStream<String>, stream: ReceiverStream<String>,
) -> Result<Streaming<DynamicMessage>> { ) -> Result<Streaming<DynamicMessage>, String> {
let service = self.pool.get_service_by_name(service).unwrap(); let service = self.pool.get_service_by_name(service).unwrap();
let method = &service.methods().find(|m| m.name() == method).unwrap(); let method = &service.methods().find(|m| m.name() == method).unwrap();
@@ -87,7 +91,7 @@ impl GrpcConnection {
service: &str, service: &str,
method: &str, method: &str,
stream: ReceiverStream<String>, stream: ReceiverStream<String>,
) -> Result<DynamicMessage> { ) -> Result<DynamicMessage, String> {
let service = self.pool.get_service_by_name(service).unwrap(); let service = self.pool.get_service_by_name(service).unwrap();
let method = &service.methods().find(|m| m.name() == method).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()); let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
@@ -121,7 +125,7 @@ impl GrpcConnection {
service: &str, service: &str,
method: &str, method: &str,
message: &str, message: &str,
) -> Result<Streaming<DynamicMessage>> { ) -> Result<Streaming<DynamicMessage>, String> {
let service = self.pool.get_service_by_name(service).unwrap(); let service = self.pool.get_service_by_name(service).unwrap();
let method = &service.methods().find(|m| m.name() == method).unwrap(); let method = &service.methods().find(|m| m.name() == method).unwrap();
let input_message = method.input(); let input_message = method.input();
@@ -159,56 +163,56 @@ impl Default for GrpcHandle {
} }
impl GrpcHandle { impl GrpcHandle {
pub async fn clean_reflect(&mut self, uri: &Uri) -> Result<Vec<ServiceDefinition>> { pub async fn services_from_files(
self.pools.remove(&uri.to_string()); &self,
self.reflect(uri).await 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>> { fn services_from_pool(&self, pool: &DescriptorPool) -> Vec<ServiceDefinition> {
let pool = match self.pools.get(&uri.to_string()) { pool.services()
Some(p) => p.clone(), .map(|s| {
None => { let mut def = ServiceDefinition {
let (pool, _) = fill_pool(uri).await?; name: s.full_name().to_string(),
self.pools.insert(uri.to_string(), pool.clone()); methods: vec![],
pool };
} for method in s.methods() {
}; let input_message = method.input();
def.methods.push(MethodDefinition {
let result = name: method.name().to_string(),
pool.services() server_streaming: method.is_server_streaming(),
.map(|s| { client_streaming: method.is_client_streaming(),
let mut def = ServiceDefinition { schema: serde_json::to_string_pretty(&json_schema::message_to_json_schema(
name: s.full_name().to_string(), &pool,
methods: vec![], input_message,
}; ))
for method in s.methods() { .unwrap(),
let input_message = method.input(); })
def.methods.push(MethodDefinition { }
name: method.name().to_string(), def
server_streaming: method.is_server_streaming(), })
client_streaming: method.is_client_streaming(), .collect::<Vec<_>>()
schema: serde_json::to_string_pretty(
&json_schema::message_to_json_schema(&pool, input_message),
)
.unwrap(),
})
}
def
})
.collect::<Vec<_>>();
Ok(result)
} }
pub async fn server_streaming( pub async fn server_streaming(
&mut self, &mut self,
id: &str, id: &str,
uri: Uri, uri: Uri,
proto_files: Vec<PathBuf>,
service: &str, service: &str,
method: &str, method: &str,
message: &str, message: &str,
) -> Result<Streaming<DynamicMessage>> { ) -> Result<Streaming<DynamicMessage>, String> {
self.connect(id, uri) self.connect(id, uri, proto_files)
.await? .await?
.server_streaming(service, method, message) .server_streaming(service, method, message)
.await .await
@@ -218,11 +222,12 @@ impl GrpcHandle {
&mut self, &mut self,
id: &str, id: &str,
uri: Uri, uri: Uri,
proto_files: Vec<PathBuf>,
service: &str, service: &str,
method: &str, method: &str,
stream: ReceiverStream<String>, stream: ReceiverStream<String>,
) -> Result<DynamicMessage> { ) -> Result<DynamicMessage, String> {
self.connect(id, uri) self.connect(id, uri, proto_files)
.await? .await?
.client_streaming(service, method, stream) .client_streaming(service, method, stream)
.await .await
@@ -232,18 +237,36 @@ impl GrpcHandle {
&mut self, &mut self,
id: &str, id: &str,
uri: Uri, uri: Uri,
proto_files: Vec<PathBuf>,
service: &str, service: &str,
method: &str, method: &str,
stream: ReceiverStream<String>, stream: ReceiverStream<String>,
) -> Result<Streaming<DynamicMessage>> { ) -> Result<Streaming<DynamicMessage>, String> {
self.connect(id, uri) self.connect(id, uri, proto_files)
.await? .await?
.streaming(service, method, stream) .streaming(service, method, stream)
.await .await
} }
pub async fn connect(&mut self, id: &str, uri: Uri) -> Result<GrpcConnection> { pub async fn connect(
let (pool, conn) = fill_pool(&uri).await?; &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 }; let connection = GrpcConnection { pool, conn, uri };
self.connections.insert(id.to_string(), connection.clone()); self.connections.insert(id.to_string(), connection.clone());
Ok(connection) Ok(connection)

View File

@@ -1,15 +1,17 @@
use std::env::temp_dir;
use std::ops::Deref; use std::ops::Deref;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command;
use std::str::FromStr; use std::str::FromStr;
use anyhow::anyhow; use anyhow::anyhow;
use hyper::client::HttpConnector; use hyper::client::HttpConnector;
use hyper::Client; use hyper::Client;
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
use log::warn; use log::{debug, warn};
use prost::Message; use prost::Message;
use prost_reflect::{DescriptorPool, MethodDescriptor}; use prost_reflect::{DescriptorPool, MethodDescriptor};
use prost_types::FileDescriptorProto; use prost_types::{FileDescriptorProto, FileDescriptorSet};
use tokio::fs; use tokio::fs;
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
use tonic::body::BoxBody; 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> { pub async fn fill_pool_from_files(paths: Vec<PathBuf>) -> Result<DescriptorPool, String> {
let mut pool = DescriptorPool::new(); 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 { for p in paths {
let bytes = fs::read(p).await.unwrap(); if p.as_path().exists() {
let fdp = FileDescriptorProto::decode(bytes.deref()).unwrap(); cmd.arg(p.as_path().to_string_lossy().as_ref());
pool.add_file_descriptor_proto(fdp) } else {
.map_err(|e| e.to_string())?; 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) Ok(pool)
} }
pub async fn fill_pool(
uri: &Uri, pub fn get_transport() -> Client<HttpsConnector<HttpConnector>, BoxBody> {
) -> Result<
(
DescriptorPool,
Client<HttpsConnector<HttpConnector>, BoxBody>,
),
String,
> {
let mut pool = DescriptorPool::new();
let connector = HttpsConnectorBuilder::new().with_native_roots(); let connector = HttpsConnectorBuilder::new().with_native_roots();
let connector = connector.https_or_http().enable_http2().wrap_connector({ let connector = connector.https_or_http().enable_http2().wrap_connector({
let mut http_connector = HttpConnector::new(); let mut http_connector = HttpConnector::new();
http_connector.enforce_http(false); http_connector.enforce_http(false);
http_connector http_connector
}); });
let transport = Client::builder() Client::builder()
.pool_max_idle_per_host(0) .pool_max_idle_per_host(0)
.http2_only(true) .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? { for service in list_services(&mut client).await? {
if service == "grpc.reflection.v1alpha.ServerReflection" { 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; file_descriptor_set_from_service_name(&service, &mut pool, &mut client).await;
} }
Ok((pool, transport)) Ok(pool)
} }
async fn list_services( async fn list_services(

View File

@@ -11,6 +11,7 @@ extern crate objc;
use std::collections::HashMap; use std::collections::HashMap;
use std::env::current_dir; use std::env::current_dir;
use std::fs::{create_dir_all, read_to_string, File}; use std::fs::{create_dir_all, read_to_string, File};
use std::path::PathBuf;
use std::process::exit; use std::process::exit;
use std::str::FromStr; use std::str::FromStr;
@@ -100,8 +101,28 @@ async fn cmd_grpc_reflect(
let req = get_grpc_request(&app_handle, request_id) let req = get_grpc_request(&app_handle, request_id)
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let uri = safe_uri(&req.url).map_err(|e| e.to_string())?; if req.proto_files.0.len() > 0 {
grpc_handle.lock().await.clean_reflect(&uri).await 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] #[tauri::command]
@@ -150,7 +171,15 @@ async fn cmd_grpc_call_unary(
let msg = match grpc_handle let msg = match grpc_handle
.lock() .lock()
.await .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? .await?
.unary( .unary(
&req.service.unwrap_or_default(), &req.service.unwrap_or_default(),
@@ -321,7 +350,18 @@ async fn cmd_grpc_client_streaming(
let msg = grpc_handle let msg = grpc_handle
.lock() .lock()
.await .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 .await
.unwrap(); .unwrap();
let message = serde_json::to_string(&msg).unwrap(); let message = serde_json::to_string(&msg).unwrap();
@@ -461,7 +501,17 @@ async fn cmd_grpc_streaming(
let mut stream = grpc_handle let mut stream = grpc_handle
.lock() .lock()
.await .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 .await
.unwrap(); .unwrap();
@@ -664,7 +714,17 @@ async fn cmd_grpc_server_streaming(
let mut stream = grpc_handle let mut stream = grpc_handle
.lock() .lock()
.await .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 .await
.unwrap(); .unwrap();

View File

@@ -75,11 +75,6 @@ export function GrpcConnectionSetupPane({
[updateRequest], [updateRequest],
); );
const handleSelectProtoFiles = useCallback(
(paths: string[]) => updateRequest.mutateAsync({ protoFiles: paths }),
[updateRequest],
);
const select = useMemo(() => { const select = useMemo(() => {
const options = const options =
services?.flatMap((s) => services?.flatMap((s) =>
@@ -159,10 +154,12 @@ export function GrpcConnectionSetupPane({
shortLabel: o.label, shortLabel: o.label,
}))} }))}
extraItems={[ extraItems={[
{ type: 'separator' },
{ {
label: 'Custom', label: 'Refresh',
type: 'default', type: 'default',
key: 'custom', key: 'custom',
leftSlot: <Icon className="text-gray-600" size="sm" icon="refresh" />,
}, },
]} ]}
> >
@@ -234,7 +231,6 @@ export function GrpcConnectionSetupPane({
reflectionError={reflectionError} reflectionError={reflectionError}
reflectionLoading={reflectionLoading} reflectionLoading={reflectionLoading}
onReflect={onReflectRefetch} onReflect={onReflectRefetch}
onSelectProtoFiles={handleSelectProtoFiles}
request={activeRequest} request={activeRequest}
/> />
</VStack> </VStack>

View File

@@ -1,4 +1,4 @@
import { open } from '@tauri-apps/api/dialog'; import classNames from 'classnames';
import type { EditorView } from 'codemirror'; import type { EditorView } from 'codemirror';
import { updateSchema } from 'codemirror-json-schema'; import { updateSchema } from 'codemirror-json-schema';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
@@ -7,14 +7,12 @@ import type { ReflectResponseService } from '../hooks/useGrpc';
import { tryFormatJson } from '../lib/formatters'; import { tryFormatJson } from '../lib/formatters';
import type { GrpcRequest } from '../lib/models'; import type { GrpcRequest } from '../lib/models';
import { count } from '../lib/pluralize'; import { count } from '../lib/pluralize';
import { Banner } from './core/Banner';
import { Button } from './core/Button'; import { Button } from './core/Button';
import type { EditorProps } from './core/Editor'; import type { EditorProps } from './core/Editor';
import { Editor } from './core/Editor'; import { Editor } from './core/Editor';
import { FormattedError } from './core/FormattedError'; import { FormattedError } from './core/FormattedError';
import { InlineCode } from './core/InlineCode'; import { InlineCode } from './core/InlineCode';
import { Link } from './core/Link'; import { VStack } from './core/Stacks';
import { HStack, VStack } from './core/Stacks';
import { useDialog } from './DialogContext'; import { useDialog } from './DialogContext';
import { GrpcProtoSelection } from './GrpcProtoSelection'; import { GrpcProtoSelection } from './GrpcProtoSelection';
@@ -23,16 +21,12 @@ type Props = Pick<EditorProps, 'heightMode' | 'onChange' | 'className'> & {
reflectionError?: string; reflectionError?: string;
reflectionLoading?: boolean; reflectionLoading?: boolean;
request: GrpcRequest; request: GrpcRequest;
onReflect: () => void;
onSelectProtoFiles: (paths: string[]) => void;
}; };
export function GrpcEditor({ export function GrpcEditor({
services, services,
reflectionError, reflectionError,
reflectionLoading, reflectionLoading,
onReflect,
onSelectProtoFiles,
request, request,
...extraEditorProps ...extraEditorProps
}: Props) { }: Props) {
@@ -97,6 +91,7 @@ export function GrpcEditor({
}, [alert, services, request.method, request.service]); }, [alert, services, request.method, request.service]);
const reflectionUnavailable = reflectionError?.match(/unimplemented/i); const reflectionUnavailable = reflectionError?.match(/unimplemented/i);
const reflectionSuccess = !reflectionError && services != null && request.protoFiles.length === 0;
reflectionError = reflectionUnavailable ? undefined : reflectionError; reflectionError = reflectionUnavailable ? undefined : reflectionError;
return ( return (
@@ -110,7 +105,7 @@ export function GrpcEditor({
placeholder="..." placeholder="..."
ref={editorViewRef} ref={editorViewRef}
actions={[ actions={[
<div key="reflection" className="!opacity-100"> <div key="reflection" className={classNames(!reflectionSuccess && '!opacity-100')}>
<Button <Button
size="xs" size="xs"
color={ color={
@@ -128,39 +123,13 @@ export function GrpcEditor({
title: 'Configure Schema', title: 'Configure Schema',
size: 'md', size: 'md',
id: 'reflection-failed', id: 'reflection-failed',
render: ({ hide }) => ( render: ({ hide }) => {
<VStack space={6} className="pb-5"> return (
{reflectionError && <FormattedError>{reflectionError}</FormattedError>} <VStack space={6} className="pb-5">
{reflectionUnavailable && request.protoFiles.length === 0 && ( <GrpcProtoSelection onDone={hide} requestId={request.id} />
<Banner> </VStack>
<VStack space={3}> );
<p> },
<InlineCode>{request.url}</InlineCode> doesn&apos;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>
),
}); });
}} }}
> >
@@ -170,10 +139,10 @@ export function GrpcEditor({
? 'Select Proto Files' ? 'Select Proto Files'
: reflectionError : reflectionError
? 'Server Error' ? 'Server Error'
: services != null
? 'Proto Schema'
: request.protoFiles.length > 0 : 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'} : 'Select Schema'}
</Button> </Button>
</div>, </div>,

View File

@@ -1,62 +1,42 @@
import { open } from '@tauri-apps/api/dialog'; import { open } from '@tauri-apps/api/dialog';
import { useGrpc } from '../hooks/useGrpc';
import { useGrpcRequest } from '../hooks/useGrpcRequest'; import { useGrpcRequest } from '../hooks/useGrpcRequest';
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest'; import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
import { count } from '../lib/pluralize';
import { Banner } from './core/Banner';
import { Button } from './core/Button'; import { Button } from './core/Button';
import { FormattedError } from './core/FormattedError';
import { IconButton } from './core/IconButton'; 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 request = useGrpcRequest(requestId);
const grpc = useGrpc(request, null);
const updateRequest = useUpdateGrpcRequest(request?.id ?? 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) { if (request == null) {
return null; return null;
} }
return ( return (
<div> <VStack className="flex-col-reverse" space={3}>
{request.protoFiles.length > 0 && ( {/* Buttons on top so they get focus first */}
<table className="w-full divide-y mb-3"> <HStack space={2} justifyContent="start" className="flex-row-reverse">
<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>
<Button <Button
color="primary" color="primary"
size="sm" size="sm"
@@ -68,12 +48,101 @@ export function GrpcProtoSelection({ requestId }: { requestId: string }) {
}); });
if (files == null || typeof files === 'string') return; if (files == null || typeof files === 'string') return;
const newFiles = files.filter((f) => !request.protoFiles.includes(f)); 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 Add Files
</Button> </Button>
<Button
isLoading={grpc.reflect.isFetching}
disabled={grpc.reflect.isFetching}
color="gray"
size="sm"
onClick={() => grpc.reflect.refetch()}
>
Refresh Schema
</Button>
</HStack> </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&apos;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>
); );
} }

View File

@@ -51,7 +51,23 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
), ),
render: ({ hide }) => { render: ({ hide }) => {
return ( 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 <Button
className="focus" className="focus"
color="gray" color="gray"
@@ -66,18 +82,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
> >
New Window New Window
</Button> </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> </HStack>
); );
}, },

View File

@@ -64,22 +64,14 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
variant === 'solid' && color === 'custom' && 'ring-blue-500/50', variant === 'solid' && color === 'custom' && 'ring-blue-500/50',
variant === 'solid' && variant === 'solid' &&
color === 'default' && 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' && variant === 'solid' &&
color === 'gray' && color === 'gray' &&
'text-gray-800 bg-highlight enabled:hocus:bg-gray-500/20 enabled:hocus:text-gray-1000 ring-blue-500/50', 'text-gray-800 bg-highlight enabled:hocus:text-gray-1000 ring-gray-400',
variant === 'solid' && variant === 'solid' && color === 'primary' && 'bg-blue-400 text-white ring-blue-700',
color === 'primary' && variant === 'solid' && color === 'secondary' && 'bg-violet-400 text-white ring-violet-700',
'bg-blue-400 text-white enabled:hocus:bg-blue-500 ring-blue-500/50', variant === 'solid' && color === 'warning' && 'bg-orange-400 text-white ring-orange-700',
variant === 'solid' && variant === 'solid' && color === 'danger' && 'bg-red-400 text-white ring-red-700',
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',
// Borders // Borders
variant === 'border' && 'border', variant === 'border' && 'border',
variant === 'border' && variant === 'border' &&

View File

@@ -399,7 +399,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
{items.map((item, i) => { {items.map((item, i) => {
if (item.type === 'separator') { if (item.type === 'separator') {
return ( return (
<Separator key={i} className="ml-2 my-1.5"> <Separator key={i} className={classNames('my-1.5', item.label && 'ml-2')}>
{item.label} {item.label}
</Separator> </Separator>
); );

View File

@@ -30,13 +30,13 @@ export function Confirm({ onHide, onResult, variant = 'confirm' }: ConfirmProps)
}; };
return ( 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}> <Button className="focus" color="gray" onClick={handleHide}>
Cancel Cancel
</Button> </Button>
<Button autoFocus className="focus" color={colors[variant]} onClick={handleSuccess}>
{confirmButtonTexts[variant]}
</Button>
</HStack> </HStack>
); );
} }

View File

@@ -52,14 +52,14 @@ export function useGrpc(req: GrpcRequest | null, conn: GrpcConnection | null) {
}); });
const debouncedUrl = useDebouncedValue<string>(req?.url ?? 'n/a', 1000); const debouncedUrl = useDebouncedValue<string>(req?.url ?? 'n/a', 1000);
const reflect = useQuery<ReflectResponseService[] | null>({ const reflect = useQuery<ReflectResponseService[] | null, string>({
enabled: req != null && req.protoFiles.length === 0, enabled: req != null,
queryKey: ['grpc_reflect', debouncedUrl], queryKey: ['grpc_reflect', debouncedUrl],
refetchOnWindowFocus: false,
queryFn: async () => { queryFn: async () => {
console.log('REFLECTING...');
return (await minPromiseMillis( return (await minPromiseMillis(
invoke('cmd_grpc_reflect', { requestId }), invoke('cmd_grpc_reflect', { requestId }),
1000, 300,
)) as ReflectResponseService[]; )) as ReflectResponseService[];
}, },
}); });