diff --git a/Cargo.lock b/Cargo.lock index e62e550..838a51a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,7 +163,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.20", ] [[package]] @@ -617,13 +617,13 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.20", ] [[package]] name = "druid" version = "0.8.3" -source = "git+https://github.com/andrewhickman/druid?branch=master#5ea6d1cb1f778e7d8f4023d0d3f80ed59876f815" +source = "git+https://github.com/andrewhickman/druid?branch=master#3acbc467fef42445c2e991a8c0c3188782a01622" dependencies = [ "console_error_panic_hook", "druid-derive", @@ -648,7 +648,7 @@ dependencies = [ [[package]] name = "druid-derive" version = "0.5.1" -source = "git+https://github.com/andrewhickman/druid?branch=master#5ea6d1cb1f778e7d8f4023d0d3f80ed59876f815" +source = "git+https://github.com/andrewhickman/druid?branch=master#3acbc467fef42445c2e991a8c0c3188782a01622" dependencies = [ "proc-macro2", "quote", @@ -658,7 +658,7 @@ dependencies = [ [[package]] name = "druid-shell" version = "0.8.3" -source = "git+https://github.com/andrewhickman/druid?branch=master#5ea6d1cb1f778e7d8f4023d0d3f80ed59876f815" +source = "git+https://github.com/andrewhickman/druid?branch=master#3acbc467fef42445c2e991a8c0c3188782a01622" dependencies = [ "anyhow", "ashpd", @@ -733,9 +733,15 @@ checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.20", ] +[[package]] +name = "equivalent" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" + [[package]] name = "errno" version = "0.3.1" @@ -971,7 +977,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.20", ] [[package]] @@ -1146,27 +1152,27 @@ dependencies = [ [[package]] name = "gix-bitmap" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc02feb20ad313d52a450852f2005c2205d24f851e74d82b7807cbe12c371667" +checksum = "311e2fa997be6560c564b070c5da2d56d038b645a94e1e5796d5d85a350da33c" dependencies = [ "thiserror", ] [[package]] name = "gix-chunk" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7acf3bc6c4b91e8fb260086daf5e105ea3a6d913f5fd3318137f7e309d6e540" +checksum = "39db5ed0fc0a2e9b1b8265993f7efdbc30379dec268f3b91b7af0c2de4672fdd" dependencies = [ "thiserror", ] [[package]] name = "gix-command" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6141b70cfb21255223e42f3379855037cbbe8673b58dd8318d2f09b516fad1" +checksum = "bb49ab557a37b0abb2415bca2b10e541277dff0565deb5bd5e99fd95f93f51eb" dependencies = [ "bstr", ] @@ -1195,9 +1201,9 @@ dependencies = [ [[package]] name = "gix-config-value" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f216df1c33e6e1555923eff0096858a879e8aaadd35b5d788641e4e8064c892" +checksum = "4783caa23062f86acfd1bc9e72c62250923d1673171ce1a524d9486f8a4556a8" dependencies = [ "bitflags 2.3.2", "bstr", @@ -1301,9 +1307,9 @@ dependencies = [ [[package]] name = "gix-hash" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee181c85d3955f54c4426e6bfaeeada4428692e1a39b8788c2ac7785fc301dd8" +checksum = "a0dd58cdbe7ffa4032fc111864c80d5f8cecd9a2c9736c97ae7e5be834188272" dependencies = [ "hex", "thiserror", @@ -1311,9 +1317,9 @@ dependencies = [ [[package]] name = "gix-hashtable" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd259bd0d96e6153e357a8cdaca76c48e103fd34208b6c0ce77b1ad995834bd2" +checksum = "2cfd7f4ea905c13579565e3c264ca2c4103d192bd5fce2300c5a884cf1977d61" dependencies = [ "gix-hash", "hashbrown 0.13.2", @@ -1437,11 +1443,12 @@ dependencies = [ [[package]] name = "gix-path" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1226f2e50adeb4d76c754c1856c06f13a24cad1624801653fbf09b869e5b808" +checksum = "4ea2a19d82dd55e5fad1d606b8a1ad2f7a804e10caa2efbb169cd37e0a07ede0" dependencies = [ "bstr", + "gix-trace", "home", "once_cell", "thiserror", @@ -1449,9 +1456,9 @@ dependencies = [ [[package]] name = "gix-prompt" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e15fe57fa48572b7d3bf465d6a2a0351cd3c55cba74fd5f0b9c23689f9c1a31e" +checksum = "8dfd363fd89a40c1e7bff9c9c1b136cd2002480f724b0c627c1bc771cd5480ec" dependencies = [ "gix-command", "gix-config-value", @@ -1462,9 +1469,9 @@ dependencies = [ [[package]] name = "gix-quote" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29d59489bff95b06dcdabe763b7266d3dc0a628cac1ac1caf65a7ca0a43eeae0" +checksum = "3874de636c2526de26a3405b8024b23ef1a327bebf4845d770d00d48700b6a40" dependencies = [ "bstr", "btoi", @@ -1521,9 +1528,9 @@ dependencies = [ [[package]] name = "gix-sec" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b7b38b766eb95dcc5350a9c450030b69892c0902fa35f4a6d0809273bd9dae" +checksum = "47f09860e2ddc7b13119e410c46d8e9f870acc7933fb53ae65817af83a8c9f80" dependencies = [ "bitflags 2.3.2", "gix-path", @@ -1546,6 +1553,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "gix-trace" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff8a60073500f4d6edd181432ee11394d843db7dcf05756aa137a1233b1cbf6" + [[package]] name = "gix-traverse" version = "0.25.0" @@ -1574,18 +1587,18 @@ dependencies = [ [[package]] name = "gix-utils" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbcfcb150c7ef553d76988467d223254045bdcad0dc6724890f32fbe96415da5" +checksum = "1ca284c260845bc0724050aec59c7a596407678342614cdf5a1d69e044f29a36" dependencies = [ "fastrand", ] [[package]] name = "gix-validate" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ea5845b506c7728b9d89f4227cc369a5fc5a1d5b26c3add0f0d323413a3a60" +checksum = "8d092b594c8af00a3a31fe526d363ee8a51a6f29d8496cdb991ed2f01ec0ec13" dependencies = [ "bstr", "thiserror", @@ -1688,7 +1701,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", @@ -1707,6 +1720,12 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + [[package]] name = "heck" version = "0.4.1" @@ -1883,6 +1902,16 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + [[package]] name = "instant" version = "0.1.12" @@ -2061,6 +2090,7 @@ dependencies = [ "tokio", "tokio-stream", "tonic", + "tonic-reflection", "tower", "tracing", "tracing-subscriber", @@ -2140,7 +2170,7 @@ dependencies = [ "proc-macro2", "quote", "regex-syntax 0.6.29", - "syn 2.0.18", + "syn 2.0.20", ] [[package]] @@ -2226,7 +2256,7 @@ checksum = "4901771e1d44ddb37964565c654a3223ba41a594d02b8da471cc4464912b5cfa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.20", ] [[package]] @@ -2601,7 +2631,7 @@ checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.20", ] [[package]] @@ -3152,14 +3182,14 @@ checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.20", ] [[package]] name = "serde_json" -version = "1.0.97" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdf3bf93142acad5821c99197022e170842cdbc1c30482b98750c688c640842a" +checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" dependencies = [ "itoa", "ryu", @@ -3174,14 +3204,14 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.20", ] [[package]] name = "serde_spanned" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" dependencies = [ "serde", ] @@ -3350,9 +3380,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +checksum = "fcb8d4cebc40aa517dfb69618fa647a346562e67228e2236ae0042ee6ac14775" dependencies = [ "proc-macro2", "quote", @@ -3367,14 +3397,14 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "system-deps" -version = "6.1.0" +version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5fa6fb9ee296c0dc2df41a656ca7948546d061958115ddb0bcaae43ad0d17d2" +checksum = "30c2de8a4d8f4b823d634affc9cd2a74ec98c53a756f317e529a48046cbf71f3" dependencies = [ "cfg-expr", "heck", "pkg-config", - "toml 0.7.4", + "toml 0.7.5", "version-compare", ] @@ -3415,7 +3445,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.20", ] [[package]] @@ -3542,7 +3572,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.20", ] [[package]] @@ -3591,9 +3621,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" +checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240" dependencies = [ "serde", "serde_spanned", @@ -3603,20 +3633,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.10" +version = "0.19.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" +checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" dependencies = [ - "indexmap", + "indexmap 2.0.0", "serde", "serde_spanned", "toml_datetime", @@ -3629,6 +3659,7 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" dependencies = [ + "async-trait", "axum", "base64 0.21.2", "bytes", @@ -3641,6 +3672,7 @@ dependencies = [ "hyper-timeout", "percent-encoding", "pin-project", + "prost", "tokio", "tokio-stream", "tower", @@ -3649,6 +3681,19 @@ dependencies = [ "tracing", ] +[[package]] +name = "tonic-reflection" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0543d7092032041fbeac1f2c84304537553421a11a623c2301b12ef0264862c7" +dependencies = [ + "prost", + "prost-types", + "tokio", + "tokio-stream", + "tonic", +] + [[package]] name = "tower" version = "0.4.13" @@ -3657,7 +3702,7 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", - "indexmap", + "indexmap 1.9.3", "pin-project", "pin-project-lite", "rand", @@ -3696,13 +3741,13 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8803eee176538f94ae9a14b55b2804eb7e1441f8210b1c31290b3bccdccff73b" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.20", ] [[package]] @@ -4061,7 +4106,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.20", "wasm-bindgen-shared", ] @@ -4083,7 +4128,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.20", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index b1a827c..dc52189 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,6 +79,7 @@ windows = { version = "0.48.0", features = ["Win32_System_LibraryLoader", "Win32 time = { version = "0.3.22", default-features = false, features = ["parsing", "serde", "serde-well-known"] } shell-words = "1.1.0" http-serde = "1.1.2" +tonic-reflection = "0.9.2" [build-dependencies] anyhow = "1.0.71" diff --git a/src/app/body/method/controller.rs b/src/app/body/method/controller.rs index 0150287..c980570 100644 --- a/src/app/body/method/controller.rs +++ b/src/app/body/method/controller.rs @@ -6,9 +6,10 @@ use tonic::metadata::MetadataMap; use crate::{ app::{ - body::{fmt_connect_err, method::MethodTabState, RequestState}, - command, fmt_err, + body::{method::MethodTabState, RequestState}, + command, }, + error::{fmt_connect_err, fmt_err}, grpc, json::JsonText, widget::update_queue::{self, UpdateQueue}, diff --git a/src/app/body/method/stream/item.rs b/src/app/body/method/stream/item.rs index 3ee6f43..d3213cd 100644 --- a/src/app/body/method/stream/item.rs +++ b/src/app/body/method/stream/item.rs @@ -8,17 +8,16 @@ use druid::{ }; use prost_reflect::{DescriptorPool, DynamicMessage, Value}; use serde::{Deserialize, Serialize}; -use tonic::{metadata::MetadataMap, Code, Status}; +use tonic::{metadata::MetadataMap, Status}; -use crate::{ - app::body::fmt_connect_err, - grpc, lens, - theme::INVALID, - widget::{code_area, error_label, Empty}, -}; use crate::{ app::metadata, + error::fmt_grpc_err, + grpc, json::{self, JsonText}, + lens, + theme::INVALID, + widget::{code_area, empty, error_label}, }; #[derive(Debug, Clone, Data, Serialize, Deserialize)] @@ -52,7 +51,7 @@ pub(in crate::app) fn build() -> impl Widget { code_area(false) .env_scope(|env: &mut Env, _: &JsonText| env.set(INVALID, true)) }, - || Empty, + empty, ) .lens(ErrorDetail::details), ) @@ -191,37 +190,3 @@ fn error_details(pool: &DescriptorPool, err: &anyhow::Error) -> Option ArcStr { - if let Some(status) = err.downcast_ref::() { - if status.message().is_empty() { - fmt_code(status.code()).into() - } else { - format!("{}: {}", fmt_code(status.code()), status.message()).into() - } - } else { - fmt_connect_err(err) - } -} - -fn fmt_code(code: Code) -> &'static str { - match code { - Code::Ok => "OK", - Code::Cancelled => "CANCELLED", - Code::Unknown => "UNKNOWN", - Code::InvalidArgument => "INVALID_ARGUMENT", - Code::DeadlineExceeded => "DEADLINE_EXCEEDED", - Code::NotFound => "NOT_FOUND", - Code::AlreadyExists => "ALREADY_EXISTS", - Code::PermissionDenied => "PERMISSION_DENIED", - Code::ResourceExhausted => "RESOURCE_EXHAUSTED", - Code::FailedPrecondition => "FAILED_PRECONDITION", - Code::Aborted => "ABORTED", - Code::OutOfRange => "OUT_OF_RANGE", - Code::Unimplemented => "UNIMPLEMENTED", - Code::Internal => "INTERNAL", - Code::Unavailable => "UNAVAILABLE", - Code::DataLoss => "DATA_LOSS", - Code::Unauthenticated => "UNAUTHENTICATED", - } -} diff --git a/src/app/body/mod.rs b/src/app/body/mod.rs index 299732a..52db545 100644 --- a/src/app/body/mod.rs +++ b/src/app/body/mod.rs @@ -6,7 +6,7 @@ mod reflection; pub(in crate::app) use self::{compile::CompileOptions, method::StreamState}; -use std::{collections::BTreeMap, io, mem, ops::Bound, sync::Arc}; +use std::{collections::BTreeMap, mem, ops::Bound, sync::Arc}; use druid::{lens::Field, widget::ViewSwitcher, ArcStr, Data, Lens, Widget, WidgetExt as _}; use iter_set::Inclusion; @@ -17,7 +17,7 @@ use self::{ reflection::ReflectionTabState, }; use crate::{ - app::{command, fmt_err, metadata, sidebar::service::ServiceOptions}, + app::{command, metadata, sidebar::service::ServiceOptions}, json::JsonText, widget::{tabs, TabId, TabLabelState, TabsData, TabsDataChange}, }; @@ -89,6 +89,19 @@ impl State { Arc::make_mut(&mut self.tabs).insert(id, TabState::new_compile(compile_options)); } + pub fn select_or_create_reflection_tab(&mut self) { + for (&id, tab) in self.tabs.iter() { + if matches!(tab, TabState::Reflection(_)) { + self.selected = Some(id); + return; + } + } + + let id = TabId::next(); + self.selected = Some(id); + Arc::make_mut(&mut self.tabs).insert(id, TabState::empty_reflection()); + } + pub fn select_or_create_method_tab( &mut self, method: &MethodDescriptor, @@ -368,6 +381,10 @@ impl TabState { TabState::Reflection(ReflectionTabState::new(options)) } + pub fn empty_reflection() -> TabState { + TabState::Reflection(ReflectionTabState::default()) + } + pub fn label(&self) -> ArcStr { match self { TabState::Method(method) => method.method().name().into(), @@ -545,11 +562,3 @@ impl TabsData for State { && !cmd.is(command::FINISH) } } - -pub fn fmt_connect_err(err: &anyhow::Error) -> ArcStr { - if let Some(err) = err.root_cause().downcast_ref::() { - format!("failed to connect: {}", err).into() - } else { - fmt_err(err) - } -} diff --git a/src/app/body/options/auth.rs b/src/app/body/options/auth.rs index 5de764a..a9953d2 100644 --- a/src/app/body/options/auth.rs +++ b/src/app/body/options/auth.rs @@ -8,8 +8,8 @@ use druid::{ use once_cell::sync::Lazy; use crate::{ - app::fmt_err, auth::AuthorizationHook, + error::fmt_err, lens, theme::{self, BODY_PADDING}, widget::{ diff --git a/src/app/body/options/controller.rs b/src/app/body/options/controller.rs index faa2fba..3aa1da1 100644 --- a/src/app/body/options/controller.rs +++ b/src/app/body/options/controller.rs @@ -5,9 +5,10 @@ use druid::{ use crate::{ app::{ - body::{fmt_connect_err, options::OptionsTabState, RequestState}, + body::{options::OptionsTabState, RequestState}, command, }, + error::fmt_connect_err, grpc, widget::update_queue::{self, UpdateQueue}, }; diff --git a/src/app/body/reflection/controller.rs b/src/app/body/reflection/controller.rs new file mode 100644 index 0000000..554e077 --- /dev/null +++ b/src/app/body/reflection/controller.rs @@ -0,0 +1,226 @@ +use std::sync::Arc; + +use anyhow::{bail, Context, Result}; +use druid::{ + widget::{prelude::*, Controller}, + Command, Handled, Target, +}; +use http::Uri; +use prost_reflect::{DescriptorPool, ServiceDescriptor}; +use tokio::sync::{mpsc, Mutex}; +use tokio_stream::wrappers::UnboundedReceiverStream; +use tonic::{metadata::MetadataMap, Code, Extensions, Request, Status, Streaming}; +use tonic_reflection::pb::{ + server_reflection_client::ServerReflectionClient, server_reflection_request::MessageRequest, + server_reflection_response::MessageResponse, ServerReflectionRequest, ServerReflectionResponse, +}; + +use crate::{ + app::{ + body::{ + reflection::{ReflectionTabState, IMPORT_SERVICE, LIST_SERVICES}, + RequestState, + }, + command, + }, + error::fmt_grpc_err, + grpc, + widget::update_queue::{self, UpdateQueue}, +}; + +pub struct ReflectionController { + updates: UpdateQueue, + session: Option>>, +} + +struct ReflectionSession { + sender: mpsc::UnboundedSender, + receiver: Streaming, + host: String, + services: Arc>, + pool: DescriptorPool, +} + +impl Controller for ReflectionController +where + W: Widget, +{ + fn event( + &mut self, + child: &mut W, + ctx: &mut EventCtx, + event: &Event, + data: &mut ReflectionTabState, + env: &Env, + ) { + match event { + Event::Command(command) if self.command(ctx, command, data) == Handled::Yes => (), + _ => child.event(ctx, event, data, env), + } + } +} + +impl ReflectionController { + pub fn new() -> Self { + ReflectionController { + updates: UpdateQueue::new(), + session: None, + } + } + + fn command( + &mut self, + ctx: &mut EventCtx, + command: &Command, + data: &mut ReflectionTabState, + ) -> Handled { + tracing::debug!("Options tab received command: {:?}", command); + + if command.is(LIST_SERVICES) { + self.list_services(ctx, data); + Handled::Yes + } else if let Some(service) = command.get(IMPORT_SERVICE) { + self.import_service(ctx, data, service.clone()); + Handled::Yes + } else if command.is(update_queue::UPDATE) { + while let Some(update) = self.updates.pop() { + (update)(self, ctx, data) + } + Handled::Yes + } else { + Handled::No + } + } + + fn list_services(&self, ctx: &mut EventCtx<'_, '_>, data: &mut ReflectionTabState) { + data.address + .set_request_state(RequestState::ConnectInProgress); + + let Some(address) = data.address.uri().cloned() else { + tracing::warn!("list-services called with invalid uri"); + return; + }; + let verify_certs = data.verify_certs; + let metadata = data.metadata.metadata(); + + let writer = self.updates.writer(ctx); + tokio::spawn(async move { + let result = ReflectionSession::connect(address, verify_certs, metadata).await; + writer.write(|controller, _, data| match result { + Ok(session) => { + data.address.set_request_state(RequestState::Connected); + data.services = Some(session.services.clone()); + controller.session = Some(Arc::new(Mutex::new(session))); + } + Err(err) => data + .address + .set_request_state(RequestState::ConnectFailed(fmt_grpc_err(&err))), + }); + }); + } + + fn import_service( + &self, + ctx: &mut EventCtx<'_, '_>, + data: &mut ReflectionTabState, + name: String, + ) { + let Some(session) = self.session.clone() else { + tracing::warn!("import-service called without session"); + return; + }; + + let service_options = data.service_options(); + + let writer = self.updates.writer(ctx); + tokio::spawn(async move { + match ReflectionSession::load_service(session, name).await { + Ok(service) => writer.submit_command( + command::ADD_SERVICE, + (service, service_options), + Target::Auto, + ), + Err(err) => writer.write(move |_, _, data| { + data.address + .set_request_state(RequestState::ConnectFailed(fmt_grpc_err(&err))); + }), + } + }); + } +} + +impl ReflectionSession { + async fn connect(address: Uri, verify_certs: bool, metadata: MetadataMap) -> Result { + let channel = grpc::channel::get(&address, verify_certs).await?; + let mut client = ServerReflectionClient::new(channel); + + let (sender, request_receiver) = mpsc::unbounded_channel::(); + let mut receiver = client + .server_reflection_info(Request::from_parts( + metadata, + Extensions::default(), + UnboundedReceiverStream::new(request_receiver), + )) + .await? + .into_inner(); + + let host = address.host().unwrap_or_default().to_owned(); + sender.send(ServerReflectionRequest { + host: host.clone(), + message_request: Some(MessageRequest::ListServices(String::default())), + })?; + let Some(response) = receiver.message().await? else { + bail!("unexpected end of response stream"); + }; + let service_list = match response.message_response { + Some(MessageResponse::ListServicesResponse(service_list)) => service_list, + Some(MessageResponse::ErrorResponse(error)) => { + return Err( + Status::new(Code::from_i32(error.error_code), error.error_message).into(), + ) + } + _ => bail!("unexpected response type"), + }; + + Ok(ReflectionSession { + sender, + receiver, + host, + services: Arc::new(service_list.service.into_iter().map(|s| s.name).collect()), + pool: DescriptorPool::new(), + }) + } + + async fn load_service(this: Arc>, name: String) -> Result { + let mut this = this.lock().await; + + this.sender.send(ServerReflectionRequest { + host: this.host.clone(), + message_request: Some(MessageRequest::FileContainingSymbol(name.clone())), + })?; + let Some(response) = this.receiver.message().await? else { + bail!("unexpected end of response stream"); + }; + let file_response = match response.message_response { + Some(MessageResponse::FileDescriptorResponse(file_response)) => file_response, + Some(MessageResponse::ErrorResponse(error)) => { + return Err( + Status::new(Code::from_i32(error.error_code), error.error_message).into(), + ) + } + _ => bail!("unexpected response type"), + }; + + for file in file_response.file_descriptor_proto { + this.pool + .decode_file_descriptor_proto(file.as_ref()) + .context("failed to load file descriptor from server")?; + } + + let Some(service) = this.pool.get_service_by_name(&name) else { + bail!("service '{}' not found in file descriptor from server", name) + }; + + Ok(service) + } +} diff --git a/src/app/body/reflection/mod.rs b/src/app/body/reflection/mod.rs index 402beda..39827eb 100644 --- a/src/app/body/reflection/mod.rs +++ b/src/app/body/reflection/mod.rs @@ -1,26 +1,128 @@ -use druid::{widget::prelude::*, Lens}; +mod controller; -use crate::{app::sidebar::service::ServiceOptions, widget::Empty}; +use std::sync::Arc; -#[derive(Debug, Clone, Data, Lens)] +use druid::{ + widget::{prelude::*, Button, Checkbox, CrossAxisAlignment, Flex, Label, List, Maybe, Scroll}, + Lens, Selector, WidgetExt, +}; + +use crate::{ + app::{ + body::address::{self, AddressState}, + metadata, + sidebar::service::ServiceOptions, + }, + theme::{self, BODY_SPACER, GRID_NARROW_SPACER}, + widget::{empty, readonly_input, Icon}, +}; + +use self::controller::ReflectionController; + +/// Connect +pub const LIST_SERVICES: Selector = Selector::new("app.body.reflection.list-services"); +pub const IMPORT_SERVICE: Selector = Selector::new("app.body.reflection.import-service"); + +#[derive(Default, Debug, Clone, Data, Lens)] pub struct ReflectionTabState { - options: ServiceOptions, + address: AddressState, + verify_certs: bool, + metadata: metadata::EditableState, + services: Option>>, } pub fn build_body() -> impl Widget { - Empty + let id = WidgetId::next(); + + let tls_checkbox = theme::check_box_scope(Checkbox::new("Enable certificate verification")); + + Scroll::new( + Flex::column() + .with_child(Label::new("Default address").with_font(theme::font::HEADER_TWO)) + .with_spacer(theme::BODY_SPACER) + .with_child(build_address_bar(id)) + .with_spacer(theme::BODY_SPACER) + .with_child(tls_checkbox.lens(ReflectionTabState::verify_certs)) + .with_spacer(theme::BODY_SPACER) + .with_child(Label::new("Metadata").with_font(theme::font::HEADER_TWO)) + .with_spacer(theme::BODY_SPACER) + .with_child(metadata::build_editable().lens(ReflectionTabState::metadata)) + .with_child( + Maybe::new(move || build_service_list(id), empty) + .lens(ReflectionTabState::services), + ) + .cross_axis_alignment(CrossAxisAlignment::Start) + .padding(theme::BODY_PADDING) + .controller(ReflectionController::new()) + .with_id(id), + ) + .vertical() + .expand_height() +} + +fn build_address_bar(parent: WidgetId) -> impl Widget { + let address_form_field = address::build(parent); + + let list_button = theme::button_scope(Button::new("List services").on_click( + move |ctx: &mut EventCtx, _: &mut ReflectionTabState, _: &Env| { + ctx.submit_command(LIST_SERVICES.to(parent)); + }, + )) + .disabled_if(|data: &ReflectionTabState, _| !data.can_send()); + + Flex::row() + .with_flex_child(address_form_field.lens(ReflectionTabState::address), 1.0) + .with_child(list_button) + .cross_axis_alignment(CrossAxisAlignment::Start) +} + +fn build_service_list(parent: WidgetId) -> impl Widget>> { + Flex::column() + .with_spacer(BODY_SPACER) + .with_child(Label::new("Available services").with_font(theme::font::HEADER_TWO)) + .with_spacer(BODY_SPACER) + .with_child(List::new(move || build_service_row(parent)).with_spacing(GRID_NARROW_SPACER)) + .cross_axis_alignment(CrossAxisAlignment::Start) +} + +fn build_service_row(parent: WidgetId) -> impl Widget { + Flex::row() + .with_flex_child(readonly_input(), 1.0) + .with_spacer(GRID_NARROW_SPACER) + .with_child( + Icon::add() + .background(theme::hot_or_active_painter( + druid::theme::BUTTON_BORDER_RADIUS, + )) + .on_click(move |ctx: &mut EventCtx, data: &mut String, _| { + ctx.submit_command(IMPORT_SERVICE.with(data.clone()).to(parent)); + }), + ) } impl ReflectionTabState { pub fn new(options: ServiceOptions) -> ReflectionTabState { - ReflectionTabState { options } + ReflectionTabState { + address: match options.default_address { + Some(uri) => AddressState::new(uri.to_string()), + None => AddressState::default(), + }, + verify_certs: options.verify_certs, + metadata: metadata::EditableState::new(options.default_metadata), + services: None, + } } - pub fn can_send(&self) -> bool { - todo!() + pub fn service_options(&self) -> ServiceOptions { + ServiceOptions { + default_address: self.address.uri().cloned(), + verify_certs: self.verify_certs, + default_metadata: self.metadata.to_state(), + auth_hook: None, + } } - pub fn service_options(&self) -> &ServiceOptions { - &self.options + pub fn can_send(&self) -> bool { + self.address.is_valid() && self.metadata.is_valid() } } diff --git a/src/app/command.rs b/src/app/command.rs index d699c03..ca2f89b 100644 --- a/src/app/command.rs +++ b/src/app/command.rs @@ -20,6 +20,10 @@ pub const SELECT_OR_CREATE_METHOD_TAB: Selector = pub const SELECT_OR_CREATE_COMPILE_TAB: Selector = Selector::new("app.select-or-create-compile-tab"); +/// Select or create a server reflection tab. +pub const SELECT_OR_CREATE_REFLECTION_TAB: Selector = + Selector::new("app.select-or-create-reflection-tab"); + /// Set compiler options pub const SET_COMPILE_OPTIONS: Selector = Selector::new("app.set-compile-options"); @@ -27,6 +31,10 @@ pub const SET_COMPILE_OPTIONS: Selector = Selector::new("app.set pub const SET_SERVICE_OPTIONS: Selector<(ServiceDescriptor, ServiceOptions)> = Selector::new("app.set-service-options"); +/// Add a service +pub const ADD_SERVICE: Selector<(ServiceDescriptor, ServiceOptions)> = + Selector::new("app.add-service"); + /// Remove a service pub const REMOVE_SERVICE: Selector = Selector::new("app.remove-service"); diff --git a/src/app/delegate.rs b/src/app/delegate.rs index a6a2118..3c1627f 100644 --- a/src/app/delegate.rs +++ b/src/app/delegate.rs @@ -1,7 +1,10 @@ use anyhow::Result; use druid::{AppDelegate, Command, DelegateCtx, Env, Handled, Target, WindowHandle, WindowId}; -use crate::app::{self, command, fmt_err}; +use crate::{ + app::{self, command}, + error::fmt_err, +}; pub(in crate::app) fn build() -> impl AppDelegate { Delegate @@ -61,6 +64,9 @@ impl AppDelegate for Delegate { data.body .select_or_create_compiler_tab(data.sidebar.compile_options()); Handled::Yes + } else if cmd.is(command::SELECT_OR_CREATE_REFLECTION_TAB) { + data.body.select_or_create_reflection_tab(); + Handled::Yes } else if let Some((service, options)) = cmd.get(command::SET_SERVICE_OPTIONS) { data.body.set_service_options(service, options); data.sidebar.set_service_options(service, options); @@ -77,6 +83,9 @@ impl AppDelegate for Delegate { .select_or_create_method_tab(method, options.clone()); } Handled::Yes + } else if let Some((service, options)) = cmd.get(command::ADD_SERVICE) { + data.sidebar.add_service(service.clone(), options.clone()); + Handled::Yes } else if let Some(service_index) = cmd.get(command::REMOVE_SERVICE) { let service = data.sidebar.remove_service(*service_index); data.body.remove_service(service.service()); diff --git a/src/app/menu.rs b/src/app/menu.rs index 5db73c6..709b414 100644 --- a/src/app/menu.rs +++ b/src/app/menu.rs @@ -27,6 +27,10 @@ fn file_menu() -> Menu { .entry( MenuItem::new("Compiler options") .command(app::command::SELECT_OR_CREATE_COMPILE_TAB), + ) + .entry( + MenuItem::new("Server reflection") + .command(app::command::SELECT_OR_CREATE_REFLECTION_TAB), ), ) .separator() diff --git a/src/app/mod.rs b/src/app/mod.rs index d03a16e..3e4549c 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -122,21 +122,3 @@ impl State { SidebarLens } } - -fn fmt_err(err: &anyhow::Error) -> ArcStr { - use std::fmt::Write; - - let mut s = String::new(); - for cause in err.chain() { - if !s.is_empty() { - s.push_str(": "); - } - let len = s.len(); - write!(s, "{}", cause).unwrap(); - if s[..len].contains(&s[len..]) { - s.truncate(len.saturating_sub(2)); - break; - } - } - s.into() -} diff --git a/src/app/serde.rs b/src/app/serde.rs index 2c7c8f1..ffefadf 100644 --- a/src/app/serde.rs +++ b/src/app/serde.rs @@ -174,7 +174,7 @@ impl<'a> TryFrom<&'a app::State> for AppState { } app::body::TabState::Compile(_) => AppBodyTabKind::Compile, app::body::TabState::Reflection(options) => AppBodyTabKind::Reflection { - options: options.service_options().clone(), + options: options.service_options(), }, }; diff --git a/src/app/sidebar/mod.rs b/src/app/sidebar/mod.rs index 659dd53..bdc9d33 100644 --- a/src/app/sidebar/mod.rs +++ b/src/app/sidebar/mod.rs @@ -13,6 +13,8 @@ use prost_reflect::ServiceDescriptor; use crate::{app::command, protoc, theme, widget::Icon}; +use self::service::ServiceOptions; + use super::body::CompileOptions; #[derive(Debug, Default, Clone, Data, Lens)] @@ -100,6 +102,11 @@ impl ServiceListState { &self.services } + pub fn add_service(&mut self, service: ServiceDescriptor, options: ServiceOptions) { + self.services + .push_back(service::ServiceState::new(service.clone(), true, options)); + } + pub fn remove_service(&mut self, index: usize) -> service::ServiceState { self.services.remove(index) } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..eeaf587 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,64 @@ +use std::io; + +use druid::ArcStr; +use tonic::{Code, Status}; + +pub fn fmt_err(err: &anyhow::Error) -> ArcStr { + use std::fmt::Write; + + let mut s = String::new(); + for cause in err.chain() { + if !s.is_empty() { + s.push_str(": "); + } + let len = s.len(); + write!(s, "{}", cause).unwrap(); + if s[..len].contains(&s[len..]) { + s.truncate(len.saturating_sub(2)); + break; + } + } + s.into() +} + +pub fn fmt_connect_err(err: &anyhow::Error) -> ArcStr { + if let Some(err) = err.root_cause().downcast_ref::() { + format!("failed to connect: {}", err).into() + } else { + fmt_err(err) + } +} + +pub fn fmt_grpc_err(err: &anyhow::Error) -> ArcStr { + if let Some(status) = err.downcast_ref::() { + if status.message().is_empty() { + fmt_code(status.code()).into() + } else { + format!("{}: {}", fmt_code(status.code()), status.message()).into() + } + } else { + fmt_connect_err(err) + } +} + +fn fmt_code(code: Code) -> &'static str { + match code { + Code::Ok => "OK", + Code::Cancelled => "CANCELLED", + Code::Unknown => "UNKNOWN", + Code::InvalidArgument => "INVALID_ARGUMENT", + Code::DeadlineExceeded => "DEADLINE_EXCEEDED", + Code::NotFound => "NOT_FOUND", + Code::AlreadyExists => "ALREADY_EXISTS", + Code::PermissionDenied => "PERMISSION_DENIED", + Code::ResourceExhausted => "RESOURCE_EXHAUSTED", + Code::FailedPrecondition => "FAILED_PRECONDITION", + Code::Aborted => "ABORTED", + Code::OutOfRange => "OUT_OF_RANGE", + Code::Unimplemented => "UNIMPLEMENTED", + Code::Internal => "INTERNAL", + Code::Unavailable => "UNAVAILABLE", + Code::DataLoss => "DATA_LOSS", + Code::Unauthenticated => "UNAUTHENTICATED", + } +} diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index 62e9771..b0a3962 100644 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -1,4 +1,4 @@ -mod channel; +pub mod channel; mod codec; use std::{ diff --git a/src/lib.rs b/src/lib.rs index 76fd21e..aeb699d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod app; mod auth; +mod error; mod grpc; mod json; mod lens; diff --git a/src/widget/empty.rs b/src/widget/empty.rs index ce47709..1f8020b 100644 --- a/src/widget/empty.rs +++ b/src/widget/empty.rs @@ -2,6 +2,10 @@ use druid::widget::prelude::*; pub struct Empty; +pub fn empty() -> Empty { + Empty +} + impl Widget for Empty { fn event(&mut self, _: &mut EventCtx, _: &Event, _: &mut T, _: &Env) {} diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 4d6fc8a..f3d8d60 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -19,7 +19,7 @@ use crate::theme; pub use self::{ editable_list::EditableList, - empty::Empty, + empty::{empty, Empty}, expander::ExpanderData, form_field::{FinishEditController, FormField, ValidationFn, ValidationState, FINISH_EDIT}, icon::Icon, @@ -103,6 +103,6 @@ pub fn error_label(insets: impl Into) -> impl Widget> { ) .padding(insets) }, - || Empty, + empty, ) } diff --git a/src/widget/update_queue.rs b/src/widget/update_queue.rs index 437e0ab..834c2a4 100644 --- a/src/widget/update_queue.rs +++ b/src/widget/update_queue.rs @@ -1,7 +1,7 @@ use std::sync::{Arc, Weak}; use crossbeam_queue::SegQueue; -use druid::{EventCtx, ExtEventSink, Selector, WidgetId}; +use druid::{EventCtx, ExtEventSink, Selector, Target, WidgetId}; pub const UPDATE: Selector = Selector::new("app.update"); @@ -42,14 +42,17 @@ impl UpdateQueue { } impl UpdateQueueWriter { - pub fn write(&self, f: impl FnOnce(&mut W, &mut EventCtx, &mut T) + Send + 'static) -> bool { + pub fn write(&self, f: impl FnOnce(&mut W, &mut EventCtx, &mut T) + Send + 'static) { if let Some(queue) = self.queue.upgrade() { queue.push(Box::new(f)); - self.event_sink - .submit_command(UPDATE, (), self.target) - .is_ok() - } else { - false + _ = self.event_sink.submit_command(UPDATE, (), self.target); } } + + pub fn submit_command(&self, selector: Selector, payload: U, target: impl Into) + where + U: Send + 'static, + { + _ = self.event_sink.submit_command(selector, payload, target); + } }