From dbdce4cf9a95a478ac339385b9b6a3424de8eae5 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Mon, 29 Jan 2024 20:50:43 -0800 Subject: [PATCH] Hooked up test call from frontend! --- src-tauri/Cargo.lock | 324 +++++++++++++++++++++++++++++- src-tauri/Cargo.toml | 2 + src-tauri/grpc/Cargo.toml | 18 ++ src-tauri/grpc/src/codec.rs | 52 +++++ src-tauri/grpc/src/json_schema.rs | 175 ++++++++++++++++ src-tauri/grpc/src/lib.rs | 79 ++++++++ src-tauri/grpc/src/proto.rs | 137 +++++++++++++ src-tauri/src/main.rs | 28 +++ src-web/main.tsx | 15 ++ 9 files changed, 821 insertions(+), 9 deletions(-) create mode 100644 src-tauri/grpc/Cargo.toml create mode 100644 src-tauri/grpc/src/codec.rs create mode 100644 src-tauri/grpc/src/json_schema.rs create mode 100644 src-tauri/grpc/src/lib.rs create mode 100644 src-tauri/grpc/src/proto.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 88fa03eb..c798866a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -77,9 +77,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "async-compression" @@ -95,6 +95,39 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "async-trait" +version = "0.1.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "atk" version = "0.15.1" @@ -134,6 +167,51 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa 1.0.9", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -1596,6 +1674,24 @@ dependencies = [ "system-deps 6.2.0", ] +[[package]] +name = "grpc" +version = "0.1.0" +dependencies = [ + "anyhow", + "log", + "once_cell", + "prost", + "prost-reflect", + "prost-types", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tonic", + "tonic-reflection", +] + [[package]] name = "gtk" version = "0.15.5" @@ -1853,6 +1949,18 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -2423,6 +2531,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "md-5" version = "0.10.6" @@ -2839,6 +2953,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "os_info" version = "3.7.0" @@ -3074,6 +3197,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -3230,6 +3373,64 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "prost-reflect" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057237efdb71cf4b3f9396302a3d6599a92fa94063ba537b66130980ea9909f3" +dependencies = [ + "base64 0.21.5", + "once_cell", + "prost", + "prost-reflect-derive", + "prost-types", + "serde", + "serde-value", +] + +[[package]] +name = "prost-reflect-derive" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172da1212c02be2c94901440cb27183cd92bff00ebacca5c323bf7520b8f9c04" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "prost-types" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +dependencies = [ + "prost", +] + [[package]] name = "psl-types" version = "2.0.11" @@ -3736,18 +3937,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.195" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] [[package]] -name = "serde_derive" -version = "1.0.195" +name = "serde-value" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", @@ -3756,9 +3967,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.111" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" dependencies = [ "itoa 1.0.9", "ryu", @@ -4314,6 +4525,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "synstructure" version = "0.13.0" @@ -4829,9 +5046,31 @@ dependencies = [ "num_cpus", "pin-project-lite", "socket2 0.5.5", + "tokio-macros", "windows-sys 0.48.0", ] +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "tokio-native-tls" version = "0.3.1" @@ -4935,6 +5174,72 @@ dependencies = [ "winnow", ] +[[package]] +name = "tonic" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.21.5", + "bytes", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-reflection" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa37c513df1339d197f4ba21d28c918b9ef1ac1768265f11ecb6b7f1cba1b76" +dependencies = [ + "prost", + "prost-types", + "tokio", + "tokio-stream", + "tonic", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand 0.8.5", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -5925,6 +6230,7 @@ dependencies = [ "cookie 0.18.0", "datetime", "futures", + "grpc", "http", "log", "objc", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f1f1d448..da9dfecf 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,3 +1,4 @@ +workspace = { members = ["grpc"] } [package] name = "yaak-app" version = "0.0.0" @@ -59,6 +60,7 @@ log = "0.4.20" datetime = "0.5.2" window-shadows = "0.2.2" reqwest_cookie_store = "0.6.0" +grpc = { path = "./grpc" } [features] # by default Tauri runs in production mode diff --git a/src-tauri/grpc/Cargo.toml b/src-tauri/grpc/Cargo.toml new file mode 100644 index 00000000..88137351 --- /dev/null +++ b/src-tauri/grpc/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "grpc" +version = "0.1.0" +edition = "2021" + +[dependencies] +tonic = "0.10.2" +prost = "0.12" +tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } +tonic-reflection = "0.10.2" +tokio-stream = "0.1.14" +prost-types = "0.12.3" +serde = { version = "1.0.196", features = ["derive"] } +serde_json = "1.0.113" +prost-reflect = { version = "0.12.0", features = ["serde", "derive"] } +log = "0.4.20" +once_cell = { version = "1.19.0", features = [] } +anyhow = "1.0.79" diff --git a/src-tauri/grpc/src/codec.rs b/src-tauri/grpc/src/codec.rs new file mode 100644 index 00000000..f01ce87d --- /dev/null +++ b/src-tauri/grpc/src/codec.rs @@ -0,0 +1,52 @@ +use prost_reflect::prost::Message; +use prost_reflect::{DynamicMessage, MethodDescriptor}; +use tonic::codec::{Codec, DecodeBuf, Decoder, EncodeBuf, Encoder}; +use tonic::Status; + +#[derive(Clone)] +pub struct DynamicCodec(MethodDescriptor); + +impl DynamicCodec { + #[allow(dead_code)] + pub fn new(md: MethodDescriptor) -> Self { + Self(md) + } +} + +impl Codec for DynamicCodec { + type Encode = DynamicMessage; + type Decode = DynamicMessage; + type Encoder = Self; + type Decoder = Self; + + fn encoder(&mut self) -> Self::Encoder { + self.clone() + } + + fn decoder(&mut self) -> Self::Decoder { + self.clone() + } +} + +impl Encoder for DynamicCodec { + type Item = DynamicMessage; + type Error = Status; + + fn encode(&mut self, item: Self::Item, dst: &mut EncodeBuf<'_>) -> Result<(), Self::Error> { + item.encode(dst) + .expect("buffer is too small to decode this message"); + Ok(()) + } +} + +impl Decoder for DynamicCodec { + type Item = DynamicMessage; + type Error = Status; + + fn decode(&mut self, src: &mut DecodeBuf<'_>) -> Result, Self::Error> { + let mut msg = DynamicMessage::new(self.0.output()); + msg.merge(src) + .map_err(|err| Status::internal(err.to_string()))?; + Ok(Some(msg)) + } +} diff --git a/src-tauri/grpc/src/json_schema.rs b/src-tauri/grpc/src/json_schema.rs new file mode 100644 index 00000000..3d859d17 --- /dev/null +++ b/src-tauri/grpc/src/json_schema.rs @@ -0,0 +1,175 @@ +use std::collections::HashMap; +use prost_reflect::{DescriptorPool, MessageDescriptor}; +use prost_types::field_descriptor_proto; +use serde::{Deserialize, Serialize}; + +#[derive(Default, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct JsonSchemaEntry { + #[serde(skip_serializing_if = "Option::is_none")] + title: Option, + + #[serde(rename = "type")] + type_: JsonType, + + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + properties: Option>, + + #[serde(rename = "enum", skip_serializing_if = "Option::is_none")] + enum_: Option>, + + /// Don't allow any other properties in the object + #[serde(skip_serializing_if = "Option::is_none")] + additional_properties: Option, + + /// Set all properties to required + #[serde(skip_serializing_if = "Option::is_none")] + required: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + items: Option>, +} + +enum JsonType { + String, + Number, + Object, + Array, + Boolean, + Null, + _UNKNOWN, +} + +impl Default for JsonType { + fn default() -> Self { + JsonType::_UNKNOWN + } +} + +impl serde::Serialize for JsonType { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + JsonType::String => serializer.serialize_str("string"), + JsonType::Number => serializer.serialize_str("number"), + JsonType::Object => serializer.serialize_str("object"), + JsonType::Array => serializer.serialize_str("array"), + JsonType::Boolean => serializer.serialize_str("boolean"), + JsonType::Null => serializer.serialize_str("null"), + JsonType::_UNKNOWN => serializer.serialize_str("unknown"), + } + } +} + +impl<'de> serde::Deserialize<'de> for JsonType { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match s.as_str() { + "string" => Ok(JsonType::String), + "number" => Ok(JsonType::Number), + "object" => Ok(JsonType::Object), + "array" => Ok(JsonType::Array), + "boolean" => Ok(JsonType::Boolean), + "null" => Ok(JsonType::Null), + _ => Ok(JsonType::_UNKNOWN), + } + } +} + +pub fn message_to_json_schema(pool: &DescriptorPool, message: MessageDescriptor) -> JsonSchemaEntry { + let mut schema = JsonSchemaEntry { + title: Some(message.name().to_string()), + type_: JsonType::Object, // Messages are objects + ..Default::default() + }; + + let mut properties = HashMap::new(); + message.fields().for_each(|f| match f.kind() { + prost_reflect::Kind::Message(m) => { + properties.insert(f.name().to_string(), message_to_json_schema(pool, m)); + } + prost_reflect::Kind::Enum(e) => { + properties.insert( + f.name().to_string(), + JsonSchemaEntry { + type_: map_proto_type_to_json_type(f.field_descriptor_proto().r#type()), + enum_: Some(e.values().map(|v| v.name().to_string()).collect::>()), + ..Default::default() + }, + ); + } + _ => { + // TODO: Handle repeated label + match f.field_descriptor_proto().label() { + field_descriptor_proto::Label::Repeated => { + // TODO: Handle more complex repeated types. This just handles primitives for now + properties.insert( + f.name().to_string(), + JsonSchemaEntry { + type_: JsonType::Array, + items: Some(Box::new(JsonSchemaEntry { + type_: map_proto_type_to_json_type( + f.field_descriptor_proto().r#type(), + ), + ..Default::default() + })), + ..Default::default() + }, + ); + } + _ => { + // Regular JSON field + properties.insert( + f.name().to_string(), + JsonSchemaEntry { + type_: map_proto_type_to_json_type(f.field_descriptor_proto().r#type()), + ..Default::default() + }, + ); + } + }; + } + }); + + schema.properties = Some(properties); + + schema.required = Some( + message + .fields() + .map(|f| f.name().to_string()) + .collect::>(), + ); + + schema +} + +fn map_proto_type_to_json_type(proto_type: field_descriptor_proto::Type) -> JsonType { + match proto_type { + field_descriptor_proto::Type::Double => JsonType::Number, + field_descriptor_proto::Type::Float => JsonType::Number, + field_descriptor_proto::Type::Int64 => JsonType::Number, + field_descriptor_proto::Type::Uint64 => JsonType::Number, + field_descriptor_proto::Type::Int32 => JsonType::Number, + field_descriptor_proto::Type::Fixed64 => JsonType::Number, + field_descriptor_proto::Type::Fixed32 => JsonType::Number, + field_descriptor_proto::Type::Bool => JsonType::Boolean, + field_descriptor_proto::Type::String => JsonType::String, + field_descriptor_proto::Type::Group => JsonType::_UNKNOWN, + field_descriptor_proto::Type::Message => JsonType::Object, + field_descriptor_proto::Type::Bytes => JsonType::String, + field_descriptor_proto::Type::Uint32 => JsonType::Number, + field_descriptor_proto::Type::Enum => JsonType::String, + field_descriptor_proto::Type::Sfixed32 => JsonType::Number, + field_descriptor_proto::Type::Sfixed64 => JsonType::Number, + field_descriptor_proto::Type::Sint32 => JsonType::Number, + field_descriptor_proto::Type::Sint64 => JsonType::Number, + } +} diff --git a/src-tauri/grpc/src/lib.rs b/src-tauri/grpc/src/lib.rs new file mode 100644 index 00000000..2668fb29 --- /dev/null +++ b/src-tauri/grpc/src/lib.rs @@ -0,0 +1,79 @@ +use prost_reflect::DynamicMessage; +use serde::{Deserialize, Serialize}; +use serde_json::Deserializer; +use tonic::IntoRequest; +use tonic::transport::Uri; + +use crate::codec::DynamicCodec; +use crate::proto::{fill_pool, method_desc_to_path}; + +mod codec; +mod json_schema; +mod proto; + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct ServiceDefinition { + pub name: String, + pub methods: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct MethodDefinition { + pub name: String, + pub schema: String, +} + +pub async fn call(uri: &Uri, service: &str, method: &str, message_json: &str) -> String { + let (pool, conn) = fill_pool(uri).await; + + let service = pool.get_service_by_name(service).unwrap(); + let method = &service.methods().find(|m| m.name() == method).unwrap(); + let input_message = method.input(); + + let mut deserializer = Deserializer::from_str(message_json); + let req_message = DynamicMessage::deserialize(input_message, &mut deserializer).unwrap(); + deserializer.end().unwrap(); + + let mut client = tonic::client::Grpc::new(conn); + + println!( + "\n---------- SENDING -----------------\n{}", + serde_json::to_string_pretty(&req_message).expect("json") + ); + + let req = req_message.into_request(); + let path = method_desc_to_path(method); + let codec = DynamicCodec::new(method.clone()); + client.ready().await.unwrap(); + + let resp = client.unary(req, path, codec).await.unwrap(); + let response_json = serde_json::to_string_pretty(&resp.into_inner()).expect("json to string"); + println!("\n---------- RECEIVING ---------------\n{}", response_json,); + + response_json +} + +pub async fn callable(uri: &Uri) -> Vec { + let (pool, _) = fill_pool(uri).await; + + pool.services() + .map(|s| { + let mut def = ServiceDefinition { + name: s.full_name().to_string(), + ..Default::default() + }; + for method in s.methods() { + let input_message = method.input(); + def.methods.push(MethodDefinition { + name: method.name().to_string(), + schema: serde_json::to_string_pretty(&json_schema::message_to_json_schema( + &pool, + input_message, + )) + .unwrap(), + }) + } + def + }) + .collect::>() +} diff --git a/src-tauri/grpc/src/proto.rs b/src-tauri/grpc/src/proto.rs new file mode 100644 index 00000000..dbd59a55 --- /dev/null +++ b/src-tauri/grpc/src/proto.rs @@ -0,0 +1,137 @@ +use std::ops::Deref; +use std::str::FromStr; +use anyhow::anyhow; +use prost::Message; +use prost_reflect::{DescriptorPool, MethodDescriptor}; +use prost_types::FileDescriptorProto; +use tokio_stream::StreamExt; +use tonic::codegen::http::uri::PathAndQuery; +use tonic::Request; +use tonic::transport::{Channel, Uri}; +use tonic_reflection::pb::server_reflection_client::ServerReflectionClient; +use tonic_reflection::pb::server_reflection_request::MessageRequest; +use tonic_reflection::pb::server_reflection_response::MessageResponse; +use tonic_reflection::pb::ServerReflectionRequest; + +pub async fn fill_pool(uri: &Uri) -> (DescriptorPool, Channel) { + let mut pool = DescriptorPool::new(); + let conn = tonic::transport::Endpoint::new(uri.clone()) + .unwrap() + .connect() + .await + .unwrap(); + + let mut client = ServerReflectionClient::new(conn.clone()); + let services = list_services(&mut client).await; + + for service in services { + if service == "grpc.reflection.v1alpha.ServerReflection" { + continue; + } + file_descriptor_set_from_service_name(&service, &mut pool, &mut client).await; + } + + (pool, conn) +} + +async fn list_services(reflect_client: &mut ServerReflectionClient) -> Vec { + let response = + send_reflection_request(reflect_client, MessageRequest::ListServices("".into())).await; + + let list_services_response = match response { + MessageResponse::ListServicesResponse(resp) => resp, + _ => panic!("Expected a ListServicesResponse variant"), + }; + + list_services_response + .service + .iter() + .map(|s| s.name.clone()) + .collect::>() +} + +async fn file_descriptor_set_from_service_name( + service_name: &str, + pool: &mut DescriptorPool, + client: &mut ServerReflectionClient, +) { + let response = send_reflection_request( + client, + MessageRequest::FileContainingSymbol(service_name.into()), + ) + .await; + + let file_descriptor_response = match response { + MessageResponse::FileDescriptorResponse(resp) => resp, + _ => panic!("Expected a FileDescriptorResponse variant"), + }; + + for fd in file_descriptor_response.file_descriptor_proto { + let fdp = FileDescriptorProto::decode(fd.deref()).unwrap(); + + // Add deps first or else we'll get an error + for dep_name in fdp.clone().dependency { + file_descriptor_set_by_filename(&dep_name, pool, client).await; + } + + pool.add_file_descriptor_proto(fdp) + .expect("add file descriptor proto"); + } +} + +async fn file_descriptor_set_by_filename( + filename: &str, + pool: &mut DescriptorPool, + client: &mut ServerReflectionClient, +) { + // We already fetched this file + if let Some(_) = pool.get_file_by_name(filename) { + return; + } + + let response = + send_reflection_request(client, MessageRequest::FileByFilename(filename.into())).await; + let file_descriptor_response = match response { + MessageResponse::FileDescriptorResponse(resp) => resp, + _ => panic!("Expected a FileDescriptorResponse variant"), + }; + + for fd in file_descriptor_response.file_descriptor_proto { + let fdp = FileDescriptorProto::decode(fd.deref()).unwrap(); + pool.add_file_descriptor_proto(fdp) + .expect("add file descriptor proto"); + } +} + +async fn send_reflection_request( + client: &mut ServerReflectionClient, + message: MessageRequest, +) -> MessageResponse { + let reflection_request = ServerReflectionRequest { + host: "".into(), // Doesn't matter + message_request: Some(message), + }; + + let request = Request::new(tokio_stream::once(reflection_request)); + + client + .server_reflection_info(request) + .await + .expect("server reflection failed") + .into_inner() + .next() + .await + .expect("steamed response") + .expect("successful response") + .message_response + .expect("some MessageResponse") +} + +pub fn method_desc_to_path(md: &MethodDescriptor) -> PathAndQuery { + let full_name = md.full_name(); + let (namespace, method_name) = full_name + .rsplit_once('.') + .ok_or_else(|| anyhow!("invalid method path")) + .expect("invalid method path"); + PathAndQuery::from_str(&format!("/{}/{}", namespace, method_name)).expect("invalid method path") +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index f29dacc9..92d69ec6 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -8,12 +8,15 @@ extern crate core; #[macro_use] extern crate objc; +use ::http::Uri; use std::collections::HashMap; use std::env::current_dir; use std::fs::{create_dir_all, read_to_string, File}; use std::process::exit; +use std::str::FromStr; use fern::colors::ColoredLevelConfig; +use grpc::ServiceDefinition; use log::{debug, error, info, warn}; use rand::random; use serde::Serialize; @@ -75,6 +78,29 @@ async fn migrate_db( Ok(()) } +#[tauri::command] +async fn grpc_reflect( + endpoint: &str, + // app_handle: AppHandle, + // db_instance: State<'_, Mutex>>, +) -> Result, String> { + let uri = Uri::from_str(endpoint).map_err(|e| e.to_string())?; + Ok(grpc::callable(&uri).await) +} + +#[tauri::command] +async fn grpc_call_unary( + endpoint: &str, + service: &str, + method: &str, + message: &str, + // app_handle: AppHandle, + // db_instance: State<'_, Mutex>>, +) -> Result { + let uri = Uri::from_str(endpoint).map_err(|e| e.to_string())?; + Ok(grpc::call(&uri, service, method, message).await) +} + #[tauri::command] async fn send_ephemeral_request( mut request: models::HttpRequest, @@ -977,6 +1003,8 @@ fn main() { get_request, get_settings, get_workspace, + grpc_call_unary, + grpc_reflect, import_data, list_cookie_jars, list_environments, diff --git a/src-web/main.tsx b/src-web/main.tsx index 0819069d..f2a7491a 100644 --- a/src-web/main.tsx +++ b/src-web/main.tsx @@ -1,3 +1,4 @@ +import { invoke } from '@tauri-apps/api'; import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { attachConsole } from 'tauri-plugin-log-api'; @@ -10,6 +11,20 @@ import { setAppearanceOnDocument } from './lib/theme/window'; import { appWindow } from '@tauri-apps/api/window'; import { type } from '@tauri-apps/api/os'; +try { + const services: any = await invoke('grpc_reflect', { endpoint: 'http://localhost:50051' }); + console.log('SERVICES', services); + const response = await invoke('grpc_call_unary', { + endpoint: 'http://localhost:50051', + service: services[0].name, + method: services[0].methods[0].name, + message: '{"name": "Greg"}', + }); + console.log('RESPONSE', response); +} catch (err) { + console.log('ERROR', err); +} + // Hide decorations here because it doesn't work in Rust for some reason (bug?) const osType = await type(); if (osType !== 'Darwin') {